diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2b6254c755..5698586e43 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -62,3 +62,5 @@ e8fc526e0d7818d45f171488c78392c4ff63902a 890b4633c5477dc074f79c69c40d650196337591 1972cef6bba0d97169b30a53f540d56bcbd97281 cdf40d265cc82775607a1bf25f5f527bacc97405 +251e389b361ba673b508e07d04ddcc06b2681989 +8ec50135eca1b99c8b903ecdaa1bd436644688bd diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 86ca420414..0000000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: black check on push and PR -# -# Run the python formatting in check mode -# -on: [push, pull_request] - -jobs: - black-check: - runs-on: ubuntu-latest - steps: - # Checkout the code - - uses: actions/checkout@v2 - # Use the latest stable version of the github action - - uses: psf/black@stable - with: - # Use options and version identical to the conda environment - # Using pyproject.toml makes sure this testing is consistent with our python directory testing - options: "--check --config python/pyproject.toml" - src: "./python" - # Version should be coordinated with the ctsm_pylib conda environment under the python directory - version: "25.1.0" - # Actions identical to above for each directory and source file we need to check (arrays aren't allowed for src: field) - - uses: psf/black@stable - with: - options: "--check --config python/pyproject.toml" - src: "./cime_config/SystemTests" - version: "25.1.0" - - uses: psf/black@stable - with: - options: "--check --config python/pyproject.toml" - src: "./cime_config/buildlib" - version: "25.1.0" - - uses: psf/black@stable - with: - options: "--check --config python/pyproject.toml" - src: "./cime_config/buildnml" - version: "25.1.0" diff --git a/.github/workflows/fleximod_test.yaml b/.github/workflows/fleximod_test.yaml new file mode 100644 index 0000000000..7f3e5a1404 --- /dev/null +++ b/.github/workflows/fleximod_test.yaml @@ -0,0 +1,31 @@ +name: git-fleximod test +# +# Test git-fleximod update and cleanliness +# Based closely on workflow from CESM repo +# +on: [push, pull_request] + +jobs: + fleximod-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # oldest supported and latest supported + python-version: ["3.7", "3.x"] + steps: + - id: checkout-CTSM + uses: actions/checkout@v4 + - id: run-fleximod + run: | + $GITHUB_WORKSPACE/bin/git-fleximod update + echo + echo "Update complete, checking status" + echo + $GITHUB_WORKSPACE/bin/git-fleximod test + - id: check-cleanliness + run: | + echo + echo "Checking if git fleximod matches expected externals" + echo + git diff --exit-code diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml new file mode 100644 index 0000000000..6fffd4261b --- /dev/null +++ b/.github/workflows/formatting_python.yml @@ -0,0 +1,44 @@ +name: Check Python formatting + +on: + push: + paths: + - 'python/**' + - 'cime_config/SystemTests/**' + - 'cime_config/buildlib/**' + - 'cime_config/buildnml/**' + pull_request: + paths: + - 'python/**' + - 'cime_config/SystemTests/**' + - 'cime_config/buildlib/**' + - 'cime_config/buildnml/**' + +jobs: + lint-and-format-check: + runs-on: ubuntu-latest + steps: + # Checkout the code + - uses: actions/checkout@v4 + + # Set up the conda environment + - uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: ctsm_pylib + environment-file: python/conda_env_ctsm_py.yml + channels: conda-forge + auto-activate-base: false + + # Run pylint check + - name: Run pylint + run: | + cd python + conda run -n ctsm_pylib make lint + + # Run black check + - name: Run black + # Run this step even if previous step(s) failed + if: success() || failure() + run: | + cd python + conda run -n ctsm_pylib make black diff --git a/.gitmodules b/.gitmodules index 2787973b6d..e569896811 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,7 +31,7 @@ url = https://github.com/NGEET/fates fxtag = sci.1.82.3_api.39.0.0 fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed -fxDONOTUSEurl = https://github.com/NCAR/fates-release +fxDONOTUSEurl = https://github.com/NGEET/fates [submodule "cism"] path = components/cism diff --git a/bld/namelist_files/namelist_definition_ctsm.xml b/bld/namelist_files/namelist_definition_ctsm.xml index 3b580af6d2..fbac021e57 100644 --- a/bld/namelist_files/namelist_definition_ctsm.xml +++ b/bld/namelist_files/namelist_definition_ctsm.xml @@ -9,7 +9,7 @@ - Full pathname to the inventory initialization control file. (Required, if use_fates_inventory_init=T) @@ -914,7 +914,7 @@ should be set to true for the spin-up case and false for the transient case. - Full pathname of unified land use harmonization (LUH) data file. This causes the land-use types to vary over time. @@ -922,7 +922,7 @@ types to vary over time. (Only relevant if FATES is on). - Full pathname of fates landuse x pft association static data map. The file associates land use types with pfts across a static global map. @@ -993,34 +993,34 @@ How LUNA and Photosynthesis (if needed) will get Leaf nitrogen content lnc_opt = false get based on LAI and fixed CN ratio from parameter file - Full pathname datafile with plant function type (PFT) constants combined with constants for biogeochem modules - Full pathname datafile with fates parameters (Only relevant if FATES is on). - Full pathname of surface data file. - Full pathname of hillslope data file. - SNICAR (SNow, ICe, and Aerosol Radiative model) optical data file name - SNICAR (SNow, ICe, and Aerosol Radiative model) snow aging data file name @@ -1559,32 +1559,32 @@ Toggle to turn on on diagnostic Snow Radiative Effect - Orography file with surface heights and land area fraction - CLM grid file - CESM domain file - CAM file - Raw topography file - CAM topography file @@ -1623,7 +1623,7 @@ Western edge of the regional grid - Historical greenhouse gas concentrations from CAM, only used by getco2_historical.ncl @@ -1632,22 +1632,22 @@ by getco2_historical.ncl - Aerosol deposition file name (only used for aerdepregrid.ncl) - Full pathname of CLM fraction dataset (only used for mkdatadomain). - Full pathname of CLM grid dataset (only used for mkdatadomain). - Full pathname of output domain dataset (only used for mkdatadomain). @@ -1750,12 +1750,12 @@ Last year to loop over for Nitrogen Deposition data Simulation year that aligns with stream_year_first_ndep value - Filename of input stream data for Nitrogen Deposition - Stream meshfile for Nitrogen Deposition data @@ -1799,12 +1799,12 @@ Mapping method from Nitrogen deposition input file to the model resolution If TRUE use the Prigent roughness dataset - Filename of input stream data for aeolian roughness length (from Prigent's roughness dataset) - mesh filename of input stream data for aeolian roughness length (from Prigent's roughness dataset) @@ -1826,13 +1826,13 @@ here in CTSM, or in the ATM model. copy = copy using the same indices - Filename of input stream data for Zender's soil erodibility source function (only used when dust_emis_method is Zender_2003, and zender_soil_erod_source is lnd) - mesh filename of input stream data for Zender's soil erodibility source function (only used when dust_emis_method is Zender_2003, and zender_soil_erod_source is lnd) @@ -1842,13 +1842,13 @@ mesh filename of input stream data for Zender's soil erodibility source function - Filename of input stream data for finundated inversion of observed (from Prigent dataset) to hydrologic variables (either TWS or ZWT) - mesh filename of input stream data for finundated inversion of observed (from Prigent dataset) to hydrologic variables (either TWS or ZWT) @@ -1875,7 +1875,7 @@ Last year to loop over for prescribed soil moisture streams data Simulation year that aligns with stream_year_first_soilm value - Filename of input stream data for prescribed soil moisture streams data @@ -1924,12 +1924,12 @@ Last year to loop over for LAI data Simulation year that aligns with stream_year_first_lai value - Filename of input stream data for LAI - Filename of input stream data for LAI @@ -2006,22 +2006,22 @@ Simulation year that aligns with stream_year_first_cropcal_cultivar_gdds value By default, a value in stream_fldFileName_swindow_start or _end outside the range [1, 365] (or 366 in leap years) will cause the run to fail. Set this to .true. to instead fall back on the paramfile sowing windows. - Filename of input stream data for date (day of year) of start of sowing window. Cells with the same sowing window start and end date are always planted on that date, regardless of climatic conditions/history. - Filename of input stream data for date (day of year) of end of sowing window. Cells with the same sowing window start and end date are always planted on that date, regardless of climatic conditions/history. - Filename of input stream data for cultivar growing degree-day targets - Filename of input stream data for baseline GDD20 values @@ -2041,17 +2041,17 @@ Set this to true to flush the accumulated GDD20 variables as soon as possible. By default, a value in stream_fldFileName_gdd20_season_start or _end outside the range [1, 365] (or 366 in leap years) will cause the run to fail. Set this to .true. to instead have such cells fall back to the hard-coded hemisphere-specific "warm seasons." - Filename of input stream data for date (day of year) of start of gdd20 accumulation season. - Filename of input stream data for date (day of year) of end of gdd20 accumulation season. - Filename of input stream data for crop calendar inputs @@ -2076,12 +2076,12 @@ Last year to loop over for Lightning data Simulation year that aligns with stream_year_first_lightng value - Filename of input stream data for Lightning - Stream meshfile for Nitrogen Deposition data @@ -2123,12 +2123,12 @@ Last year to loop over for human population density data Simulation year that aligns with stream_year_first_popdens value - Filename of input stream data for human population density - mesh file for input stream data for human population density @@ -2170,12 +2170,12 @@ Last year to loop over for urban time varying data Simulation year that aligns with stream_year_first_urbantv value - Filename of input stream data for urban time varying - mesh filename of input stream data for urban time varying @@ -2196,15 +2196,15 @@ Mapping method from urban time varying input file to the model resolution copy = copy using the same indices - datm input directory - datm output directory - Datm logfile name @@ -2213,7 +2213,7 @@ Datm logfile name - Mapping file to go from one resolution/land-mask to another resolution/land-mask @@ -2465,7 +2465,7 @@ Enable C14 model Flag to use the atmospheric time series of C14 concentrations from bomb fallout and Seuss effect, rather than natural abundance C14 (nominally set as 10^-12 mol C14 / mol C) - Filename with time series of atmospheric Delta C14 data. variables in file are "time" and "Delta14co2_in_air". time variable is in format: years since 1850-01-01 0:0:0.0 units are permil. @@ -2475,7 +2475,7 @@ Filename with time series of atmospheric Delta C14 data. variables in file are Flag to use the atmospheric time series of C13 concentrations from natural abundance and the Seuss Effect, rather than static values. - Filename with time series of atmospheric Delta C13 data, which use CMIP6 format. variables in file are "time" and "delta13co2_in_air". time variable is in format: years since 1850-01-01 0:0:0.0. units are permil. @@ -2671,7 +2671,7 @@ net methane production. NOTE: Currently this must be TRUE. - Full pathname of time varying landuse data file. This causes the land-use types of the initial surface dataset to vary over time. @@ -3076,12 +3076,12 @@ if FALSE and use_excess_ice is TRUE, expect excess ice to come from the initial Expect to be FALSE is use_excess_ice is FALSE - Filename of input stream data for excess ice data - mesh filename of input stream data for excess ice diff --git a/cime_config/config_archive.xml b/cime_config/config_archive.xml index c219a1d1ef..e78861e451 100644 --- a/cime_config/config_archive.xml +++ b/cime_config/config_archive.xml @@ -8,12 +8,14 @@ e locfnh - rpointer.lnd$NINST_STRING + rpointer.lnd$NINST_STRING.$DATENAME ./$CASE.clm2$NINST_STRING.r.$DATENAME.nc - rpointer.lnd - rpointer.lnd_9999 + rpointer.lnd.1976-01-01-00000 + rpointer.lnd_9999.1976-01-01-00000 + rpointer.lnd.0000-01-01-00000 + rpointer.lnd_9999.1976-01-03-00000 casename.clm2.r.1976-01-01-00000.nc casename.clm2.rh4.1976-01-01-00000.nc casename.clm2.h0.1976-01-01-00000.nc @@ -32,12 +34,14 @@ e locfnh - rpointer.lnd$NINST_STRING + rpointer.lnd$NINST_STRING.$DATENAME ./$CASE.ctsm$NINST_STRING.r.$DATENAME.nc - rpointer.lnd - rpointer.lnd_9999 + rpointer.lnd.1976-01-01-00000 + rpointer.lnd_9999.1976-01-01-00000 + rpointer.lnd.0000-01-01-00000 + rpointer.lnd_9999.1976-01-03-00000 casename.ctsm.r.1976-01-01-00000.nc casename.ctsm.rh4.1976-01-01-00000.nc casename.ctsm.h0.1976-01-01-00000.nc diff --git a/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/shell_commands b/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/shell_commands index 94a832af25..db5a1f8672 100644 --- a/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/shell_commands +++ b/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/shell_commands @@ -1,7 +1,7 @@ SRCDIR=`./xmlquery SRCROOT --value` CASEDIR=`./xmlquery CASEROOT --value` FATESDIR=$SRCDIR/src/fates/ -FATESPARAMFILE=$SRCDIR/fates_params_seeddisp_4x5.nc +FATESPARAMFILE=$CASEDIR/fates_params_seeddisp_4x5.nc ncgen -o $FATESPARAMFILE $FATESDIR/parameter_files/fates_params_default.cdl diff --git a/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/user_nl_clm b/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/user_nl_clm index e8d24253c1..ecd1dc8b57 100644 --- a/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/user_nl_clm +++ b/cime_config/testdefs/testmods_dirs/clm/FatesColdSeedDisp/user_nl_clm @@ -1,3 +1,3 @@ -fates_paramfile = '$SRCROOT/fates_params_seeddisp_4x5.nc' +fates_paramfile = '$CASEROOT/fates_params_seeddisp_4x5.nc' fates_seeddisp_cadence = 1 hist_fincl1 = 'FATES_SEEDS_IN_GRIDCELL_PF', 'FATES_SEEDS_OUT_GRIDCELL_PF' diff --git a/doc/ChangeLog b/doc/ChangeLog index 0dce226ac6..89cba365be 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,4 +1,84 @@ =============================================================== +Tag name: ctsm5.3.041 +Originator(s): samrabin (Sam Rabin, UCAR/TSS) +Date: Fri Apr 25 08:02:37 MDT 2025 +One-line Summary: Merge b4b-dev to master + +Purpose and description of changes +---------------------------------- + +Merge b4b-dev to master. Includes PRs: +- [#3042: Allow file paths with length 512 by samsrabin](https://github.com/ESCOMP/CTSM/pull/3042) +- [#3024: Python scripts: Require user to specify lon format when ambiguous by samsrabin](https://github.com/ESCOMP/CTSM/pull/3024) +- [#3017: Avoid repeated ESMF initialization in unittestTimeManagerMod.F90 by billsacks](https://github.com/ESCOMP/CTSM/pull/3017) +- [#2964: Add GitHub workflows to check black, pylint, git-fleximod by samsrabin](https://github.com/ESCOMP/CTSM/pull/2964) +- [#3067: fix some st_archive issues with rpointer files by jedwards4b](https://github.com/ESCOMP/CTSM/pull/3067) + + +Significant changes to scientifically-supported configurations +-------------------------------------------------------------- + +Does this tag change answers significantly for any of the following physics configurations? +(Details of any changes will be given in the "Answer changes" section below.) + + [Put an [X] in the box for any configuration with significant answer changes.] + +[ ] clm6_0 + +[ ] clm5_0 + +[ ] ctsm5_0-nwp + +[ ] clm4_5 + + +Bugs fixed +---------- + +List of CTSM issues fixed: +- [Issue #2322: long parameter file path string in fates seed dispersal test](https://github.com/ESCOMP/CTSM/issues/2322) +- [Issue #2453: FUNIT test not working on izumi](https://github.com/ESCOMP/CTSM/issues/2453) +- [Issue #2651: Change all filepath variables to SHR_KIND_CX (512)](https://github.com/ESCOMP/CTSM/issues/2651) +- [Issue #3001: Make Python scripts smarter about lons (-180, 180) vs. (0, 360)](https://github.com/ESCOMP/CTSM/issues/3001) +- [Issue #3015: CNPhenology (and other?) unit tests not being run](https://github.com/ESCOMP/CTSM/issues/3015) +- [Issue #3041: Race condition with multiple FatesColdSeedDisp tests](https://github.com/ESCOMP/CTSM/issues/3041) +- [Issue #3076: rpointer files not moved to st_archiver](https://github.com/ESCOMP/CTSM/issues/3076) + + +Notes of particular relevance for users +--------------------------------------- + +Caveats for users: +- See updated `subset_data` docs + +Changes to documentation: +- Adds pFUnit testing design page +- Updates `subset_data` docs + + +Testing summary: +---------------- + + regular tests (aux_clm: https://github.com/ESCOMP/CTSM/wiki/System-Testing-Guide#pre-merge-system-testing): + + derecho ----- OK + izumi ------- OK + +Other details +------------- + +List any git submodules updated: +- FATES reference repo changed from NCAR/fates-release to NGEET/FATES. No version change. + +Pull Requests that document the changes (include PR ids): +- [#3042: Allow file paths with length 512 by samsrabin](https://github.com/ESCOMP/CTSM/pull/3042) +- [#3024: Python scripts: Require user to specify lon format when ambiguous by samsrabin](https://github.com/ESCOMP/CTSM/pull/3024) +- [#3017: Avoid repeated ESMF initialization in unittestTimeManagerMod.F90 by billsacks](https://github.com/ESCOMP/CTSM/pull/3017) +- [#2964: Add GitHub workflows to check black, pylint, git-fleximod by samsrabin](https://github.com/ESCOMP/CTSM/pull/2964) +- [#3067: fix some st_archive issues with rpointer files by jedwards4b](https://github.com/ESCOMP/CTSM/pull/3067) + +=============================================================== +=============================================================== Tag name: ctsm5.3.040 Originator(s): samrabin (Sam Rabin, UCAR/TSS) Date: Tue 15 Apr 2025 10:15:39 AM MDT diff --git a/doc/ChangeSum b/doc/ChangeSum index b1ce6cf594..c23981470d 100644 --- a/doc/ChangeSum +++ b/doc/ChangeSum @@ -1,5 +1,6 @@ Tag Who Date Summary ============================================================================================================================ + ctsm5.3.041 samrabin 04/25/2025 Merge b4b-dev to master ctsm5.3.040 samrabin 04/15/2025 Update ctsm_pylib to 3.13.2 ctsm5.3.039 afoster 04/14/2025 Create driver data structure for SP mode ctsm5.3.038 samrabin 04/10/2025 Merge b4b-dev diff --git a/doc/design/pfunit_testing.rst b/doc/design/pfunit_testing.rst new file mode 100644 index 0000000000..57e7e496dd --- /dev/null +++ b/doc/design/pfunit_testing.rst @@ -0,0 +1,37 @@ +.. sectnum:: + +.. contents:: + +================================== + Overview of this design document +================================== + +This document describes various design considerations for writing pFUnit-based unit tests. + +================= + Handling errors +================= + +When building the unit tests, we replace the standard ``shr_abort_mod`` with a version that throws a pFUnit exception instead of actually aborting. This is important for expected-error testing, where we want to check that the code aborts when it should: by throwing a pFUnit exception, we can check for this exception in the test. If we actually aborted, we wouldn't be able to catch and check this, and all remaining tests in the given module would be skipped. + +This behavior can sometimes be a problem, though, because Fortran doesn't have real exception handling. So when a pFUnit exception is "thrown", the code continues along as if nothing abnormal happened. This can lead to later errors, such as accessing unallocated variables, array bounds exceptions, floating point exceptions, etc. This means that you may not be able to do some expected error testing that you'd like to do, or in order to do this expected error testing, you'll need to add some code to prevent errors after calling ``endrun``. (This code after ``endrun`` won't be executed in the production case, but would be executed in unit testing.) + +In unit test-specific code, if we detect an unexpected error - which in this case would typically indicate an issue in the unit test code, rather than an issue in the production code - we use different techniques in different places. Where possible, we prefer calling ``shr_sys_abort`` if an unexpected error occurs in unit test-specific code. However, there are some situations where this is problematic, and we instead use ``stop 1``. (Note that ``stop 1`` causes the program to abort with a non-zero error code, which pFUnit interprets as a test failure, as is desired in these cases; in contrast, ``stop`` with no argument, an argument of 0, or a string argument causes the program to abort with a zero error code, which pFUnit interprets as a pass, which is often not what you want.) Some situations where we use ``stop 1`` instead of ``shr_sys_abort`` are: + +1. Unit test-specific code where throwing a pFUnit exception and continuing will lead to cryptic errors later (e.g., array bounds exceptions, etc.). + +2. Code called using pFUnit's ``EXTRA_FINALIZE`` technique - i.e., code called at the end of all tests in a given pFUnit executable. Throwing a pFUnit exception in these cases doesn't lead to a FAIL result, so we use a ``stop 1`` to ensure that we get a FAIL result if there's a problem in this finalization. + +(See https://github.com/ESCOMP/CTSM/pull/3017/commits/c4377dda9b45a9649f53cd0981657ee6eb268e8b for an illustration of some of these cases.) + +====================================== + ESMF initialization and finalization +====================================== + +Some unit tests require ESMF to be initialized. It is an error to re-initialize ESMF after finalizing it, so we ensure that initialization and finalization are only done once. + +To ensure that ESMF initialization is only done once, any call to ``ESMF_Initialize`` is within a conditional on the return value of ``ESMF_IsInitialized``. (ESMF appears to allow multiple initializations, with subsequent initializations not doing anything, but this behavior may not be guaranteed in the future, so to be safe, we ensure that initialization is only done once in unit testing.) + +Handling ESMF finalization is trickier, because we only want to do it at the end of a given pFUnit executable. (In practice, it might be okay to skip ESMF finalization, but this might cause issues like un-flushed log files.) To accomplish this, we use the ``EXTRA_FINALIZE`` argument to ``add_pfunit_ctest`` in the ``CMakeLists.txt`` file for any unit test that might initialize ESMF (as indicated by including ``esmf`` in the ``LINK_LIBRARIES``. (e.g., see https://github.com/ESCOMP/CTSM/pull/3017/commits/55b373b184ac56c4c1f9b837f8556cfc9ef33339.) This argument specifies a subroutine that pFUnit calls after the last test in a given executable. The associated ``EXTRA_USE`` argument gives the module name in which this subroutine is defined. Note that the ``ESMF_Finalize`` call is only done if ``ESMF_IsInitialized`` returns ``.true.``, so it's safe to use this in situations where ESMF may or may not have been initialized. + +We could handle ESMF initialization in the same way as finalization. But this might lead to unnecessary ``ESMF_Initialize`` calls. So, currently, we invoke ``ESMF_Initialize`` from the unit tests that need it, so that it's only done when truly needed by a unit test. \ No newline at end of file diff --git a/doc/source/users_guide/running-single-points/index.rst b/doc/source/users_guide/running-single-points/index.rst index f65e9c5bf3..d5ece00ec9 100644 --- a/doc/source/users_guide/running-single-points/index.rst +++ b/doc/source/users_guide/running-single-points/index.rst @@ -7,9 +7,9 @@ .. _running-single-points: -##################################### -Running Single Point Regional Cases -##################################### +####################################### +Running Single Point and Regional Cases +####################################### .. toctree:: :maxdepth: 2 diff --git a/doc/source/users_guide/running-single-points/running-single-point-subset-data.rst b/doc/source/users_guide/running-single-points/running-single-point-subset-data.rst index b843f06d8e..fc9d011e93 100644 --- a/doc/source/users_guide/running-single-points/running-single-point-subset-data.rst +++ b/doc/source/users_guide/running-single-points/running-single-point-subset-data.rst @@ -20,13 +20,14 @@ To subset surface data and climate forcings (DATM) for a single point, use the c .. code:: shell tools/site_and_regional/subset_data point \ - --lat $my_lat --lon $my_lon --site $my_site_name \ + --lat $my_lat --lon $my_lon --lon-type $my_lon_type --site $my_site_name \ --create-surface --create-datm \ --datm-syr $my_start_year --datm-eyr $my_end_year \ --create-user-mods --outdir $my_output_dir - ``$my_lat``: latitude of point, *must be between -90 and 90 degrees*. E.g., Boulder, CO, USA: 40. -- ``$my_lon``: longitude of point, *must be between 0 and 360 degrees*. E.g., Boulder, CO, USA: 55. +- ``$my_lon``: longitude of point. *Must be between -180 and 360 degrees.* E.g., Boulder, CO, USA: 255 or -105. +- ``$my_lon_type``: 180 if your longitude is in the [-180, 180] format (i.e., centered at the Prime/0th Meridian); 360 if it's in the [0, 360] format (i.e., centered at the 180th Meridian). Note that ``--lon-type $my_lon_type`` is not necessary if your longitude is unambiguous---i.e., it's only needed if your longitude is in the range [0, 180]. - ``$my_site_name``: name of site, *used for file naming* - ``$my_start_year``: start year for DATM data to subset, *default between 1901 and 2014* - ``$my_end_year``: end year for DATM data to subset, *default between 1901 and 2014; the default CRUJRA2024 DATM data ends in 2023, while the old default GSWP3 ends in 2015; see note below about switching the default DATM data* @@ -34,7 +35,8 @@ To subset surface data and climate forcings (DATM) for a single point, use the c You can also have the script subset land-use data. See the help (``tools/site_and_regional/subset_data --help``) for all argument options. -**Note that this script defaults to subsetting specific surface, domain, and land-use files and the CRUJRA2024 DATM data, and can currently only be run as-is on Derecho. If you're not on Derecho, use the ``--inputdata-dir`` to specify where the top level of your CESM input data is. Also, to subset GSWP3 instead of CRUJRA2024 DATM data, you currently need to hardwire datm_type = "datm_gswp3" (instead of the default "datm_crujra") in python/ctsm/subset_data.py.** +.. note:: + This script defaults to subsetting specific surface, domain, and land-use files and the CRUJRA2024 DATM data, and can currently only be run as-is on Derecho. If you're not on Derecho, use ``--inputdata-dir`` to specify where the top level of your CESM input data is. Also, to subset GSWP3 instead of CRUJRA2024 DATM data, you currently need to hardwire ``datm_type = "datm_gswp3"`` (instead of the default ``"datm_crujra"``) in ``python/ctsm/subset_data.py``. The ``--create-user-mods`` command tells the script to set up a user mods directory in your specified ``$my_output_dir`` and to specify the required ``PTS_LAT`` and ``PTS_LON`` settings. You can then use this user mods directory to set up your CTSM case, as described below. diff --git a/py_env_create b/py_env_create index 8b7ffaec47..5b4515bc75 100755 --- a/py_env_create +++ b/py_env_create @@ -18,8 +18,8 @@ cd "${SCRIPT_DIR}" condadir="${SCRIPT_DIR}/python" default_new_env_name=ctsm_pylib default_old_env_name=ctsm_pylib_3_7_9 -default_condafile="${condadir}/conda_env_ctsm_py.txt" -default_condafile_old="${condadir}/conda_env_ctsm_py_old_3.7.9.txt" +default_condafile="${condadir}/conda_env_ctsm_py.yml" +default_condafile_old="${condadir}/conda_env_ctsm_py_old_3.7.9.yml" condafile="${default_condafile}" quiet="" @@ -228,26 +228,15 @@ if [[ ${conda_env_does_exist} -gt 0 ]]; then fi # -# Create the environment -# -echo "Create $new_env_name" -if [[ "${quiet}" == "--quiet" ]]; then - (set -x; ${condamamba} create ${yes} --name $new_env_name ${quiet} 1>/dev/null) -else - (set -x; ${condamamba} create ${yes} --name $new_env_name ${verbose}) -fi - -# -# Install the environment +# Create and install the environment # echo "Installing $new_env_name; this can take a few minutes." -cmd="" if [[ "${quiet}" == "--quiet" ]]; then - # -q/--quiet option not available on Derecho - (set -x; ${condamamba} install ${yes} --channel conda-forge --name $new_env_name --file $condafile $option ${quiet} 1>/dev/null) + (set -x; ${condamamba} env create ${yes} --file "${condafile}" --name $new_env_name ${quiet} $option 1>/dev/null) else - (set -x; ${condamamba} install ${yes} --channel conda-forge --name $new_env_name --file $condafile $option ${verbose}) + (set -x; ${condamamba} env create ${yes} --file "${condafile}" --name $new_env_name $option ${verbose}) fi + # # Report on success # diff --git a/python/conda_env_ctsm_py.txt b/python/conda_env_ctsm_py.txt deleted file mode 100644 index 58f32092e6..0000000000 --- a/python/conda_env_ctsm_py.txt +++ /dev/null @@ -1,27 +0,0 @@ -# -# NOTE: On Derecho you may need to "module load conda" -# -# use the top level bash script: -# ../py_env_create # Do this each time you update your CTSM Version -# conda activate ctsm_pylib # Do this anytime you want to run a CTSM python script -# Or the individual conda commands: -# conda create -n ctsm_pylib # Do this one time for each machine -# conda install -n ctsm_pylib --file conda_env_ctsm_py.txt # Do this each time you update your CTSM Version -# conda activate ctsm_pylib # Do this anytime you want to run a CTSM python script -# -python=3.13.2 -xarray=2025.1.2 -tqdm=4.67.1 -scipy=1.15.2 -netcdf4=1.7.2 -requests=2.32.3 -xesmf=0.8.8 -numba=0.61.0 -pylint=3.3.4 -black=25.1.0 # NOTE: This version must be coordinated with ../.github/workflows/black.yml -cartopy=0.24.0 -matplotlib=3.10.1 - -# Used in ctsm_postprocessing repo -pytest=8.3.5 -coverage=7.8.0 diff --git a/python/conda_env_ctsm_py.yml b/python/conda_env_ctsm_py.yml new file mode 100644 index 0000000000..8a951bb3f6 --- /dev/null +++ b/python/conda_env_ctsm_py.yml @@ -0,0 +1,22 @@ +# This file is used by py_env_create to install the latest version of the ctsm_pylib conda environment. +name: ctsm_pylib +channels: + - conda-forge + - defaults +dependencies: + - python=3.13.2 + - xarray=2025.1.2 + - tqdm=4.67.1 + - scipy=1.15.2 + - netcdf4=1.7.2 + - requests=2.32.3 + - xesmf=0.8.8 + - numba=0.61.0 + - pylint=3.3.4 + - black=25.1.0 + - cartopy=0.24.0 + - matplotlib=3.10.1 + + # Used in ctsm_postprocessing repo + - pytest=8.3.5 + - coverage=7.8.0 diff --git a/python/conda_env_ctsm_py_old_3.7.9.txt b/python/conda_env_ctsm_py_old_3.7.9.txt deleted file mode 100644 index 6d39d2c6b8..0000000000 --- a/python/conda_env_ctsm_py_old_3.7.9.txt +++ /dev/null @@ -1,19 +0,0 @@ -# This file is used by py_env_create to install the previous version of the ctsm_pylib environment, -# which used Python 3.7.9. Doing so is only useful for running CTSM python tools in repos from -# before the new ctsm_pylib version (with Python 3.13.2) was introduced. -# -python=3.7.9 -pandas=1.3.5 -tqdm=4.67.1 -scipy=1.7.3 -netcdf4=1.6.0 -requests=2.32.2 -packaging=21.3 -numpy=1.19.5 -xarray=0.17.0 -xesmf=0.7.0 -numba=0.55.2 -pylint=2.8.3 -black=22.3.0 -cartopy=0.20.2 -matplotlib=3.3.2 diff --git a/python/conda_env_ctsm_py_old_3.7.9.yml b/python/conda_env_ctsm_py_old_3.7.9.yml new file mode 100644 index 0000000000..e77bab7634 --- /dev/null +++ b/python/conda_env_ctsm_py_old_3.7.9.yml @@ -0,0 +1,24 @@ +# This file is used by py_env_create to install the previous version of the ctsm_pylib environment, +# which used Python 3.7.9. Doing so is only useful for running CTSM python tools in repos from +# before the new ctsm_pylib version (with Python 3.13.2) was introduced. +# +name: ctsm_pylib_3_7_9 +channels: + - conda-forge + - defaults +dependencies: + - python=3.7.9 + - pandas=1.3.5 + - tqdm=4.67.1 + - scipy=1.7.3 + - netcdf4=1.6.0 + - requests=2.32.2 + - packaging=21.3 + - numpy=1.19.5 + - xarray=0.17.0 + - xesmf=0.7.0 + - numba=0.55.2 + - pylint=2.8.3 + - black=22.3.0 + - cartopy=0.20.2 + - matplotlib=3.3.2 diff --git a/python/ctsm/args_utils.py b/python/ctsm/args_utils.py index 5029c01f55..612e3f09a1 100644 --- a/python/ctsm/args_utils.py +++ b/python/ctsm/args_utils.py @@ -7,8 +7,6 @@ import logging import argparse -from ctsm.config_utils import lon_range_0_to_360 - logger = logging.getLogger(__name__) @@ -34,9 +32,7 @@ def plat_type(plat): def plon_type(plon): """ Function to define lon type for the parser and - convert negative longitudes to 0-360 and raise error if lon is not between -180 and 360. - Args: plon (str): longitude Raises: @@ -49,5 +45,4 @@ def plon_type(plon): raise argparse.ArgumentTypeError( "ERROR: Longitude should be between 0 and 360 or -180 and 180." ) - plon_out = lon_range_0_to_360(plon_float) - return plon_out + return plon_float diff --git a/python/ctsm/config_utils.py b/python/ctsm/config_utils.py index e5bc84e5d3..def17f6c9b 100644 --- a/python/ctsm/config_utils.py +++ b/python/ctsm/config_utils.py @@ -7,6 +7,7 @@ import configparser from ctsm.utils import abort +from ctsm.longitude import Longitude logger = logging.getLogger(__name__) @@ -18,24 +19,32 @@ _CONFIG_UNSET = "UNSET" -def lon_range_0_to_360(lon_in): +def check_lon1_lt_lon2(lon1, lon2, lon_type): """ Description ----------- - Restrict longitude to 0 to 360 when given as -180 to 180. + Given two longitudes, check that lon1 is < lon2. Useful for avoiding CTSM Issue #2017, but note + that to use this function properly for that purpose, you need to have already converted + longitudes from lon_type 180 to 360. """ - if -180 <= lon_in < 0: - raise NotImplementedError( - "A negative longitude suggests you input longitudes in the range [-180, 0)---" - "i.e., centered around the Prime Meridian. This code requires longitudes in the " - "range [0, 360)---i.e., starting at the International Date Line." - ) - if not (0 <= lon_in <= 360 or lon_in is None): - errmsg = "lon_in needs to be in the range 0 to 360" - abort(errmsg) - lon_out = lon_in - - return lon_out + # Convert to Longitude class, if needed + if not isinstance(lon1, Longitude): + lon1 = Longitude(lon1, lon_type) + if not isinstance(lon2, Longitude): + lon2 = Longitude(lon2, lon_type) + + # Convert to type 360, if needed + lon1 = lon1.get(360) + lon2 = lon2.get(360) + + if lon1 < lon2: + return + + msg = f"--lon1 ({lon1}) must be < --lon2 ({lon2})\n" + msg += "See CTSM issue #2017: https://github.com/ESCOMP/CTSM/issues/2017" + if lon_type == 180: + msg = "After converting to --lon-type 360, " + msg + raise ValueError(msg) def get_config_value( diff --git a/python/ctsm/crop_calendars/cropcal_utils.py b/python/ctsm/crop_calendars/cropcal_utils.py index e0a4e5a410..c7e8b6ac52 100644 --- a/python/ctsm/crop_calendars/cropcal_utils.py +++ b/python/ctsm/crop_calendars/cropcal_utils.py @@ -357,10 +357,9 @@ def safer_timeslice(ds_in, time_slice, time_var="time"): return ds_in -def lon_idl2pm(lons_in, fail_silently=False): +def lon_axis_type180_to_type360(lons_in, fail_silently=False): """ - Convert a longitude axis that's -180 to 180 around the international date line to one that's 0 - to 360 around the prime meridian. + Convert a longitude axis that's -180 to 180 to one that's 0 to 360 - If you pass in a Dataset or DataArray, the "lon" coordinates will be changed. Otherwise, it assumes you're passing in numeric data. @@ -397,13 +396,13 @@ def do_it(tmp): lons_out = do_it(lons_in) if not is_strictly_increasing(lons_out): print( - "WARNING: You passed in numeric longitudes to lon_idl2pm() and these have been" - " converted, but they're not strictly increasing." + "WARNING: You passed in numeric longitudes to lon_axis_type180_to_type360() and" + " these have been converted, but they're not strictly increasing." ) print( "To assign the new longitude coordinates to an Xarray object, use" - " xarrayobject.assign_coordinates()! (Pass the object directly in to lon_idl2pm() in" - " order to suppress this message.)" + " xarrayobject.assign_coordinates()! (Pass the object directly in to" + " lon_axis_type180_to_type360() in order to suppress this message.)" ) return lons_out diff --git a/python/ctsm/crop_calendars/process_ggcmi_shdates.py b/python/ctsm/crop_calendars/process_ggcmi_shdates.py index 2663689df7..7ac4304574 100644 --- a/python/ctsm/crop_calendars/process_ggcmi_shdates.py +++ b/python/ctsm/crop_calendars/process_ggcmi_shdates.py @@ -500,7 +500,7 @@ def process_ggcmi_shdates( # Flip latitude to match destination cropcal_ds = cropcal_ds.reindex(lat=cropcal_ds.lat[::-1]) # Rearrange longitude to match destination (does nothing if not needed) - cropcal_ds = utils.lon_idl2pm(cropcal_ds, fail_silently=True) + cropcal_ds = utils.lon_axis_type180_to_type360(cropcal_ds, fail_silently=True) for thisvar_clm in variable_dict: # pylint: disable=consider-using-dict-items # Get GGCMI netCDF info diff --git a/python/ctsm/longitude.py b/python/ctsm/longitude.py new file mode 100644 index 0000000000..bb04e2b98f --- /dev/null +++ b/python/ctsm/longitude.py @@ -0,0 +1,106 @@ +""" +A class to handle longitude values, plus helper functions for that class +""" + +import logging + +logger = logging.getLogger(__name__) + + +def _check_lon_type_180(lon_in): + """ + Checks value range of longitude with type 180 + """ + if not -180 <= lon_in <= 180: + raise ValueError(f"lon_in needs to be in the range [-180, 180]: {lon_in}") + + +def _check_lon_type_360(lon_in): + """ + Checks value range of longitude with type 360 + """ + if not 0 <= lon_in <= 360: + raise ValueError(f"lon_in needs to be in the range [0, 360]: {lon_in}") + + +def _check_lon_value_given_type(lon_in, lon_type_in): + """ + Checks value range of longitude + """ + if lon_type_in == 180: + _check_lon_type_180(lon_in) + elif lon_type_in == 360: + _check_lon_type_360(lon_in) + else: + raise RuntimeError(f"You must specify lon_type_in as 180 or 360, not {lon_type_in}") + + +def _convert_lon_type_180_to_360(lon_in): + """ + Convert a longitude from type 180 to type 360 + """ + _check_lon_type_180(lon_in) + lon_out = lon_in % 360 + logger.info( + "Converting longitude from [-180, 180] to [0, 360]: %s to %s", + str(lon_in), + str(lon_out), + ) + + return lon_out + + +def _convert_lon_type_360_to_180(lon_in): + """ + Convert a longitude from type 360 to type 180 + """ + _check_lon_type_360(lon_in) + + if lon_in <= 180: + lon_out = lon_in + else: + lon_out = lon_in - 360 + + logger.info( + "Converting longitude from [-180, 180] to [0, 360]: %s to %s", + str(lon_in), + str(lon_out), + ) + + return lon_out + + +class Longitude: + """ + A class to keep track of a longitude and its type + """ + + # pylint: disable=too-few-public-methods + + def __init__(self, lon, lon_type): + + # Do not allow nesting Longitude objects + if isinstance(lon, Longitude): + raise TypeError("Trying to initialize a Longitude object with a Longitude object") + + # If lon_type comes in as a string, convert it to int + if isinstance(lon_type, str): + lon_type = int(lon_type) + + # Check that longitude is within bounds + _check_lon_value_given_type(lon, lon_type) + + self._lon = lon + self._lon_type = lon_type + + def get(self, lon_type_out): + """ + Get the longitude value, converting longitude type if needed + """ + if lon_type_out == self._lon_type: + return self._lon + if lon_type_out == 180: + return _convert_lon_type_360_to_180(self._lon) + if lon_type_out == 360: + return _convert_lon_type_180_to_360(self._lon) + raise RuntimeError(f"Add handling for lon_type_out {lon_type_out}") diff --git a/python/ctsm/modify_input_files/fsurdat_modifier.py b/python/ctsm/modify_input_files/fsurdat_modifier.py index ff68c14f42..75f969269b 100644 --- a/python/ctsm/modify_input_files/fsurdat_modifier.py +++ b/python/ctsm/modify_input_files/fsurdat_modifier.py @@ -499,6 +499,13 @@ def read_cfg_required_basic_opts(config, section, cfg_path): file_path=cfg_path, convert_to_type=float, ) + lon_type = get_config_value( + config=config, + section=section, + item="lon_type", + file_path=cfg_path, + convert_to_type=int, + ) landmask_file = get_config_value( config=config, @@ -514,7 +521,16 @@ def read_cfg_required_basic_opts(config, section, cfg_path): lon_dimname = get_config_value( config=config, section=section, item="lon_dimname", file_path=cfg_path, can_be_unset=True ) - return (lnd_lat_1, lnd_lat_2, lnd_lon_1, lnd_lon_2, landmask_file, lat_dimname, lon_dimname) + return ( + lnd_lat_1, + lnd_lat_2, + lnd_lon_1, + lnd_lon_2, + landmask_file, + lat_dimname, + lon_dimname, + lon_type, + ) def fsurdat_modifier(parser): @@ -569,6 +585,7 @@ def fsurdat_modifier(parser): landmask_file, lat_dimname, lon_dimname, + lon_type, ) = read_cfg_required_basic_opts(config, section, cfg_path) # Create ModifyFsurdat object modify_fsurdat = ModifyFsurdat.init_from_file( @@ -580,6 +597,7 @@ def fsurdat_modifier(parser): landmask_file=landmask_file, lat_dimname=lat_dimname, lon_dimname=lon_dimname, + lon_type=lon_type, ) # Read control information about the optional sections diff --git a/python/ctsm/modify_input_files/mesh_mask_modifier.py b/python/ctsm/modify_input_files/mesh_mask_modifier.py index 78dc20d334..45bdbb3346 100644 --- a/python/ctsm/modify_input_files/mesh_mask_modifier.py +++ b/python/ctsm/modify_input_files/mesh_mask_modifier.py @@ -66,6 +66,7 @@ def mesh_mask_modifier(cfg_path): lon_varname = get_config_value( config=config, section=section, item="lon_varname", file_path=cfg_path ) + lon_type = get_config_value(config=config, section=section, item="lon_type", file_path=cfg_path) # Create ModifyMeshMask object modify_mesh_mask = ModifyMeshMask.init_from_file( @@ -75,6 +76,7 @@ def mesh_mask_modifier(cfg_path): lon_dimname=lon_dimname, lat_varname=lat_varname, lon_varname=lon_varname, + lon_type=lon_type, ) # If output file exists, abort before starting work diff --git a/python/ctsm/modify_input_files/modify_fsurdat.py b/python/ctsm/modify_input_files/modify_fsurdat.py index 7e24350694..b8f68dbac8 100644 --- a/python/ctsm/modify_input_files/modify_fsurdat.py +++ b/python/ctsm/modify_input_files/modify_fsurdat.py @@ -14,7 +14,7 @@ from ctsm.utils import abort, update_metadata from ctsm.git_utils import get_ctsm_git_short_hash -from ctsm.config_utils import lon_range_0_to_360 +from ctsm.longitude import Longitude logger = logging.getLogger(__name__) @@ -26,9 +26,23 @@ class ModifyFsurdat: """ def __init__( - self, my_data, *, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname + self, + my_data, + *, + lon_1, + lon_2, + lat_1, + lat_2, + landmask_file, + lat_dimname, + lon_dimname, + lon_type=None, ): + if lon_type is not None: + lon_1 = Longitude(lon_1, lon_type) + lon_2 = Longitude(lon_2, lon_type) + self.numurbl = 3 # Number of urban density types self.file = my_data if "numurbl" in self.file.dims: @@ -72,7 +86,17 @@ def __init__( @classmethod def init_from_file( - cls, *, fsurdat_in, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname + cls, + *, + fsurdat_in, + lon_1, + lon_2, + lat_1, + lat_2, + landmask_file, + lat_dimname, + lon_dimname, + lon_type, ): """Initialize a ModifyFsurdat object from file fsurdat_in""" logger.info("Opening fsurdat_in file to be modified: %s", fsurdat_in) @@ -86,18 +110,22 @@ def init_from_file( landmask_file=landmask_file, lat_dimname=lat_dimname, lon_dimname=lon_dimname, + lon_type=lon_type, ) @staticmethod - def _get_rectangle(*, lon_1, lon_2, lat_1, lat_2, longxy, latixy): + def _get_rectangle(*, lon_1: Longitude, lon_2: Longitude, lat_1, lat_2, longxy, latixy): """ Description ----------- """ - # ensure that lon ranges 0-360 in case user entered -180 to 180 - lon_1 = lon_range_0_to_360(lon_1) - lon_2 = lon_range_0_to_360(lon_2) + # Get the longitudes in the correct format + if not all(isinstance(x, Longitude) for x in [lon_1, lon_2]): + raise TypeError("lon_1 and lon_2 must be of type Longitude") + lon_type = 360 + lon_1 = lon_1.get(lon_type) + lon_2 = lon_2.get(lon_type) # determine the rectangle(s) # TODO This is not really "nearest" for the edges but isel didn't work diff --git a/python/ctsm/modify_input_files/modify_mesh_mask.py b/python/ctsm/modify_input_files/modify_mesh_mask.py index 44dff2bc22..5cca1c8ce9 100644 --- a/python/ctsm/modify_input_files/modify_mesh_mask.py +++ b/python/ctsm/modify_input_files/modify_mesh_mask.py @@ -12,7 +12,7 @@ import xarray as xr from ctsm.utils import abort -from ctsm.config_utils import lon_range_0_to_360 +from ctsm.longitude import Longitude logger = logging.getLogger(__name__) @@ -30,7 +30,15 @@ class ModifyMeshMask: # ... /islas_examples/modify_fsurdat/fill_indian_ocean/ # Read mod_lnd_props here only for consistency checks def __init__( - self, my_data, *, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname + self, + my_data, + *, + landmask_file, + lat_dimname, + lon_dimname, + lat_varname, + lon_varname, + lon_type, ): self.file = my_data @@ -48,6 +56,7 @@ def __init__( self.lonvar = self._landmask_file[lon_varname][..., :] self.lsmlat = self._landmask_file.dims[lat_dimname] self.lsmlon = self._landmask_file.dims[lon_dimname] + self.lon_type = lon_type lonvar_first = self.lonvar[..., 0].data.max() lonvar_last = self.lonvar[..., -1].data.max() @@ -77,7 +86,7 @@ def __init__( @classmethod def init_from_file( - cls, *, file_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname + cls, *, file_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname, lon_type ): """Initialize a ModifyMeshMask object from file_in""" logger.info("Opening file to be modified: %s", file_in) @@ -89,6 +98,7 @@ def init_from_file( lon_dimname=lon_dimname, lat_varname=lat_varname, lon_varname=lon_varname, + lon_type=lon_type, ) def set_mesh_mask(self, var): @@ -144,13 +154,12 @@ def set_mesh_mask(self, var): + f"{len(self.latvar.sizes)}" ) abort(errmsg) - # ensure lon range of 0-360 rather than -180 to 180 - lonvar_scalar = lon_range_0_to_360(lonvar_scalar) + # lon and lat from the mesh file lat_mesh = float(self.file["centerCoords"][ncount, 1]) lon_mesh = float(self.file["centerCoords"][ncount, 0]) # ensure lon range of 0-360 rather than -180 to 180 - lon_mesh = lon_range_0_to_360(lon_mesh) + lonvar_scalar = Longitude(lonvar_scalar, self.lon_type).get(360) errmsg = ( "Must be equal: " diff --git a/python/ctsm/site_and_regional/regional_case.py b/python/ctsm/site_and_regional/regional_case.py index b653f0fc2d..a0b746d47d 100644 --- a/python/ctsm/site_and_regional/regional_case.py +++ b/python/ctsm/site_and_regional/regional_case.py @@ -18,6 +18,7 @@ from ctsm.site_and_regional.mesh_type import MeshType from ctsm.utils import add_tag_to_filename from ctsm.utils import abort +from ctsm.config_utils import check_lon1_lt_lon2 logger = logging.getLogger(__name__) @@ -37,6 +38,8 @@ class RegionalCase(BaseCase): first (bottom) longitude of a region. lon2 : float second (top) longitude of a region. + lon_type : int + 180 if longitudes are in [-180, 180], 360 if they're in [0, 360] reg_name: str -- default = None Region's name create_domain : bool @@ -65,9 +68,6 @@ class RegionalCase(BaseCase): check_region_bounds Check for the regional bounds - check_region_lons - Check for the regional lons - check_region_lats Check for the regional lats @@ -118,6 +118,7 @@ def __init__( create_user_mods=create_user_mods, overwrite=overwrite, ) + self.lat1 = lat1 self.lat2 = lat2 self.lon1 = lon1 @@ -148,27 +149,18 @@ def check_region_bounds(self): """ Check for the regional bounds """ - self.check_region_lons() - self.check_region_lats() - - def check_region_lons(self): - """ - Check for the regional lon bounds - """ - if self.lon1 >= self.lon2: - err_msg = """ - \n - ERROR: lon1 is bigger than lon2. - lon1 points to the westernmost longitude of the region. {} - lon2 points to the easternmost longitude of the region. {} - Please make sure lon1 is smaller than lon2. - - Please note that if longitude in -180-0, the code automatically - convert it to 0-360. - """.format( - self.lon1, self.lon2 + # If you're calling this, lat/lon bounds need to have been provided + if any(x is None for x in [self.lon1, self.lon2, self.lat1, self.lat2]): + raise argparse.ArgumentTypeError( + "Latitude and longitude bounds must be provided and not None.\n" + + f" lon1: {self.lon1}\n" + + f" lon2: {self.lon2}\n" + + f" lat1: {self.lat1}\n" + + f" lat2: {self.lat2}" ) - raise argparse.ArgumentTypeError(err_msg) + # By now, you need to have already converted to longitude [0, 360] + check_lon1_lt_lon2(self.lon1, self.lon2, 360) + self.check_region_lats() def check_region_lats(self): """ diff --git a/python/ctsm/subset_data.py b/python/ctsm/subset_data.py index e16cad4195..e4a323be53 100644 --- a/python/ctsm/subset_data.py +++ b/python/ctsm/subset_data.py @@ -65,9 +65,11 @@ from ctsm.site_and_regional.base_case import DatmFiles from ctsm.site_and_regional.single_point_case import SinglePointCase from ctsm.site_and_regional.regional_case import RegionalCase -from ctsm.args_utils import plon_type, plat_type +from ctsm.args_utils import plat_type, plon_type from ctsm.path_utils import path_to_ctsm_root from ctsm.utils import abort +from ctsm.config_utils import check_lon1_lt_lon2 +from ctsm.longitude import Longitude # -- import ctsm logging flags from ctsm.ctsm_logging import ( @@ -81,6 +83,23 @@ logger = logging.getLogger(__name__) +def _add_lon_type_arg(this_parser): + lon_type_help_str = ( + "Whether longitudes are in the [-180, 180] format (centered around the Prime/0th" + " Meridian) or the [0, 360] format (centered around the 180th Meridian)." + " Choose by specifying the upper limit." + ) + this_parser.add_argument( + "--lon-type", + help=lon_type_help_str, + required=False, + default=None, + type=int, + choices=[180, 360], + ) + return this_parser + + def get_parser(): """ Get the parser object for subset_data.py script. @@ -117,8 +136,9 @@ def get_parser(): dest="plon", required=False, type=plon_type, - default=287.8, + default=287.8, # Must be unambiguous: Either < 0 or > 180 ) + pt_parser = _add_lon_type_arg(pt_parser) pt_parser.add_argument( "--site", help="Site name or tag. [default: %(default)s]", @@ -215,30 +235,23 @@ def get_parser(): ) rg_parser.add_argument( "--lon1", - help=( - "Region westernmost longitude. Must be in [0, 360) format: i.e., starting at the" - " International Date Line rather than centered on the Prime Meridian. [default:" - " %(default)s]" - ), + help=("Region westernmost longitude. [default: %(default)s]"), action="store", dest="lon1", required=False, type=plon_type, - default=275.0, + default=275.0, # Must be unambiguous: Either < 0 or > 180 ) rg_parser.add_argument( "--lon2", - help=( - "Region easternmost longitude. Must be in [0, 360) format: i.e., starting at the" - " International Date Line rather than centered on the Prime Meridian. [default:" - " %(default)s]" - ), + help=("Region easternmost longitude. [default: %(default)s]"), action="store", dest="lon2", required=False, type=plon_type, - default=330.0, + default=330.0, # Must be unambiguous: Either < 0 or > 180 ) + rg_parser = _add_lon_type_arg(rg_parser) rg_parser.add_argument( "--reg", help="Region name or tag. [default: %(default)s]", @@ -400,85 +413,96 @@ def get_parser(): return parser -def check_args(args): - """Check the command line arguments""" - # --------------------------------- # - # print help and exit when no option is chosen - if args.run_type not in ("point", "region"): +def check_surf_year(args): + """ + Check command-line arguments w/r/t --surf-year + """ + if args.surf_year != 2000 and not args.create_surfdata: err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n Must supply a positional argument: 'point' or 'region'. + \n --surf-year option is set to something besides the default of 2000 + \n without the --create-surface option" """ ) raise argparse.ArgumentError(None, err_msg) - if not any([args.create_surfdata, args.create_landuse, args.create_datm, args.create_domain]): + if args.surf_year != 1850 and args.create_landuse: err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n Must supply one of: - \n --create-surface \n --create-landuse \n --create-datm \n --create-domain \n \n + \n --surf-year option is NOT set to 1850 and the --create-landuse option + \n is selected which requires it to be 1850 (see + https://github.com/ESCOMP/CTSM/issues/2018) """ ) raise argparse.ArgumentError(None, err_msg) - if not os.path.exists(args.config_file): + if args.surf_year not in [1850, 2000]: err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n Entered default config file does not exist" + \n --surf-year option can only be set to 1850 or 2000 """ ) raise argparse.ArgumentError(None, err_msg) - if args.out_surface and not args.create_surfdata: + +def check_args(args): + """Check the command line arguments""" + # --------------------------------- # + # print help and exit when no option is chosen + if args.run_type not in ("point", "region"): err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n out-surface option is given without the --create-surface option" + \n Must supply a positional argument: 'point' or 'region'. """ ) raise argparse.ArgumentError(None, err_msg) - if args.create_landuse and not args.create_surfdata: + args = process_args(args) + + if not any([args.create_surfdata, args.create_landuse, args.create_datm, args.create_domain]): err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n --create-landuse option requires the --create-surface option: + \n Must supply one of: + \n --create-surface \n --create-landuse \n --create-datm \n --create-domain \n \n """ ) raise argparse.ArgumentError(None, err_msg) - if args.surf_year != 2000 and not args.create_surfdata: + + if not os.path.exists(args.config_file): err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n --surf-year option is set to something besides the default of 2000 - \n without the --create-surface option" + \n Entered default config file does not exist" """ ) raise argparse.ArgumentError(None, err_msg) - if args.surf_year != 1850 and args.create_landuse: + if args.out_surface and not args.create_surfdata: err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n --surf-year option is NOT set to 1850 and the --create-landuse option - \n is selected which requires it to be 1850 (see - https://github.com/ESCOMP/CTSM/issues/2018) + \n out-surface option is given without the --create-surface option" """ ) raise argparse.ArgumentError(None, err_msg) - if args.surf_year not in [1850, 2000]: + if args.create_landuse and not args.create_surfdata: err_msg = textwrap.dedent( """\ \n ------------------------------------ - \n --surf-year option can only be set to 1850 or 2000 + \n --create-landuse option requires the --create-surface option: """ ) raise argparse.ArgumentError(None, err_msg) + # Checks related to --surf-year + check_surf_year(args) + if args.out_surface and os.path.exists(args.out_surface) and not args.overwrite: err_msg = textwrap.dedent( """\ @@ -525,6 +549,20 @@ def check_args(args): ) raise NotImplementedError(None, err_msg) + if hasattr(args, "lon1"): + if (args.lon1 is None) != (args.lon2 is None): + err_msg = textwrap.dedent( + """\ + \n ------------------------------------ + \nERROR: If providing --lon1, you must also provide --lon2 + """ + ) + raise argparse.ArgumentError(None, err_msg) + if args.lon1 is not None: + check_lon1_lt_lon2(args.lon1, args.lon2, args.lon_type) + + return args + def setup_user_mods(user_mods_dir, cesmroot): """ @@ -715,15 +753,10 @@ def subset_point(args, file_dict: dict): logger.info("Successfully ran script for single point.") -def subset_region(args, file_dict: dict): +def _set_up_regional_case(args): """ - Subsets surface, domain, land use, and/or DATM files for a region + Set up regional case """ - - logger.info("----------------------------------------------------------------------------") - logger.info("This script extracts a region from the global CTSM datasets.") - - # -- Create Region Object region = RegionalCase( lat1=args.lat1, lat2=args.lat2, @@ -739,8 +772,20 @@ def subset_region(args, file_dict: dict): out_dir=args.out_dir, overwrite=args.overwrite, ) - logger.debug(region) + return region + + +def subset_region(args, file_dict: dict): + """ + Subsets surface, domain, land use, and/or DATM files for a region + """ + + logger.info("----------------------------------------------------------------------------") + logger.info("This script extracts a region from the global CTSM datasets.") + + # -- Create Region Object + region = _set_up_regional_case(args) # -- Create CTSM domain file if region.create_domain: @@ -776,6 +821,47 @@ def subset_region(args, file_dict: dict): logger.info("Successfully ran script for a regional case.") +def _detect_lon_type(lon_in): + if lon_in < 0: + lon_type = 180 + elif lon_in > 180: + lon_type = 360 + else: + msg = "When providing an ambiguous longitude, you must specify --lon-type 180 or 360" + raise argparse.ArgumentTypeError(msg) + return lon_type + + +def process_args(args): + """ + Process arguments after parsing + """ + # process logging args (i.e. debug and verbose) + process_logging_args(args) + + # process longitude args + lon_args = [var for var in ["plon", "lon1", "lon2"] if hasattr(args, var)] + lon_arg_values = [getattr(args, var) is not None for var in lon_args] + if any(lon_arg_values): + if args.lon_type is None: + if hasattr(args, "plon"): + args.lon_type = _detect_lon_type(args.plon) + else: + lon1_type = _detect_lon_type(args.lon1) + lon2_type = _detect_lon_type(args.lon2) + if lon1_type != lon2_type: + raise argparse.ArgumentTypeError( + "--lon1 and --lon2 seem to be of different types" + ) + args.lon_type = lon1_type + for var in lon_args: + val = getattr(args, var) + if val is None: + continue + setattr(args, var, Longitude(val, args.lon_type)) + return args + + def main(): """ Calls functions that subset surface, landuse, domain, and/or DATM files for a region or a @@ -788,10 +874,8 @@ def main(): parser = get_parser() args = parser.parse_args() - check_args(args) # --------------------------------- # - # process logging args (i.e. debug and verbose) - process_logging_args(args) + args = check_args(args) # --------------------------------- # # parse defaults file diff --git a/python/ctsm/test/test_sys_mesh_modifier.py b/python/ctsm/test/test_sys_mesh_modifier.py index f94c86e547..69ec8afdac 100755 --- a/python/ctsm/test/test_sys_mesh_modifier.py +++ b/python/ctsm/test/test_sys_mesh_modifier.py @@ -57,6 +57,7 @@ def setUp(self): self._lon_varname = None self._lat_dimname = None self._lon_dimname = None + self._lon_type = 360 self._previous_dir = os.getcwd() os.chdir(self._tempdir) # cd to tempdir @@ -233,6 +234,8 @@ def _create_config_file(self): line = f"\nlat_varname = {self._lat_varname}" elif re.match(r" *lon_varname *=", line): line = f"\nlon_varname = {self._lon_varname}" + elif re.match(r" *lon_type *=", line): + line = f"\nlon_type = {self._lon_type}" cfg_out.write(line) diff --git a/python/ctsm/test/test_unit_args_utils.py b/python/ctsm/test/test_unit_args_utils.py index 2328e17d91..548e7d9389 100755 --- a/python/ctsm/test/test_unit_args_utils.py +++ b/python/ctsm/test/test_unit_args_utils.py @@ -18,7 +18,6 @@ # pylint: disable=wrong-import-position from ctsm.args_utils import plon_type, plat_type from ctsm import unit_testing -from ctsm.test.test_unit_utils import wrong_lon_type_error_regex # pylint: disable=invalid-name @@ -39,15 +38,10 @@ def test_plonType_positive(self): # --between -180-0 def test_plonType_negative(self): """ - Test of negative plon between -180 and 0 + Test that negative plon between -180 and 0 does not error and is not changed """ - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - plon_type(-30) - - # result = plon_type(-30) - # self.assertEqual(result, 330.0) + result = plon_type(-30) + self.assertEqual(result, -30.0) # -- > 360 def test_plonType_outOfBounds_positive(self): @@ -68,15 +62,10 @@ def test_plonType_outOfBounds_negative(self): # -- = -180 def test_plonType_negative_180(self): """ - Test for when plon values are -180 + Test that plon value of -180 does not error and is not changed """ - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - plon_type(-180) - - # result = plon_type(-180) - # self.assertEqual(result, 180.0) + result = plon_type(-180) + self.assertEqual(result, -180.0) # -- = 0 def test_plonType_zero(self): diff --git a/python/ctsm/test/test_unit_config_utils.py b/python/ctsm/test/test_unit_config_utils.py index 37ad008f32..4f7d3be236 100644 --- a/python/ctsm/test/test_unit_config_utils.py +++ b/python/ctsm/test/test_unit_config_utils.py @@ -7,8 +7,7 @@ from configparser import ConfigParser from ctsm import unit_testing -from ctsm.config_utils import lon_range_0_to_360, get_config_value_or_array -from ctsm.test.test_unit_utils import wrong_lon_type_error_regex +from ctsm.config_utils import get_config_value_or_array # Allow test names that pylint doesn't like; otherwise hard to make them # readable @@ -29,48 +28,6 @@ def setUp(self): self.file_path = "path_to_file" self.config[self.section] = {} - def test_negative_lon(self): - """Test lon_range_0_to_360 for a negative longitude""" - lon = -180.0 - - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - lon_range_0_to_360(lon) - - # lon_new = lon_range_0_to_360(lon) - # self.assertEqual(lon_new, 180.0, "lon not as expected") - - def test_negative2_lon(self): - """Test lon_range_0_to_360 for a negative longitude""" - lon = -5.0 - - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - lon_range_0_to_360(lon) - - # lon_new = lon_range_0_to_360(lon) - # self.assertEqual(lon_new, 355.0, "lon not as expected") - - def test_regular_lon(self): - """Test lon_range_0_to_360 for a regular longitude""" - lon = 22.567 - lon_new = lon_range_0_to_360(lon) - self.assertEqual(lon_new, lon, "lon not as expected") - - def test_lon_out_of_range(self): - """Test lon_range_0_to_360 for longitude out of range""" - lon = 361.0 - with self.assertRaisesRegex(SystemExit, "lon_in needs to be in the range 0 to 360"): - lon_range_0_to_360(lon) - - def test_lon_out_of_range_negative(self): - """Test lon_range_0_to_360 for longitude out of range""" - lon = -181.0 - with self.assertRaisesRegex(SystemExit, "lon_in needs to be in the range 0 to 360"): - lon_range_0_to_360(lon) - def test_config_value_or_array_single_value(self): """Simple test of get_config_value_or_array""" item = "single_value_thing" diff --git a/python/ctsm/test/test_unit_fsurdat_modifier.py b/python/ctsm/test/test_unit_fsurdat_modifier.py index 2a6a8f455c..eb5e5959f0 100755 --- a/python/ctsm/test/test_unit_fsurdat_modifier.py +++ b/python/ctsm/test/test_unit_fsurdat_modifier.py @@ -15,6 +15,7 @@ from ctsm import unit_testing from ctsm.path_utils import path_to_ctsm_root +from ctsm.longitude import Longitude from ctsm.modify_input_files.fsurdat_modifier import fsurdat_modifier_arg_process from ctsm.modify_input_files.fsurdat_modifier import read_cfg_subgrid from ctsm.modify_input_files.fsurdat_modifier import read_cfg_option_control @@ -62,10 +63,11 @@ def setUp(self): self.config = ConfigParser() self.config.read(self.cfg_path) my_data = xr.open_dataset(self._fsurdat_in) + lon_type = 360 self.modify_fsurdat = ModifyFsurdat( my_data=my_data, - lon_1=0.0, - lon_2=360.0, + lon_1=Longitude(0.0, lon_type), + lon_2=Longitude(360.0, lon_type), lat_1=90.0, lat_2=90.0, landmask_file=None, diff --git a/python/ctsm/test/test_unit_longitude.py b/python/ctsm/test/test_unit_longitude.py new file mode 100644 index 0000000000..1810cfb10b --- /dev/null +++ b/python/ctsm/test/test_unit_longitude.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +"""Unit tests for config_utils""" + +import unittest + +from ctsm import unit_testing +from ctsm.longitude import Longitude +from ctsm.longitude import _convert_lon_type_180_to_360, _convert_lon_type_360_to_180 + +# Allow test names that pylint doesn't like; otherwise hard to make them +# readable +# pylint: disable=invalid-name + +# pylint: disable=protected-access + + +class TestLongitude(unittest.TestCase): + """Tests of Longitude class and helper functions""" + + # Converting between types 180 and 360 + + def test_convert_lon_type_180_to_360_positive(self): + """Test conversion 180→360 for a middle positive longitude""" + lon = 80 + lon_new = _convert_lon_type_180_to_360(lon) + self.assertEqual(lon_new, 80) + + def test_convert_lon_type_180_to_360_negative(self): + """Test conversion 180→360 for a middle negative longitude""" + lon = -80 + lon_new = _convert_lon_type_180_to_360(lon) + self.assertEqual(lon_new, 280) + + def test_convert_lon_type_180_to_360_lowerbound(self): + """Test conversion 180→360 at the lower bound of [-180, 180]""" + lon = -180.0 + lon_new = _convert_lon_type_180_to_360(lon) + self.assertEqual(lon_new, 180) + + def test_convert_lon_type_180_to_360_upperbound(self): + """Test conversion 180→360 at the upper bound of [-180, 180]""" + lon = 180.0 + lon_new = _convert_lon_type_180_to_360(lon) + self.assertEqual(lon_new, 180) + + def test_convert_lon_type_180_to_360_toohigh(self): + """Test conversion 180→360 for a value > 180: Should error""" + lon = 555 + with self.assertRaisesRegex(ValueError, r"lon_in needs to be in the range \[-180, 180\]"): + _convert_lon_type_180_to_360(lon) + + def test_convert_lon_type_180_to_360_toolow(self): + """Test conversion 180→360 for a value < -180: Should error""" + lon = -555 + + with self.assertRaisesRegex(ValueError, r"lon_in needs to be in the range \[-180, 180\]"): + _convert_lon_type_180_to_360(lon) + + def test_convert_lon_type_360_to_180_positive_low(self): + """Test conversion 360→180 for a low positive longitude""" + lon = 80 + lon_new = _convert_lon_type_360_to_180(lon) + self.assertEqual(lon_new, 80) + + def test_convert_lon_type_360_to_180_positive_high(self): + """Test conversion 360→180 for a high positive longitude""" + lon = 270 + lon_new = _convert_lon_type_360_to_180(lon) + self.assertEqual(lon_new, -90) + + def test_convert_lon_type_360_to_180_lowerbound(self): + """Test conversion 360→180 at the lower bound of [0, 360]""" + lon = 0 + lon_new = _convert_lon_type_360_to_180(lon) + self.assertEqual(lon_new, 0) + + def test_convert_lon_type_360_to_180_upperbound(self): + """Test conversion 360→180 at the upper bound of [0, 360]""" + lon = 360 + lon_new = _convert_lon_type_360_to_180(lon) + self.assertEqual(lon_new, 0) + + def test_convert_lon_type_360_to_180_toohigh(self): + """Test conversion 360→180 for a value > 180: Should error""" + lon = 555 + with self.assertRaisesRegex(ValueError, r"lon_in needs to be in the range \[0, 360\]"): + _convert_lon_type_360_to_180(lon) + + def test_convert_lon_type_360_to_180_toolow(self): + """Test conversion 360→180 for a value < -180: Should error""" + lon = -555 + + with self.assertRaisesRegex(ValueError, r"lon_in needs to be in the range \[0, 360\]"): + _convert_lon_type_360_to_180(lon) + + # Initializing new Longitude objects + + def test_lon_obj_lon_errors(self): + """Trying to initialize a Longitude object with a Longitude object should error""" + lon_type = 360 + lon_obj = Longitude(0, lon_type) + with self.assertRaises(TypeError): + Longitude(lon_obj, lon_type) + + def test_lon_obj_type180_neg(self): + """Test that creating an in-bounds negative Longitude of type 180 works""" + lon_type = 180 + this_lon = -55 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type180_pos(self): + """Test that creating an in-bounds positive Longitude of type 180 works""" + lon_type = 180 + this_lon = 87 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type180_min(self): + """Test that creating a lower-bound Longitude of type 180 works""" + lon_type = 180 + this_lon = -180 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type180_max(self): + """Test that creating an upper-bound Longitude of type 180 works""" + lon_type = 180 + this_lon = 180 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type360_pos(self): + """Test that creating an in-bounds Longitude of type 360 works""" + lon_type = 360 + this_lon = 87 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type360_min(self): + """Test that creating a lower-bound Longitude of type 360 works""" + lon_type = 360 + this_lon = 0 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + def test_lon_obj_type360_max(self): + """Test that creating an upper-bound Longitude of type 360 works""" + lon_type = 360 + this_lon = 360 + lon_obj = Longitude(this_lon, lon_type) + self.assertEqual(lon_obj.get(lon_type), this_lon) + + +if __name__ == "__main__": + unit_testing.setup_for_tests() + unittest.main() diff --git a/python/ctsm/test/test_unit_modify_fsurdat.py b/python/ctsm/test/test_unit_modify_fsurdat.py index ed8d7e3e46..2360df683d 100755 --- a/python/ctsm/test/test_unit_modify_fsurdat.py +++ b/python/ctsm/test/test_unit_modify_fsurdat.py @@ -10,9 +10,8 @@ import xarray as xr from ctsm import unit_testing -from ctsm.config_utils import lon_range_0_to_360 +from ctsm.longitude import Longitude from ctsm.modify_input_files.modify_fsurdat import ModifyFsurdat -from ctsm.test.test_unit_utils import wrong_lon_type_error_regex # Allow test names that pylint doesn't like; otherwise hard to make them # readable @@ -35,13 +34,18 @@ def setUp(self): # get cols, rows also self.min_lon = 2 # expects min_lon < max_lon self.min_lat = 3 # expects min_lat < max_lat + self.lon_type = 360 longxy, latixy, self.cols, self.rows = self._get_longxy_latixy( - _min_lon=self.min_lon, _max_lon=10, _min_lat=self.min_lat, _max_lat=12 + _min_lon=self.min_lon, + _max_lon=10, + _min_lat=self.min_lat, + _max_lat=12, + lon_type=self.lon_type, ) # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 - self.lon_1 = 3 - self.lon_2 = 5 # lon_1 < lon_2 + self.lon_1 = Longitude(3, self.lon_type) + self.lon_2 = Longitude(5, self.lon_type) # lon_1 < lon_2 self.lat_1 = 5 self.lat_2 = 7 # lat_1 < lat_2 @@ -94,17 +98,26 @@ def test_setvarLev(self): val_for_rectangle = 1.5 comp_lev0[ self.lat_1 - self.min_lat : self.lat_2 - self.min_lat + 1, - self.lon_1 - self.min_lon : self.lon_2 - self.min_lon + 1, + self.lon_1.get(self.lon_type) + - self.min_lon : self.lon_2.get(self.lon_type) + - self.min_lon + + 1, ] = val_for_rectangle comp_lev1[ ..., self.lat_1 - self.min_lat : self.lat_2 - self.min_lat + 1, - self.lon_1 - self.min_lon : self.lon_2 - self.min_lon + 1, + self.lon_1.get(self.lon_type) + - self.min_lon : self.lon_2.get(self.lon_type) + - self.min_lon + + 1, ] = val_for_rectangle comp_lev2[ ..., self.lat_1 - self.min_lat : self.lat_2 - self.min_lat + 1, - self.lon_1 - self.min_lon : self.lon_2 - self.min_lon + 1, + self.lon_1.get(self.lon_type) + - self.min_lon : self.lon_2.get(self.lon_type) + - self.min_lon + + 1, ] = val_for_rectangle # test setvar @@ -131,12 +144,12 @@ def test_getNotRectangle_lon1leLon2Lat1leLat2(self): min_lon = 2 # expects min_lon < max_lon min_lat = 3 # expects min_lat < max_lat longxy, latixy, cols, rows = self._get_longxy_latixy( - _min_lon=min_lon, _max_lon=7, _min_lat=min_lat, _max_lat=8 + _min_lon=min_lon, _max_lon=7, _min_lat=min_lat, _max_lat=8, lon_type=self.lon_type ) # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 - lon_1 = 3 - lon_2 = 5 # lon_1 < lon_2 + lon_1 = Longitude(3, self.lon_type) + lon_2 = Longitude(5, self.lon_type) # lon_1 < lon_2 lat_1 = 6 lat_2 = 8 # lat_1 < lat_2 rectangle = ModifyFsurdat._get_rectangle( @@ -155,7 +168,10 @@ def test_getNotRectangle_lon1leLon2Lat1leLat2(self): # Hardwire where I expect not_rectangle to be False (0) # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple - compare[lat_1 - min_lat : lat_2 - min_lat + 1, lon_1 - min_lon : lon_2 - min_lon + 1] = 0 + compare[ + lat_1 - min_lat : lat_2 - min_lat + 1, + lon_1.get(self.lon_type) - min_lon : lon_2.get(self.lon_type) - min_lon + 1, + ] = 0 np.testing.assert_array_equal(not_rectangle, compare) def test_getNotRectangle_lon1leLon2Lat1gtLat2(self): @@ -173,41 +189,42 @@ def test_getNotRectangle_lon1leLon2Lat1gtLat2(self): min_lon = -3 # expects min_lon < max_lon min_lat = -2 # expects min_lat < max_lat - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - self._get_longxy_latixy(_min_lon=min_lon, _max_lon=6, _min_lat=min_lat, _max_lat=5) - - # longxy, latixy, cols, rows = self._get_longxy_latixy( - # _min_lon=min_lon, _max_lon=6, _min_lat=min_lat, _max_lat=5 - # ) - - # # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 - # # I have chosen the lon/lat ranges to match their corresponding index - # # values to keep this simple (see usage below) - # lon_1 = 0 - # lon_2 = 4 # lon_1 < lon_2 - # lat_1 = 4 - # lat_2 = 0 # lat_1 > lat_2 - # rectangle = ModifyFsurdat._get_rectangle( - # lon_1=lon_1, - # lon_2=lon_2, - # lat_1=lat_1, - # lat_2=lat_2, - # longxy=longxy, - # latixy=latixy, - # ) - # not_rectangle = np.logical_not(rectangle) - # compare = np.ones((rows, cols)) - # # assert this to confirm intuitive understanding of these matrices - # self.assertEqual(np.size(not_rectangle), np.size(compare)) - - # # Hardwire where I expect not_rectangle to be False (0) - # # I have chosen the lon/lat ranges to match their corresponding index - # # values to keep this simple - # compare[: lat_2 - min_lat + 1, lon_1 - min_lon : lon_2 - min_lon + 1] = 0 - # compare[lat_1 - min_lat :, lon_1 - min_lon : lon_2 - min_lon + 1] = 0 - # np.testing.assert_array_equal(not_rectangle, compare) + longxy, latixy, cols, rows = self._get_longxy_latixy( + _min_lon=min_lon, _max_lon=6, _min_lat=min_lat, _max_lat=5, lon_type=self.lon_type + ) + + # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 + # I have chosen the lon/lat ranges to match their corresponding index + # values to keep this simple (see usage below) + lon_1 = Longitude(0, self.lon_type) + lon_2 = Longitude(4, self.lon_type) # lon_1 < lon_2 + lat_1 = 4 + lat_2 = 0 # lat_1 > lat_2 + rectangle = ModifyFsurdat._get_rectangle( + lon_1=lon_1, + lon_2=lon_2, + lat_1=lat_1, + lat_2=lat_2, + longxy=longxy, + latixy=latixy, + ) + not_rectangle = np.logical_not(rectangle) + compare = np.ones((rows, cols)) + # assert this to confirm intuitive understanding of these matrices + self.assertEqual(np.size(not_rectangle), np.size(compare)) + + # Hardwire where I expect not_rectangle to be False (0) + # I have chosen the lon/lat ranges to match their corresponding index + # values to keep this simple + compare[ + : lat_2 - min_lat + 1, + lon_1.get(self.lon_type) - min_lon : lon_2.get(self.lon_type) - min_lon + 1, + ] = 0 + compare[ + lat_1 - min_lat :, + lon_1.get(self.lon_type) - min_lon : lon_2.get(self.lon_type) - min_lon + 1, + ] = 0 + np.testing.assert_array_equal(not_rectangle, compare) def test_getNotRectangle_lon1gtLon2Lat1leLat2(self): """ @@ -224,14 +241,14 @@ def test_getNotRectangle_lon1gtLon2Lat1leLat2(self): min_lon = 1 # expects min_lon < max_lon min_lat = 1 # expects min_lat < max_lat longxy, latixy, cols, rows = self._get_longxy_latixy( - _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=90 + _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=90, lon_type=self.lon_type ) # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple (see usage below) - lon_1 = 4 - lon_2 = 2 # lon_1 > lon_2 + lon_1 = Longitude(4, self.lon_type) + lon_2 = Longitude(2, self.lon_type) # lon_1 > lon_2 lat_1 = 2 lat_2 = 3 # lat_1 < lat_2 rectangle = ModifyFsurdat._get_rectangle( @@ -250,8 +267,8 @@ def test_getNotRectangle_lon1gtLon2Lat1leLat2(self): # Hardwire where I expect not_rectangle to be False (0) # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple - compare[lat_1 - min_lat : lat_2 - min_lat + 1, : lon_2 - min_lon + 1] = 0 - compare[lat_1 - min_lat : lat_2 - min_lat + 1, lon_1 - min_lon :] = 0 + compare[lat_1 - min_lat : lat_2 - min_lat + 1, : lon_2.get(self.lon_type) - min_lon + 1] = 0 + compare[lat_1 - min_lat : lat_2 - min_lat + 1, lon_1.get(self.lon_type) - min_lon :] = 0 np.testing.assert_array_equal(not_rectangle, compare) def test_getNotRectangle_lon1gtLon2Lat1gtLat2(self): @@ -268,43 +285,39 @@ def test_getNotRectangle_lon1gtLon2Lat1gtLat2(self): # get cols, rows also min_lon = -8 # expects min_lon < max_lon min_lat = -9 # expects min_lat < max_lat + longxy, latixy, cols, rows = self._get_longxy_latixy( + _min_lon=min_lon, _max_lon=5, _min_lat=min_lat, _max_lat=6, lon_type=180 + ) + + # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 + # I have chosen the lon/lat ranges to match their corresponding index + # values to keep this simple (see usage below) + lon_type = 180 + lon_1 = Longitude(-1, lon_type=lon_type) + lon_2 = Longitude(-6, lon_type=lon_type) # lon_1 > lon_2 + lat_1 = 0 + lat_2 = -3 # lat_1 > lat_2 + rectangle = ModifyFsurdat._get_rectangle( + lon_1=lon_1, + lon_2=lon_2, + lat_1=lat_1, + lat_2=lat_2, + longxy=longxy, + latixy=latixy, + ) + not_rectangle = np.logical_not(rectangle) + compare = np.ones((rows, cols)) + # assert this to confirm intuitive understanding of these matrices + self.assertEqual(np.size(not_rectangle), np.size(compare)) - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - self._get_longxy_latixy(_min_lon=min_lon, _max_lon=5, _min_lat=min_lat, _max_lat=6) - - # longxy, latixy, cols, rows = self._get_longxy_latixy( - # _min_lon=min_lon, _max_lon=5, _min_lat=min_lat, _max_lat=6 - # ) - # # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 - # # I have chosen the lon/lat ranges to match their corresponding index - # # values to keep this simple (see usage below) - # lon_1 = -1 - # lon_2 = -6 # lon_1 > lon_2 - # lat_1 = 0 - # lat_2 = -3 # lat_1 > lat_2 - # rectangle = ModifyFsurdat._get_rectangle( - # lon_1=lon_1, - # lon_2=lon_2, - # lat_1=lat_1, - # lat_2=lat_2, - # longxy=longxy, - # latixy=latixy, - # ) - # not_rectangle = np.logical_not(rectangle) - # compare = np.ones((rows, cols)) - # # assert this to confirm intuitive understanding of these matrices - # self.assertEqual(np.size(not_rectangle), np.size(compare)) - - # # Hardwire where I expect not_rectangle to be False (0) - # # I have chosen the lon/lat ranges to match their corresponding index - # # values to keep this simple - # compare[: lat_2 - min_lat + 1, : lon_2 - min_lon + 1] = 0 - # compare[: lat_2 - min_lat + 1, lon_1 - min_lon :] = 0 - # compare[lat_1 - min_lat :, : lon_2 - min_lon + 1] = 0 - # compare[lat_1 - min_lat :, lon_1 - min_lon :] = 0 - # np.testing.assert_array_equal(not_rectangle, compare) + # Hardwire where I expect not_rectangle to be False (0) + # I have chosen the lon/lat ranges to match their corresponding index + # values to keep this simple + compare[: lat_2 - min_lat + 1, : lon_2.get(lon_type) - min_lon + 1] = 0 + compare[: lat_2 - min_lat + 1, lon_1.get(lon_type) - min_lon :] = 0 + compare[lat_1 - min_lat :, : lon_2.get(lon_type) - min_lon + 1] = 0 + compare[lat_1 - min_lat :, lon_1.get(lon_type) - min_lon :] = 0 + np.testing.assert_array_equal(not_rectangle, compare) def test_getNotRectangle_lonsStraddle0deg(self): """ @@ -321,14 +334,14 @@ def test_getNotRectangle_lonsStraddle0deg(self): min_lon = 0 # expects min_lon < max_lon min_lat = -5 # expects min_lat < max_lat longxy, latixy, cols, rows = self._get_longxy_latixy( - _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=5 + _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=5, lon_type=self.lon_type ) # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple (see usage below) - lon_1 = 355 - lon_2 = 5 # lon_1 > lon_2 + lon_1 = Longitude(355, self.lon_type) + lon_2 = Longitude(5, self.lon_type) # lon_1 > lon_2 lat_1 = -4 lat_2 = -6 # lat_1 > lat_2 rectangle = ModifyFsurdat._get_rectangle( @@ -347,10 +360,10 @@ def test_getNotRectangle_lonsStraddle0deg(self): # Hardwire where I expect not_rectangle to be False (0) # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple - compare[: lat_2 - min_lat + 1, : lon_2 - min_lon + 1] = 0 - compare[: lat_2 - min_lat + 1, lon_1 - min_lon :] = 0 - compare[lat_1 - min_lat :, : lon_2 - min_lon + 1] = 0 - compare[lat_1 - min_lat :, lon_1 - min_lon :] = 0 + compare[: lat_2 - min_lat + 1, : lon_2.get(self.lon_type) - min_lon + 1] = 0 + compare[: lat_2 - min_lat + 1, lon_1.get(self.lon_type) - min_lon :] = 0 + compare[lat_1 - min_lat :, : lon_2.get(self.lon_type) - min_lon + 1] = 0 + compare[lat_1 - min_lat :, lon_1.get(self.lon_type) - min_lon :] = 0 np.testing.assert_array_equal(not_rectangle, compare) def test_getNotRectangle_latsOutOfBounds(self): @@ -364,14 +377,14 @@ def test_getNotRectangle_latsOutOfBounds(self): min_lon = 0 # expects min_lon < max_lon min_lat = -5 # expects min_lat < max_lat longxy, latixy, _, _ = self._get_longxy_latixy( - _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=5 + _min_lon=min_lon, _max_lon=359, _min_lat=min_lat, _max_lat=5, lon_type=self.lon_type ) # get not_rectangle from user-defined lon_1, lon_2, lat_1, lat_2 # I have chosen the lon/lat ranges to match their corresponding index # values to keep this simple (see usage below) - lon_1 = 355 - lon_2 = 5 + lon_1 = Longitude(355, self.lon_type) + lon_2 = Longitude(5, self.lon_type) lat_1 = -91 lat_2 = 91 with self.assertRaisesRegex( @@ -471,7 +484,7 @@ def test_set_varlist_toohighdim_uppercase(self): ): self.modify_fsurdat.check_varlist(settings, allow_uppercase_vars=True) - def _get_longxy_latixy(self, _min_lon, _max_lon, _min_lat, _max_lat): + def _get_longxy_latixy(self, _min_lon, _max_lon, _min_lat, _max_lat, lon_type): """ Return longxy, latixy, cols, rows """ @@ -479,7 +492,8 @@ def _get_longxy_latixy(self, _min_lon, _max_lon, _min_lat, _max_lat): rows = _max_lat - _min_lat + 1 long = np.arange(_min_lon, _max_lon + 1) - long = [lon_range_0_to_360(longitude) for longitude in long] + if lon_type == 180: + long = [Longitude(longitude, lon_type).get(360) for longitude in long] longxy = long * np.ones((rows, cols)) compare = np.repeat([long], rows, axis=0) # alternative way to form # assert this to confirm intuitive understanding of these matrices diff --git a/python/ctsm/test/test_unit_subset_data.py b/python/ctsm/test/test_unit_subset_data.py index a089a11a90..d1b27d12cc 100755 --- a/python/ctsm/test/test_unit_subset_data.py +++ b/python/ctsm/test/test_unit_subset_data.py @@ -18,11 +18,10 @@ # pylint: disable=wrong-import-position from ctsm import unit_testing -from ctsm.subset_data import get_parser, setup_files, check_args +from ctsm.subset_data import get_parser, setup_files, check_args, _set_up_regional_case from ctsm.path_utils import path_to_ctsm_root -from ctsm.test.test_unit_utils import wrong_lon_type_error_regex -# pylint: disable=invalid-name +# pylint: disable=invalid-name,too-many-public-methods class TestSubsetData(unittest.TestCase): @@ -45,7 +44,7 @@ def test_inputdata_setup_files_basic(self): """ Test """ - check_args(self.args) + self.args = check_args(self.args) files = setup_files(self.args, self.defaults, self.cesmroot) self.assertEqual( files["fsurf_in"], @@ -67,7 +66,7 @@ def test_inputdata_setup_files_inputdata_dne(self): """ Test that inputdata directory does not exist """ - check_args(self.args) + self.args = check_args(self.args) self.defaults.set("main", "clmforcingindir", "/zztop") with self.assertRaisesRegex(SystemExit, "inputdata directory does not exist"): setup_files(self.args, self.defaults, self.cesmroot) @@ -108,7 +107,7 @@ def test_check_args_outsurfdat_provided(self): """ sys.argv = ["subset_data", "point", "--create-surface", "--out-surface", "outputsurface.nc"] self.args = self.parser.parse_args() - check_args(self.args) + self.args = check_args(self.args) files = setup_files(self.args, self.defaults, self.cesmroot) self.assertEqual( files["fsurf_out"], @@ -203,7 +202,7 @@ def test_inputdata_setup_files_bad_inputdata_arg(self): """ Test that inputdata directory provided on command line does not exist if it's bad """ - check_args(self.args) + self.args = check_args(self.args) self.args.inputdatadir = "/zztop" with self.assertRaisesRegex(SystemExit, "inputdata directory does not exist"): setup_files(self.args, self.defaults, self.cesmroot) @@ -273,14 +272,279 @@ def test_complex_option_works(self): "--verbose", "--crop", ] - self.args = self.parser.parse_args() - check_args(self.args) + args = self.parser.parse_args() + args = check_args(args) + _set_up_regional_case(args) + + def test_region_lon_type_360_ok(self): + """ + In region mode, test that --lon-type 360 works with valid longitudes + """ + lon_type = 360 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + str(lon_type), + "--lon1", + "320", + "--lon2", + "340", + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + self.assertEqual(args.lon1.get(lon_type), 320) + self.assertEqual(args.lon2.get(lon_type), 340) + _set_up_regional_case(args) + + def test_region_lon_type_360_toolow(self): + """ + In region mode, test that --lon-type 360 fails with a longitude value that's below [0, 360] + """ + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + "360", + "--lon1", + "-1", + "--lon2", + "360", + ] + self.parser = get_parser() + args = self.parser.parse_args() + with self.assertRaisesRegex( + ValueError, + r"lon_in needs to be in the range \[0, 360\]", + ): + check_args(args) + + def test_region_lon_type_360_ok_at_360(self): + """ + In region mode, test that --lon-type 360 works with a longitude value of 360 + """ + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + "360", + "--lon1", + "320", + "--lon2", + "360", + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + _set_up_regional_case(args) + + def test_region_lon_type_360_crosses_pm_errors(self): + """ + In region mode, test that --lon-type 360 errors if lon range crosses Prime Meridian + """ + lon1 = 320 + lon2 = 300 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + "360", + "--lon1", + str(lon1), + "--lon2", + str(lon2), + ] + self.parser = get_parser() + args = self.parser.parse_args() + + expected_err_msg = rf"--lon1 \({lon1}[\.\d]*\) must be < --lon2 \({lon2}[\.\d]*\)" + with self.assertRaisesRegex(ValueError, expected_err_msg): + check_args(args) + + def test_region_lon_type_180_crosses_pm_errors(self): + """ + In region mode, test that --lon-type 180 errors if lon range crosses Prime Meridian + """ + lon1 = -5 + lon2 = 5 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + "180", + "--lon1", + str(lon1), + "--lon2", + str(lon2), + ] + self.parser = get_parser() + args = self.parser.parse_args() + + lon1_conv = lon1 % 360 + lon2_conv = lon2 % 360 + expected_err_msg = ( + "After converting to --lon-type 360, " + + rf"--lon1 \({lon1_conv}[\.\d]*\) must be < --lon2 \({lon2_conv}[\.\d]*\)" + ) + with self.assertRaisesRegex(ValueError, expected_err_msg): + check_args(args) + + def test_region_lon_type_180_neg_ok(self): + """ + In region mode, test that --lon-type 180 works with valid negative longitudes + """ + lon1 = -87 + lon2 = -24 + lon_type = 180 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + str(lon_type), + "--lon1", + str(lon1), + "--lon2", + str(lon2), + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + self.assertEqual(args.lon1.get(lon_type), lon1) + self.assertEqual(args.lon2.get(lon_type), lon2) + _set_up_regional_case(args) + + def test_region_lon_type_180_pos_ok(self): + """ + In region mode, test that --lon-type 180 works with valid positive longitudes + """ + lon1 = 24 + lon2 = 87 + lon_type = 180 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + str(lon_type), + "--lon1", + str(lon1), + "--lon2", + str(lon2), + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + self.assertEqual(args.lon1.get(lon_type), lon1) + self.assertEqual(args.lon2.get(lon_type), lon2) + _set_up_regional_case(args) + + def test_point_ambiguous_lon_errors(self): + """ + In point mode, test that an error is thrown if you give it an ambiguous longitude without + also giving --lon-type + """ + sys.argv = [ + "subset_data", + "point", + "--create-domain", + "--verbose", + "--lat", + "0", + "--lon", + "87", + ] + self.parser = get_parser() + args = self.parser.parse_args() + with self.assertRaisesRegex( + argparse.ArgumentTypeError, + "When providing an ambiguous longitude, you must specify --lon-type 180 or 360", + ): + check_args(args) - # When CTSM issue #3001 is fixed, this test should be replaced with one that checks for correct - # conversion of longitudes specified in the [-180, 180) format. - def test_negative_lon_errors(self): + def test_point_unambiguous_lon_180_ok(self): """ - Test that a negative longitude results in a descriptive error + In point mode, test that no error is thrown if an unambiguous longitude is given without + specifying --lon-type 180 + """ + sys.argv = [ + "subset_data", + "point", + "--create-domain", + "--verbose", + "--lat", + "0", + "--lon", + "-87", + ] + self.parser = get_parser() + args = self.parser.parse_args() + check_args(args) + + def test_point_unambiguous_lon_360_ok(self): + """ + In point mode, test that no error is thrown if an unambiguous longitude is given without + specifying --lon-type 360 + """ + sys.argv = [ + "subset_data", + "point", + "--create-domain", + "--verbose", + "--lat", + "0", + "--lon", + "194", + ] + self.parser = get_parser() + args = self.parser.parse_args() + check_args(args) + + def test_region_ambiguous_lon_errors(self): + """ + In region mode, test that an error is thrown if you give it one ambiguous longitude without + also giving --lon-type """ sys.argv = [ "subset_data", @@ -292,12 +556,122 @@ def test_negative_lon_errors(self): "--lat2", "40", "--lon1", - "-20", + "-24", "--lon2", + "87", + ] + self.parser = get_parser() + args = self.parser.parse_args() + with self.assertRaisesRegex( + argparse.ArgumentTypeError, + "When providing an ambiguous longitude, you must specify --lon-type 180 or 360", + ): + check_args(args) + + def test_region_ambiguous_lons_errors(self): + """ + In region mode, test that an error is thrown if you give two ambiguous longitudes without + also giving --lon-type + """ + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon1", + "24", + "--lon2", + "87", + ] + self.parser = get_parser() + args = self.parser.parse_args() + with self.assertRaisesRegex( + argparse.ArgumentTypeError, + "When providing an ambiguous longitude, you must specify --lon-type 180 or 360", + ): + check_args(args) + + def test_region_unambiguous_lons_180_ok(self): + """ + In region mode, test that no error is thrown if two unambiguous longitudes are given without + specifying --lon-type 180 + """ + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon1", + "-87", + "--lon2", + "-24", + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + _set_up_regional_case(args) + + def test_region_unambiguous_lons_360_ok(self): + """ + In region mode, test that no error is thrown if two unambiguous longitudes are given without + specifying --lon-type 360 + """ + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", "40", + "--lon1", + "194", + "--lon2", + "287", ] - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - self.args = self.parser.parse_args() + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + _set_up_regional_case(args) + + def test_region_lon_type_180_ok_at_180(self): + """ + In region mode, test that --lon-type 180 passes at lon 180 + """ + lon1 = 24 + lon2 = 180 + lon_type = 180 + sys.argv = [ + "subset_data", + "region", + "--create-domain", + "--verbose", + "--lat1", + "0", + "--lat2", + "40", + "--lon-type", + str(lon_type), + "--lon1", + str(lon1), + "--lon2", + str(lon2), + ] + self.parser = get_parser() + args = self.parser.parse_args() + args = check_args(args) + self.assertEqual(args.lon1.get(lon_type), lon1) + self.assertEqual(args.lon2.get(lon_type), lon2) + _set_up_regional_case(args) if __name__ == "__main__": diff --git a/python/ctsm/test/test_unit_utils.py b/python/ctsm/test/test_unit_utils.py index f1d94fb1c7..75b240b365 100755 --- a/python/ctsm/test/test_unit_utils.py +++ b/python/ctsm/test/test_unit_utils.py @@ -9,15 +9,12 @@ from ctsm import unit_testing from ctsm.utils import fill_template_file, ensure_iterable -from ctsm.config_utils import lon_range_0_to_360, _handle_config_value +from ctsm.config_utils import _handle_config_value # Allow names that pylint doesn't like, because otherwise I find it hard # to make readable unit test names # pylint: disable=invalid-name -# When CTSM Issue #3001 is resolved, this should be deleted -wrong_lon_type_error_regex = r"\[-180, 0\).*\[0, 360\)" - class TestUtilsFillTemplateFile(unittest.TestCase): """Tests of utils: fill_template_file""" @@ -58,61 +55,6 @@ def test_fillTemplateFile_basic(self): self.assertEqual(final_contents, expected_final_text) -class TestUtilsLonRange0to360(unittest.TestCase): - """Test of utils: lon_range_0_to_360""" - - def test_lonRange0To360_lonIsNeg180(self): - """ - Tests that negative inputs to lon_range_0_to_360 get 360 added to them - """ - inval = -180 - - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - lon_range_0_to_360(inval) - - # result = lon_range_0_to_360(inval) - # self.assertEqual(result, inval + 360) - - def test_lonRange0To360_lonIsNegGreaterThan1(self): - """ - Tests that negative inputs to lon_range_0_to_360 get 360 added to them - """ - inval = -0.001 - - # When CTSM Issue #3001 is resolved, this assertRaisesRegex block should be deleted and the - # rest of this test uncommented - with self.assertRaisesRegex(NotImplementedError, wrong_lon_type_error_regex): - lon_range_0_to_360(inval) - - # result = lon_range_0_to_360(inval) - # self.assertEqual(result, inval + 360) - - def test_lonRange0To360_lonIs0(self): - """ - Tests that input to lon_range_0_to_360 of 0 remains unchanged - """ - inval = 0 - result = lon_range_0_to_360(inval) - self.assertEqual(result, inval) - - def test_lonRange0To360_lonIs360(self): - """ - Tests that input to lon_range_0_to_360 of 360 remains unchanged - """ - inval = 360 - result = lon_range_0_to_360(inval) - self.assertEqual(result, inval) - - def test_lonRange0To360_outOfBounds(self): - """ - Tests that lon_range_0_to_360 aborts gracefully when lon = 361 - """ - with self.assertRaisesRegex(SystemExit, "lon_in needs to be in the range 0 to 360"): - _ = lon_range_0_to_360(361) - - class TestUtilsHandleConfigValue(unittest.TestCase): """Test of utils: _handle_config_value""" diff --git a/python/ctsm/test/testinputs/modify_fsurdat_1x1mexicocity.cfg b/python/ctsm/test/testinputs/modify_fsurdat_1x1mexicocity.cfg index 1190f003b2..398fa20232 100644 --- a/python/ctsm/test/testinputs/modify_fsurdat_1x1mexicocity.cfg +++ b/python/ctsm/test/testinputs/modify_fsurdat_1x1mexicocity.cfg @@ -25,6 +25,7 @@ lnd_lat_1 = -90 lnd_lat_2 = 90 lnd_lon_1 = 0 lnd_lon_2 = 360 +lon_type = 360 # Section for subgrid_fractions [modify_fsurdat_subgrid_fractions] diff --git a/python/ctsm/test/testinputs/modify_fsurdat_opt_sections.cfg b/python/ctsm/test/testinputs/modify_fsurdat_opt_sections.cfg index 36d7b50713..be2fa85879 100644 --- a/python/ctsm/test/testinputs/modify_fsurdat_opt_sections.cfg +++ b/python/ctsm/test/testinputs/modify_fsurdat_opt_sections.cfg @@ -25,6 +25,7 @@ lnd_lat_1 = -90 lnd_lat_2 = 90 lnd_lon_1 = 0 lnd_lon_2 = 360 +lon_type = 360 # Section for subgrid_fractions [modify_fsurdat_subgrid_fractions] diff --git a/python/ctsm/test/testinputs/modify_fsurdat_short.cfg b/python/ctsm/test/testinputs/modify_fsurdat_short.cfg index 3a7f885c30..03f4f603f2 100644 --- a/python/ctsm/test/testinputs/modify_fsurdat_short.cfg +++ b/python/ctsm/test/testinputs/modify_fsurdat_short.cfg @@ -28,3 +28,4 @@ lnd_lat_1 = -90 lnd_lat_2 = 90 lnd_lon_1 = 0 lnd_lon_2 = 360 +lon_type = 360 diff --git a/python/ctsm/test/testinputs/modify_fsurdat_short_nofiles.cfg b/python/ctsm/test/testinputs/modify_fsurdat_short_nofiles.cfg index 3e5aada24b..9e38ae085e 100644 --- a/python/ctsm/test/testinputs/modify_fsurdat_short_nofiles.cfg +++ b/python/ctsm/test/testinputs/modify_fsurdat_short_nofiles.cfg @@ -9,3 +9,4 @@ lnd_lat_1 = -90 lnd_lat_2 = 90 lnd_lon_1 = 0 lnd_lon_2 = 360 +lon_type = 360 diff --git a/src/biogeochem/CNFireBaseMod.F90 b/src/biogeochem/CNFireBaseMod.F90 index 5e4fd2caef..2f9e99ea44 100644 --- a/src/biogeochem/CNFireBaseMod.F90 +++ b/src/biogeochem/CNFireBaseMod.F90 @@ -14,7 +14,7 @@ module CNFireBaseMod ! climatological lightning data. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use clm_varctl , only : iulog use clm_varpar , only : nlevgrnd diff --git a/src/biogeochem/CNFireFactoryMod.F90 b/src/biogeochem/CNFireFactoryMod.F90 index 27a1085336..44407da927 100644 --- a/src/biogeochem/CNFireFactoryMod.F90 +++ b/src/biogeochem/CNFireFactoryMod.F90 @@ -89,7 +89,6 @@ subroutine create_cnfire_method( NLFilename, cnfire_method ) ! is determined based on a namelist parameter. ! ! !USES: - use shr_kind_mod , only : SHR_KIND_CL use FireMethodType , only : fire_method_type use CNFireNoFireMod , only : cnfire_nofire_type use CNFireLi2014Mod , only : cnfire_li2014_type diff --git a/src/biogeochem/CNFireLi2014Mod.F90 b/src/biogeochem/CNFireLi2014Mod.F90 index 4078da740b..ff6155367a 100644 --- a/src/biogeochem/CNFireLi2014Mod.F90 +++ b/src/biogeochem/CNFireLi2014Mod.F90 @@ -14,7 +14,7 @@ module CNFireLi2014Mod ! climatological lightning data. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_const_mod , only : SHR_CONST_PI,SHR_CONST_TKFRZ use shr_infnan_mod , only : shr_infnan_isnan use clm_varctl , only : iulog diff --git a/src/biogeochem/CNFireLi2016Mod.F90 b/src/biogeochem/CNFireLi2016Mod.F90 index b7c14938f7..11278b8016 100644 --- a/src/biogeochem/CNFireLi2016Mod.F90 +++ b/src/biogeochem/CNFireLi2016Mod.F90 @@ -14,7 +14,7 @@ module CNFireLi2016Mod ! (clm50fire), CRUNCEPv5, and climatological lightning data. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_const_mod , only : SHR_CONST_PI,SHR_CONST_TKFRZ use shr_infnan_mod , only : shr_infnan_isnan use clm_varctl , only : iulog diff --git a/src/biogeochem/CNFireLi2021Mod.F90 b/src/biogeochem/CNFireLi2021Mod.F90 index d1fd9338b8..95a75a7c94 100644 --- a/src/biogeochem/CNFireLi2021Mod.F90 +++ b/src/biogeochem/CNFireLi2021Mod.F90 @@ -14,7 +14,7 @@ module CNFireLi2021Mod ! (clm50fire), CRUNCEPv5, and climatological lightning data. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_const_mod , only : SHR_CONST_PI,SHR_CONST_TKFRZ use shr_infnan_mod , only : shr_infnan_isnan use clm_varctl , only : iulog diff --git a/src/biogeochem/CNFireLi2024Mod.F90 b/src/biogeochem/CNFireLi2024Mod.F90 index 5f7c0019f6..dbad9a773d 100644 --- a/src/biogeochem/CNFireLi2024Mod.F90 +++ b/src/biogeochem/CNFireLi2024Mod.F90 @@ -14,7 +14,7 @@ module CNFireLi2024Mod ! (clm50fire), CRUNCEPv5, and climatological lightning data. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_const_mod , only : SHR_CONST_PI,SHR_CONST_TKFRZ use shr_infnan_mod , only : shr_infnan_isnan use clm_varctl , only : iulog diff --git a/src/biogeochem/CNFireNoFireMod.F90 b/src/biogeochem/CNFireNoFireMod.F90 index 6785faa851..e0605585e9 100644 --- a/src/biogeochem/CNFireNoFireMod.F90 +++ b/src/biogeochem/CNFireNoFireMod.F90 @@ -7,7 +7,7 @@ module CNFireNoFireMod ! module for fire dynamics with fire explicitly turned off ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod , only : r8 => shr_kind_r8 use decompMod , only : bounds_type use atm2lndType , only : atm2lnd_type use CNVegStateType , only : cnveg_state_type diff --git a/src/biogeochem/DustEmisFactory.F90 b/src/biogeochem/DustEmisFactory.F90 index cdae8bc20a..fdd6d19e1d 100644 --- a/src/biogeochem/DustEmisFactory.F90 +++ b/src/biogeochem/DustEmisFactory.F90 @@ -40,7 +40,6 @@ function create_dust_emissions(bounds, NLFilename) result(dust_emis) use DustEmisZender2003, only : dust_emis_zender2003_type use DustEmisLeung2023 , only : dust_emis_leung2023_type use decompMod , only : bounds_type - use shr_kind_mod , only : CL => shr_kind_cl use shr_dust_emis_mod , only : is_dust_emis_zender, is_dust_emis_leung implicit none ! Arguments diff --git a/src/biogeochem/FATESFireDataMod.F90 b/src/biogeochem/FATESFireDataMod.F90 index 06845cf03a..9b97528a28 100644 --- a/src/biogeochem/FATESFireDataMod.F90 +++ b/src/biogeochem/FATESFireDataMod.F90 @@ -7,7 +7,7 @@ module FATESFireDataMod ! module for FATES to obtain fire inputs from data ! ! !USES: - use shr_kind_mod, only: r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod, only: r8 => shr_kind_r8 use shr_log_mod, only: errmsg => shr_log_errMsg use abortutils, only: endrun use clm_varctl, only: iulog diff --git a/src/biogeochem/FATESFireNoDataMod.F90 b/src/biogeochem/FATESFireNoDataMod.F90 index 6bc9162d43..4034b68e97 100644 --- a/src/biogeochem/FATESFireNoDataMod.F90 +++ b/src/biogeochem/FATESFireNoDataMod.F90 @@ -7,7 +7,7 @@ module FATESFireNoDataMod ! module for FATES when not obtaining fire inputs from data ! ! !USES: - use shr_kind_mod, only: r8 => shr_kind_r8, CL => shr_kind_CL + use shr_kind_mod, only: r8 => shr_kind_r8 use shr_log_mod, only: errmsg => shr_log_errMsg use abortutils, only: endrun use clm_varctl, only: iulog diff --git a/src/biogeochem/SatellitePhenologyMod.F90 b/src/biogeochem/SatellitePhenologyMod.F90 index 61c9fc845a..ba9610d536 100644 --- a/src/biogeochem/SatellitePhenologyMod.F90 +++ b/src/biogeochem/SatellitePhenologyMod.F90 @@ -9,7 +9,7 @@ module SatellitePhenologyMod ! so that DryDeposition code can get estimates of LAI differences between months. ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL, CS => shr_kind_CS + use shr_kind_mod , only : r8 => shr_kind_r8, CS => shr_kind_CS use shr_log_mod , only : errMsg => shr_log_errMsg use decompMod , only : bounds_type use abortutils , only : endrun diff --git a/src/biogeochem/test/CNPhenology_test/CMakeLists.txt b/src/biogeochem/test/CNPhenology_test/CMakeLists.txt index 283e089ba6..c94f1502df 100644 --- a/src/biogeochem/test/CNPhenology_test/CMakeLists.txt +++ b/src/biogeochem/test/CNPhenology_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(CNPhenology TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeochem/test/CNVegComputeSeed_test/CMakeLists.txt b/src/biogeochem/test/CNVegComputeSeed_test/CMakeLists.txt index b958439031..3e15ab2bed 100644 --- a/src/biogeochem/test/CNVegComputeSeed_test/CMakeLists.txt +++ b/src/biogeochem/test/CNVegComputeSeed_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(CNVegComputeSeed TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeochem/test/DustEmis_test/CMakeLists.txt b/src/biogeochem/test/DustEmis_test/CMakeLists.txt index d312b721b9..88570923b8 100644 --- a/src/biogeochem/test/DustEmis_test/CMakeLists.txt +++ b/src/biogeochem/test/DustEmis_test/CMakeLists.txt @@ -5,4 +5,6 @@ set (pfunit_sources add_pfunit_ctest(DustEmis TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeochem/test/Latbaset_test/CMakeLists.txt b/src/biogeochem/test/Latbaset_test/CMakeLists.txt index d9f1c044f3..11c266bc21 100644 --- a/src/biogeochem/test/Latbaset_test/CMakeLists.txt +++ b/src/biogeochem/test/Latbaset_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(CropTypeLatbaset TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/Balance_test/CMakeLists.txt b/src/biogeophys/test/Balance_test/CMakeLists.txt index e140323124..ad6c157e4c 100644 --- a/src/biogeophys/test/Balance_test/CMakeLists.txt +++ b/src/biogeophys/test/Balance_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(balance TEST_SOURCES "test_Balance.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/Daylength_test/CMakeLists.txt b/src/biogeophys/test/Daylength_test/CMakeLists.txt index bd2d6407a9..15c2543d8f 100644 --- a/src/biogeophys/test/Daylength_test/CMakeLists.txt +++ b/src/biogeophys/test/Daylength_test/CMakeLists.txt @@ -4,4 +4,6 @@ set (pfunit_sources add_pfunit_ctest(Daylength TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/HillslopeHydrology_test/CMakeLists.txt b/src/biogeophys/test/HillslopeHydrology_test/CMakeLists.txt index 078934cc78..7573c0ce59 100644 --- a/src/biogeophys/test/HillslopeHydrology_test/CMakeLists.txt +++ b/src/biogeophys/test/HillslopeHydrology_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(HillslopeHydrologyUtils TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/Irrigation_test/CMakeLists.txt b/src/biogeophys/test/Irrigation_test/CMakeLists.txt index 4acf72961d..0fd9691e7f 100644 --- a/src/biogeophys/test/Irrigation_test/CMakeLists.txt +++ b/src/biogeophys/test/Irrigation_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(irrigation TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/Photosynthesis_test/CMakeLists.txt b/src/biogeophys/test/Photosynthesis_test/CMakeLists.txt index 628a98994a..457e62b013 100644 --- a/src/biogeophys/test/Photosynthesis_test/CMakeLists.txt +++ b/src/biogeophys/test/Photosynthesis_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(Photosynthesis TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/SnowHydrology_test/CMakeLists.txt b/src/biogeophys/test/SnowHydrology_test/CMakeLists.txt index 9e1738ab83..bfa8edc25e 100644 --- a/src/biogeophys/test/SnowHydrology_test/CMakeLists.txt +++ b/src/biogeophys/test/SnowHydrology_test/CMakeLists.txt @@ -5,4 +5,6 @@ set (pfunit_sources add_pfunit_ctest(SnowHydrology TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/TotalWaterAndHeat_test/CMakeLists.txt b/src/biogeophys/test/TotalWaterAndHeat_test/CMakeLists.txt index 424515e414..d995b0a431 100644 --- a/src/biogeophys/test/TotalWaterAndHeat_test/CMakeLists.txt +++ b/src/biogeophys/test/TotalWaterAndHeat_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(total_water_and_heat TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/WaterTracerContainerType_test/CMakeLists.txt b/src/biogeophys/test/WaterTracerContainerType_test/CMakeLists.txt index 645972aa1e..9a30b674dd 100644 --- a/src/biogeophys/test/WaterTracerContainerType_test/CMakeLists.txt +++ b/src/biogeophys/test/WaterTracerContainerType_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(water_tracer_container TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/WaterTracerUtils_test/CMakeLists.txt b/src/biogeophys/test/WaterTracerUtils_test/CMakeLists.txt index 1a65bbfadd..eb259cb421 100644 --- a/src/biogeophys/test/WaterTracerUtils_test/CMakeLists.txt +++ b/src/biogeophys/test/WaterTracerUtils_test/CMakeLists.txt @@ -5,4 +5,6 @@ set (pfunit_sources add_pfunit_ctest(water_tracer_utils TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/WaterType_test/CMakeLists.txt b/src/biogeophys/test/WaterType_test/CMakeLists.txt index 3f0ab409da..f1e77198ed 100644 --- a/src/biogeophys/test/WaterType_test/CMakeLists.txt +++ b/src/biogeophys/test/WaterType_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(water_type TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/biogeophys/test/Wateratm2lnd_test/CMakeLists.txt b/src/biogeophys/test/Wateratm2lnd_test/CMakeLists.txt index 1ddb840431..1aa11a2727 100644 --- a/src/biogeophys/test/Wateratm2lnd_test/CMakeLists.txt +++ b/src/biogeophys/test/Wateratm2lnd_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(water_atm2lnd TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/cpl/lilac/lnd_shr_methods.F90 b/src/cpl/lilac/lnd_shr_methods.F90 index f8ca9f4f3e..aeb7e096fd 100644 --- a/src/cpl/lilac/lnd_shr_methods.F90 +++ b/src/cpl/lilac/lnd_shr_methods.F90 @@ -4,7 +4,7 @@ module lnd_shr_methods use ESMF , only : ESMF_LogWrite, ESMF_LOGMSG_ERROR, ESMF_FAILURE use ESMF , only : ESMF_LOGERR_PASSTHRU, ESMF_LogFoundError use ESMF , only : ESMF_SUCCESS, ESMF_Field, ESMF_LOGMSG_INFO - use shr_kind_mod , only : r8 => shr_kind_r8, cl=>shr_kind_cl, cs=>shr_kind_cs + use shr_kind_mod , only : r8 => shr_kind_r8, cs=>shr_kind_cs use shr_sys_mod , only : shr_sys_abort implicit none diff --git a/src/cpl/nuopc/lnd_comp_nuopc.F90 b/src/cpl/nuopc/lnd_comp_nuopc.F90 index 8ee6c2014e..b7ef7216d9 100644 --- a/src/cpl/nuopc/lnd_comp_nuopc.F90 +++ b/src/cpl/nuopc/lnd_comp_nuopc.F90 @@ -38,6 +38,7 @@ module lnd_comp_nuopc use clm_varctl , only : inst_index, inst_suffix, inst_name use clm_varctl , only : single_column, clm_varctl_set, iulog use clm_varctl , only : nsrStartup, nsrContinue, nsrBranch + use clm_varctl , only : FL => fname_len use clm_time_manager , only : set_timemgr_init, advance_timestep use clm_time_manager , only : update_rad_dtime use clm_time_manager , only : get_nstep, get_step_size @@ -383,7 +384,7 @@ subroutine InitializeRealize(gcomp, importState, exportState, clock, rc) integer :: shrlogunit ! original log unit integer :: n, ni, nj ! Indices character(len=CL) :: cvalue ! config data - character(len=CL) :: meshfile_mask ! filename of mesh file with land mask + character(len=FL) :: meshfile_mask ! filename of mesh file with land mask character(len=CL) :: ctitle ! case description title character(len=CL) :: caseid ! case identifier name real(r8) :: scol_lat ! single-column latitude @@ -392,7 +393,7 @@ subroutine InitializeRealize(gcomp, importState, exportState, clock, rc) real(r8) :: scol_frac ! single-column frac integer :: scol_mask ! single-column mask real(r8) :: scol_spval ! single-column special value to indicate it isn't set - character(len=CL) :: single_column_lnd_domainfile ! domain filename to use for single-column mode (i.e. SCAM) + character(len=FL) :: single_column_lnd_domainfile ! domain filename to use for single-column mode (i.e. SCAM) type(bounds_type) :: bounds ! bounds type(ESMF_Field) :: lfield ! Land field read in character(CL) ,pointer :: lfieldnamelist(:) => null() ! Land field namelist item sent with land field diff --git a/src/cpl/nuopc/lnd_comp_shr.F90 b/src/cpl/nuopc/lnd_comp_shr.F90 index 8dc9738f7a..94a12603d6 100644 --- a/src/cpl/nuopc/lnd_comp_shr.F90 +++ b/src/cpl/nuopc/lnd_comp_shr.F90 @@ -3,13 +3,14 @@ module lnd_comp_shr ! Model mesh info is here in order to be leveraged by CDEPS in line calls use ESMF , only : ESMF_Clock, ESMF_Mesh - use shr_kind_mod, only : r8 => shr_kind_r8, cl=>shr_kind_cl + use shr_kind_mod, only : r8 => shr_kind_r8 + use clm_varctl , only : fl => fname_len implicit none public type(ESMF_Clock) :: model_clock ! model clock type(ESMF_Mesh) :: mesh ! model_mesh - character(len=cl) :: model_meshfile ! model mesh file + character(len=fl) :: model_meshfile ! model mesh file end module lnd_comp_shr diff --git a/src/cpl/share_esmf/ExcessIceStreamType.F90 b/src/cpl/share_esmf/ExcessIceStreamType.F90 index 92d5632aff..a3b31c17b8 100644 --- a/src/cpl/share_esmf/ExcessIceStreamType.F90 +++ b/src/cpl/share_esmf/ExcessIceStreamType.F90 @@ -21,7 +21,7 @@ module ExcessIceStreamType use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl use shr_log_mod , only : errMsg => shr_log_errMsg use spmdMod , only : mpicom, masterproc - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use abortutils , only : endrun use decompMod , only : bounds_type @@ -45,8 +45,8 @@ module ExcessIceStreamType end type excessicestream_type ! ! PRIVATE DATA: type, private :: streamcontrol_type - character(len=CL) :: stream_fldFileName_exice ! data Filename - character(len=CL) :: stream_meshfile_exice ! mesh Filename + character(len=FL) :: stream_fldFileName_exice ! data Filename + character(len=FL) :: stream_meshfile_exice ! mesh Filename character(len=CL) :: stream_mapalgo_exice ! map algo contains procedure, private :: ReadNML ! Read in namelist @@ -264,8 +264,8 @@ subroutine ReadNML(this, bounds, NLFilename) integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag logical :: use_excess_ice_streams = .false. ! logical to turn on use of excess ice streams - character(len=CL) :: stream_fldFileName_exice = ' ' - character(len=CL) :: stream_meshfile_exice = ' ' + character(len=FL) :: stream_fldFileName_exice = ' ' + character(len=FL) :: stream_meshfile_exice = ' ' character(len=CL) :: stream_mapalgo_exice = 'bilinear' character(len=*), parameter :: namelist_name = 'exice_streams' ! MUST agree with name in namelist and read character(len=*), parameter :: subName = "('exice_streams::ReadNML')" diff --git a/src/cpl/share_esmf/FireDataBaseType.F90 b/src/cpl/share_esmf/FireDataBaseType.F90 index 42bb874812..b84e3bfa33 100644 --- a/src/cpl/share_esmf/FireDataBaseType.F90 +++ b/src/cpl/share_esmf/FireDataBaseType.F90 @@ -11,7 +11,7 @@ module FireDataBaseType use dshr_strdata_mod , only : shr_strdata_type use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL use shr_log_mod , only : errMsg => shr_log_errMsg - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use spmdMod , only : masterproc, mpicom, iam use abortutils , only : endrun use decompMod , only : bounds_type @@ -171,8 +171,8 @@ subroutine hdm_init( this, bounds, NLFilename ) integer :: stream_year_first_popdens ! first year in pop. dens. stream to use integer :: stream_year_last_popdens ! last year in pop. dens. stream to use integer :: model_year_align_popdens ! align stream_year_first_hdm with - character(len=CL) :: stream_fldFileName_popdens ! population density streams filename - character(len=CL) :: stream_meshfile_popdens ! population density streams filename + character(len=FL) :: stream_fldFileName_popdens ! population density streams filename + character(len=FL) :: stream_meshfile_popdens ! population density streams filename character(len=CL) :: popdensmapalgo ! mapping alogrithm for population density character(len=CL) :: popdens_tintalgo ! time interpolation alogrithm for population density integer :: rc @@ -338,8 +338,8 @@ subroutine lnfm_init( this, bounds, NLFilename ) integer :: stream_year_first_lightng ! first year in Lightning stream to use integer :: stream_year_last_lightng ! last year in Lightning stream to use integer :: model_year_align_lightng ! align stream_year_first_lnfm with - character(len=CL) :: stream_fldFileName_lightng ! lightning stream filename to read - character(len=CL) :: stream_meshfile_lightng ! lightning stream filename to read + character(len=FL) :: stream_fldFileName_lightng ! lightning stream filename to read + character(len=FL) :: stream_meshfile_lightng ! lightning stream filename to read character(len=CL) :: lightng_tintalgo ! stream -> model time interpolation alogrithm character(len=CL) :: lightngmapalgo ! stream-> model mapping alogrithm integer :: rc diff --git a/src/cpl/share_esmf/PrigentRoughnessStreamType.F90 b/src/cpl/share_esmf/PrigentRoughnessStreamType.F90 index 2e78704614..afc65f2ece 100644 --- a/src/cpl/share_esmf/PrigentRoughnessStreamType.F90 +++ b/src/cpl/share_esmf/PrigentRoughnessStreamType.F90 @@ -11,7 +11,7 @@ module PrigentRoughnessStreamType use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl use shr_log_mod , only : errMsg => shr_log_errMsg use spmdMod , only : mpicom, masterproc - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use abortutils , only : endrun use decompMod , only : bounds_type @@ -36,8 +36,8 @@ module PrigentRoughnessStreamType ! ! PRIVATE DATA: type, private :: streamcontrol_type - character(len=CL) :: stream_fldFileName_prigentroughness ! data Filename - character(len=CL) :: stream_meshfile_prigentroughness ! mesh Filename + character(len=FL) :: stream_fldFileName_prigentroughness ! data Filename + character(len=FL) :: stream_meshfile_prigentroughness ! mesh Filename character(len=CL) :: prigentroughnessmapalgo ! map algo contains procedure, private :: ReadNML ! Read in control namelist @@ -280,8 +280,8 @@ subroutine ReadNML(this, bounds, NLFilename) integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag logical :: use_prigent_roughness = .true. - character(len=CL) :: stream_fldFileName_prigentroughness = ' ' - character(len=CL) :: stream_meshfile_prigentroughness = ' ' + character(len=FL) :: stream_fldFileName_prigentroughness = ' ' + character(len=FL) :: stream_meshfile_prigentroughness = ' ' character(len=CL) :: prigentroughnessmapalgo = 'bilinear' character(len=*), parameter :: namelist_name = 'prigentroughness' ! MUST agree with group name in namelist definition to read. character(len=*), parameter :: subName = "('prigentroughness::ReadNML')" diff --git a/src/cpl/share_esmf/SoilMoistureStreamMod.F90 b/src/cpl/share_esmf/SoilMoistureStreamMod.F90 index a93f413e7a..70a5758ffd 100644 --- a/src/cpl/share_esmf/SoilMoistureStreamMod.F90 +++ b/src/cpl/share_esmf/SoilMoistureStreamMod.F90 @@ -17,7 +17,7 @@ module SoilMoistureStreamMod use shr_mpi_mod , only : shr_mpi_bcast use decompMod , only : bounds_type, subgrid_level_column use abortutils , only : endrun - use clm_varctl , only : iulog, use_soil_moisture_streams + use clm_varctl , only : iulog, use_soil_moisture_streams, FL => fname_len use controlMod , only : NLFilename use LandunitType , only : lun use ColumnType , only : col @@ -78,7 +78,7 @@ subroutine PrescribedSoilMoistureInit(bounds) integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag integer :: soilm_offset ! Offset in time for dataset (sec) - character(len=CL) :: stream_fldfilename_soilm ! ustar stream filename to read + character(len=FL) :: stream_fldfilename_soilm ! ustar stream filename to read character(len=CL) :: soilm_tintalgo = 'linear' ! Time interpolation alogrithm character(len=CL) :: stream_mapalgo = 'bilinear' real(r8) :: stream_dtlimit = 15._r8 diff --git a/src/cpl/share_esmf/UrbanTimeVarType.F90 b/src/cpl/share_esmf/UrbanTimeVarType.F90 index 926a2c0557..eb537a7031 100644 --- a/src/cpl/share_esmf/UrbanTimeVarType.F90 +++ b/src/cpl/share_esmf/UrbanTimeVarType.F90 @@ -11,7 +11,7 @@ module UrbanTimeVarType use shr_log_mod , only : errMsg => shr_log_errMsg use abortutils , only : endrun use decompMod , only : bounds_type, subgrid_level_landunit - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use landunit_varcon , only : isturb_MIN, isturb_MAX use clm_varcon , only : spval use LandunitType , only : lun @@ -126,8 +126,8 @@ subroutine urbantv_init(this, bounds, NLFilename) integer :: model_year_align_urbantv ! align stream_year_first_urbantv with this model year integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag - character(len=CL) :: stream_fldFileName_urbantv ! urban tv streams filename - character(len=CL) :: stream_meshfile_urbantv ! urban tv streams filename + character(len=FL) :: stream_fldFileName_urbantv ! urban tv streams filename + character(len=FL) :: stream_meshfile_urbantv ! urban tv streams filename character(len=CL) :: urbantvmapalgo = 'nn' ! mapping alogrithm for urban ac character(len=CL) :: urbantv_tintalgo = 'linear' ! time interpolation alogrithm integer :: rc ! error code diff --git a/src/cpl/share_esmf/ZenderSoilErodStreamType.F90 b/src/cpl/share_esmf/ZenderSoilErodStreamType.F90 index 32e776063b..1465f44085 100644 --- a/src/cpl/share_esmf/ZenderSoilErodStreamType.F90 +++ b/src/cpl/share_esmf/ZenderSoilErodStreamType.F90 @@ -18,7 +18,7 @@ module ZenderSoilErodStreamType use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl use shr_log_mod , only : errMsg => shr_log_errMsg use spmdMod , only : mpicom, masterproc - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use abortutils , only : endrun use decompMod , only : bounds_type @@ -42,8 +42,8 @@ module ZenderSoilErodStreamType ! ! PRIVATE DATA: type, private :: streamcontrol_type - character(len=CL) :: stream_fldFileName_zendersoilerod ! data Filename - character(len=CL) :: stream_meshfile_zendersoilerod ! mesh Filename + character(len=FL) :: stream_fldFileName_zendersoilerod ! data Filename + character(len=FL) :: stream_meshfile_zendersoilerod ! mesh Filename character(len=CL) :: zendersoilerod_mapalgo ! map algo logical :: namelist_set = .false. ! if namelist was set yet contains @@ -300,10 +300,10 @@ subroutine ReadNML(this, bounds, NLFilename) integer :: i ! Indices integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag - character(len=CL) :: stream_fldFileName_zendersoilerod = ' ' - character(len=CL) :: stream_meshfile_zendersoilerod = ' ' + character(len=FL) :: stream_fldFileName_zendersoilerod = ' ' + character(len=FL) :: stream_meshfile_zendersoilerod = ' ' character(len=CL) :: zendersoilerod_mapalgo = ' ' - character(len=CL) :: tmp_file_array(3) + character(len=FL) :: tmp_file_array(3) character(len=*), parameter :: namelist_name = 'zendersoilerod' ! MUST agree with group name in namelist definition to read. character(len=*), parameter :: subName = "('zendersoilerod::ReadNML')" !----------------------------------------------------------------------- diff --git a/src/cpl/share_esmf/ch4FInundatedStreamType.F90 b/src/cpl/share_esmf/ch4FInundatedStreamType.F90 index 2bebf30062..eeda0f4a00 100644 --- a/src/cpl/share_esmf/ch4FInundatedStreamType.F90 +++ b/src/cpl/share_esmf/ch4FInundatedStreamType.F90 @@ -12,7 +12,7 @@ module ch4FInundatedStreamType use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl use shr_log_mod , only : errMsg => shr_log_errMsg use spmdMod , only : mpicom, masterproc - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use abortutils , only : endrun use decompMod , only : bounds_type use ch4varcon , only : finundation_mtd @@ -41,8 +41,8 @@ module ch4FInundatedStreamType ! ! PRIVATE DATA: type, private :: streamcontrol_type - character(len=CL) :: stream_fldFileName_ch4finundated ! data Filename - character(len=CL) :: stream_meshfile_ch4finundated ! mesh Filename + character(len=FL) :: stream_fldFileName_ch4finundated ! data Filename + character(len=FL) :: stream_meshfile_ch4finundated ! mesh Filename character(len=CL) :: ch4finundatedmapalgo ! map algo contains procedure, private :: ReadNML ! Read in namelist @@ -338,8 +338,8 @@ subroutine ReadNML(this, bounds, NLFilename) ! local variables integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag - character(len=CL) :: stream_fldFileName_ch4finundated = ' ' - character(len=CL) :: stream_meshfile_ch4finundated = ' ' + character(len=FL) :: stream_fldFileName_ch4finundated = ' ' + character(len=FL) :: stream_meshfile_ch4finundated = ' ' character(len=CL) :: ch4finundatedmapalgo = 'bilinear' character(len=*), parameter :: namelist_name = 'ch4finundated' ! MUST agree with name in namelist and read character(len=*), parameter :: subName = "('ch4finundated::ReadNML')" diff --git a/src/cpl/share_esmf/cropcalStreamMod.F90 b/src/cpl/share_esmf/cropcalStreamMod.F90 index 5587641c21..b19612ca09 100644 --- a/src/cpl/share_esmf/cropcalStreamMod.F90 +++ b/src/cpl/share_esmf/cropcalStreamMod.F90 @@ -14,6 +14,7 @@ module cropcalStreamMod use decompMod , only : bounds_type use abortutils , only : endrun use clm_varctl , only : iulog + use clm_varctl , only : FL => fname_len use clm_varctl , only : use_crop use clm_varctl , only : use_cropcal_rx_swindows, use_cropcal_rx_cultivar_gdds, use_cropcal_streams use clm_varctl , only : adapt_cropcal_rx_cultivar_gdds @@ -47,16 +48,16 @@ module cropcalStreamMod character(len=CS), allocatable :: stream_varnames_gdd20_season_enddate(:) ! start uses stream_varnames_sdate integer :: ncft ! Number of crop functional types (excl. generic crops) logical :: allow_invalid_swindow_inputs ! Fall back on paramfile sowing windows in cases of invalid values in stream_fldFileName_swindow_start and _end? - character(len=CL) :: stream_fldFileName_swindow_start ! sowing window start stream filename to read - character(len=CL) :: stream_fldFileName_swindow_end ! sowing window end stream filename to read - character(len=CL) :: stream_fldFileName_cultivar_gdds ! cultivar growing degree-days stream filename to read - character(len=CL) :: stream_fldFileName_gdd20_baseline ! GDD20 baseline stream filename to read + character(len=FL) :: stream_fldFileName_swindow_start ! sowing window start stream filename to read + character(len=FL) :: stream_fldFileName_swindow_end ! sowing window end stream filename to read + character(len=FL) :: stream_fldFileName_cultivar_gdds ! cultivar growing degree-days stream filename to read + character(len=FL) :: stream_fldFileName_gdd20_baseline ! GDD20 baseline stream filename to read logical :: cropcals_rx ! Used only for setting input files in namelist; does nothing in code, but needs to be here so namelist read doesn't crash logical :: cropcals_rx_adapt ! Used only for setting input files in namelist; does nothing in code, but needs to be here so namelist read doesn't crash logical :: stream_gdd20_seasons ! Read start and end dates for gdd20 seasons from streams instead of using hemisphere-specific values logical :: allow_invalid_gdd20_season_inputs ! Fall back on hemisphere "warm periods" in cases of invalid values in stream_fldFileName_gdd20_season_start and _end? - character(len=CL) :: stream_fldFileName_gdd20_season_start ! Stream filename to read for start of gdd20 season - character(len=CL) :: stream_fldFileName_gdd20_season_end ! Stream filename to read for end of gdd20 season + character(len=FL) :: stream_fldFileName_gdd20_season_start ! Stream filename to read for start of gdd20 season + character(len=FL) :: stream_fldFileName_gdd20_season_end ! Stream filename to read for end of gdd20 season character(len=*), parameter :: sourcefile = & __FILE__ @@ -89,7 +90,7 @@ subroutine cropcal_init(bounds) integer :: model_year_align_cropcal_cultivar_gdds ! alignment year for cultivar gdd stream integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag - character(len=CL) :: stream_meshfile_cropcal ! crop calendar stream meshfile + character(len=FL) :: stream_meshfile_cropcal ! crop calendar stream meshfile character(len=CL) :: cropcal_mapalgo = 'nn' ! Mapping alogrithm character(len=CL) :: cropcal_tintalgo = 'nearest' ! Time interpolation alogrithm integer :: cropcal_offset = 0 ! Offset in time for dataset (sec) diff --git a/src/cpl/share_esmf/laiStreamMod.F90 b/src/cpl/share_esmf/laiStreamMod.F90 index 64ee045390..0dc7ebb17f 100644 --- a/src/cpl/share_esmf/laiStreamMod.F90 +++ b/src/cpl/share_esmf/laiStreamMod.F90 @@ -12,7 +12,7 @@ module laiStreamMod use dshr_strdata_mod , only : shr_strdata_type use decompMod , only : bounds_type use abortutils , only : endrun - use clm_varctl , only : iulog + use clm_varctl , only : iulog, FL => fname_len use perf_mod , only : t_startf, t_stopf use spmdMod , only : masterproc, mpicom, iam ! @@ -60,8 +60,8 @@ subroutine lai_init(bounds) integer :: model_year_align_lai ! align stream_year_first_lai with integer :: nu_nml ! unit for namelist file integer :: nml_error ! namelist i/o error flag - character(len=CL) :: stream_fldFileName_lai ! lai stream filename to read - character(len=CL) :: stream_meshfile_lai ! lai stream meshfile + character(len=FL) :: stream_fldFileName_lai ! lai stream filename to read + character(len=FL) :: stream_meshfile_lai ! lai stream meshfile real(r8) :: lai_dtlimit = 1.5_r8 ! dlimit for lai stream to use character(len=CL) :: lai_mapalgo = 'bilinear' ! Mapping alogrithm character(len=CL) :: lai_tintalgo = 'linear' ! Time interpolation alogrithm diff --git a/src/cpl/share_esmf/lnd_set_decomp_and_domain.F90 b/src/cpl/share_esmf/lnd_set_decomp_and_domain.F90 index 592d5f441c..0b066ceb5b 100644 --- a/src/cpl/share_esmf/lnd_set_decomp_and_domain.F90 +++ b/src/cpl/share_esmf/lnd_set_decomp_and_domain.F90 @@ -18,7 +18,7 @@ module lnd_set_decomp_and_domain use shr_sys_mod , only : shr_sys_abort use shr_log_mod , only : errMsg => shr_log_errMsg use spmdMod , only : masterproc, mpicom - use clm_varctl , only : iulog, inst_suffix + use clm_varctl , only : iulog, inst_suffix, FL => fname_len use abortutils , only : endrun implicit none @@ -452,7 +452,7 @@ subroutine lnd_set_lndmask_from_maskmesh(mesh_lnd, mesh_mask, vm, gsize, lndmask real(r8) :: fmaxval = 1._r8 logical :: lexist logical :: checkflag = .false. - character(len=CL) :: flandfrac + character(len=FL) :: flandfrac character(len=CL) :: flandfrac_status !------------------------------------------------------------------------------- diff --git a/src/cpl/share_esmf/ndepStreamMod.F90 b/src/cpl/share_esmf/ndepStreamMod.F90 index b1c9d1a0e5..ff91d5a202 100644 --- a/src/cpl/share_esmf/ndepStreamMod.F90 +++ b/src/cpl/share_esmf/ndepStreamMod.F90 @@ -12,7 +12,7 @@ module ndepStreamMod use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl, CS => shr_kind_cs use spmdMod , only : mpicom, masterproc, iam use decompMod , only : bounds_type - use clm_varctl , only : iulog, inst_name + use clm_varctl , only : iulog, inst_name, FL => fname_len use abortutils , only : endrun ! !PUBLIC TYPES: @@ -66,8 +66,8 @@ subroutine ndep_init(bounds, NLFilename) character(len=CS) :: ndep_taxmode = 'extend' character(len=CL) :: ndep_varlist = 'NDEP_year' integer :: ndep_offset = 0 ! Offset in time for dataset (sec) - character(len=CL) :: stream_fldFileName_ndep - character(len=CL) :: stream_meshfile_ndep + character(len=FL) :: stream_fldFileName_ndep + character(len=FL) :: stream_meshfile_ndep integer :: stream_nflds integer :: rc character(*), parameter :: subName = "('ndepdyn_init')" diff --git a/src/dyn_subgrid/test/dynConsBiogeophys_test/CMakeLists.txt b/src/dyn_subgrid/test/dynConsBiogeophys_test/CMakeLists.txt index da9c27090c..8a1fd5eb76 100644 --- a/src/dyn_subgrid/test/dynConsBiogeophys_test/CMakeLists.txt +++ b/src/dyn_subgrid/test/dynConsBiogeophys_test/CMakeLists.txt @@ -3,4 +3,6 @@ set(pfunit_sources add_pfunit_ctest(dynConsBiogeophys TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/dyn_subgrid/test/dynInitColumns_test/CMakeLists.txt b/src/dyn_subgrid/test/dynInitColumns_test/CMakeLists.txt index 0adbd696ad..d6daa2a77c 100644 --- a/src/dyn_subgrid/test/dynInitColumns_test/CMakeLists.txt +++ b/src/dyn_subgrid/test/dynInitColumns_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(dynInitColumns TEST_SOURCES "test_init_columns.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/dyn_subgrid/test/dynTimeInfo_test/CMakeLists.txt b/src/dyn_subgrid/test/dynTimeInfo_test/CMakeLists.txt index 625ddae91b..c8cda773ab 100644 --- a/src/dyn_subgrid/test/dynTimeInfo_test/CMakeLists.txt +++ b/src/dyn_subgrid/test/dynTimeInfo_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(dynTimeInfo TEST_SOURCES "test_dynTimeInfo.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/dyn_subgrid/test/dynTimeInfo_test/test_dynTimeInfo.pf b/src/dyn_subgrid/test/dynTimeInfo_test/test_dynTimeInfo.pf index 3c6092381e..d04c68d89f 100644 --- a/src/dyn_subgrid/test/dynTimeInfo_test/test_dynTimeInfo.pf +++ b/src/dyn_subgrid/test/dynTimeInfo_test/test_dynTimeInfo.pf @@ -30,8 +30,9 @@ contains ! Make sure the date is set to the start of the year (such that the year differs ! between the start and end of the timestep), to make sure that the appropriate - ! year_position is being used. - call set_date(yr=1, mon=1, day=1, tod=0) + ! year_position is being used. (Need to use yr=2 instead of yr=1 because it's an error + ! to try to set yr,mon,day,tod = 1,1,1,0.) + call set_date(yr=2, mon=1, day=1, tod=0) end subroutine setUp subroutine tearDown(this) diff --git a/src/dyn_subgrid/test/dynVar_test/CMakeLists.txt b/src/dyn_subgrid/test/dynVar_test/CMakeLists.txt index 7164947f1e..4325c787cb 100644 --- a/src/dyn_subgrid/test/dynVar_test/CMakeLists.txt +++ b/src/dyn_subgrid/test/dynVar_test/CMakeLists.txt @@ -9,4 +9,6 @@ set (extra_sources add_pfunit_ctest(dynVar TEST_SOURCES "${pfunit_sources}" OTHER_SOURCES "${extra_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/main/clm_varctl.F90 b/src/main/clm_varctl.F90 index 9d4fcc645f..61ae7c4eb3 100644 --- a/src/main/clm_varctl.F90 +++ b/src/main/clm_varctl.F90 @@ -5,7 +5,7 @@ module clm_varctl ! Module containing run control variables ! ! !USES: - use shr_kind_mod, only: r8 => shr_kind_r8, SHR_KIND_CL + use shr_kind_mod, only: r8 => shr_kind_r8, SHR_KIND_CX use shr_sys_mod , only: shr_sys_abort ! cannot use endrun here due to circular dependency ! ! !PUBLIC MEMBER FUNCTIONS: @@ -21,7 +21,7 @@ module clm_varctl ! integer , parameter, public :: iundef = -9999999 real(r8), parameter, public :: rundef = -9999999._r8 - integer , parameter, public :: fname_len = SHR_KIND_CL ! max length of file names in this module + integer , parameter, public :: fname_len = SHR_KIND_CX ! max length of file names in this module !---------------------------------------------------------- ! ! Run control variables @@ -535,7 +535,7 @@ module clm_varctl !---------------------------------------------------------- ! To retrieve namelist !---------------------------------------------------------- - character(len=SHR_KIND_CL), public :: NLFilename_in ! Namelist filename + character(len=SHR_KIND_CX), public :: NLFilename_in ! Namelist filename ! logical, private :: clmvarctl_isset = .false. !----------------------------------------------------------------------- diff --git a/src/main/controlMod.F90 b/src/main/controlMod.F90 index f8f6dc6cf4..8c78194e0a 100644 --- a/src/main/controlMod.F90 +++ b/src/main/controlMod.F90 @@ -10,7 +10,7 @@ module controlMod ! Display the file in a browser to see it neatly formatted in html. ! ! !USES: - use shr_kind_mod , only: r8 => shr_kind_r8, SHR_KIND_CL + use shr_kind_mod , only: r8 => shr_kind_r8 use shr_nl_mod , only: shr_nl_find_group_name use shr_const_mod , only: SHR_CONST_CDAY use shr_log_mod , only: errMsg => shr_log_errMsg @@ -67,7 +67,7 @@ module controlMod ! ! !PRIVATE TYPES: character(len= 7) :: runtyp(4) ! run type - character(len=SHR_KIND_CL) :: NLFilename = 'lnd.stdin' ! Namelist filename + character(len=fname_len) :: NLFilename = 'lnd.stdin' ! Namelist filename #if (defined _OPENMP) integer, external :: omp_get_max_threads ! max number of threads that can execute concurrently in a single parallel region @@ -1273,7 +1273,7 @@ subroutine check_missing_initdata_status(finidat_interp_dest) ! !LOCAL VARIABLES: logical :: lexists integer :: klen - character(len=SHR_KIND_CL) :: status_file + character(len=fname_len) :: status_file character(len=*), parameter :: subname = 'check_missing_initdata_status' !----------------------------------------------------------------------- @@ -1314,7 +1314,7 @@ subroutine apply_use_init_interp(finidat_interp_dest, finidat, finidat_interp_so ! !LOCAL VARIABLES: logical :: lexists integer :: ncid - character(len=SHR_KIND_CL) :: initial_source_file + character(len=fname_len) :: initial_source_file integer :: status character(len=*), parameter :: subname = 'apply_use_init_interp' !----------------------------------------------------------------------- diff --git a/src/main/ncdio_pio.F90.in b/src/main/ncdio_pio.F90.in index 6d58ded872..86f3e0cb43 100644 --- a/src/main/ncdio_pio.F90.in +++ b/src/main/ncdio_pio.F90.in @@ -9,7 +9,7 @@ module ncdio_pio ! Generic interfaces to write fields to netcdf files for CLM ! ! !USES: - use shr_kind_mod , only : r8 => shr_kind_r8, i4=>shr_kind_i4, shr_kind_cl, r4 => shr_kind_r4 + use shr_kind_mod , only : r8 => shr_kind_r8, i4=>shr_kind_i4, r4 => shr_kind_r4 use shr_infnan_mod , only : nan => shr_infnan_nan, isnan => shr_infnan_isnan, assignment(=) use shr_sys_mod , only : shr_sys_abort use shr_file_mod , only : shr_file_getunit, shr_file_freeunit diff --git a/src/main/test/accumul_test/CMakeLists.txt b/src/main/test/accumul_test/CMakeLists.txt index 8d7cf69f1c..3c138e3258 100644 --- a/src/main/test/accumul_test/CMakeLists.txt +++ b/src/main/test/accumul_test/CMakeLists.txt @@ -3,4 +3,6 @@ set(pfunit_sources add_pfunit_ctest(accumul TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/main/test/atm2lnd_test/CMakeLists.txt b/src/main/test/atm2lnd_test/CMakeLists.txt index fcc4159ce2..1ac2e3a917 100644 --- a/src/main/test/atm2lnd_test/CMakeLists.txt +++ b/src/main/test/atm2lnd_test/CMakeLists.txt @@ -4,4 +4,6 @@ set(pfunit_sources add_pfunit_ctest(atm2lnd TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/main/test/initVertical_test/CMakeLists.txt b/src/main/test/initVertical_test/CMakeLists.txt index aec45772c4..bf3bf70c35 100644 --- a/src/main/test/initVertical_test/CMakeLists.txt +++ b/src/main/test/initVertical_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(initVertical TEST_SOURCES "test_initVertical.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/self_tests/test/assertions_test/CMakeLists.txt b/src/self_tests/test/assertions_test/CMakeLists.txt index aeae976839..133e8e5f01 100644 --- a/src/self_tests/test/assertions_test/CMakeLists.txt +++ b/src/self_tests/test/assertions_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(assertions TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/soilbiogeochem/test/tillage_test/CMakeLists.txt b/src/soilbiogeochem/test/tillage_test/CMakeLists.txt index 13be9aee3e..e04eec918b 100644 --- a/src/soilbiogeochem/test/tillage_test/CMakeLists.txt +++ b/src/soilbiogeochem/test/tillage_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(tillage TEST_SOURCES "test_tillage.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/unit_test_shr/CMakeLists.txt b/src/unit_test_shr/CMakeLists.txt index f6a204bd72..2ded8ebe40 100644 --- a/src/unit_test_shr/CMakeLists.txt +++ b/src/unit_test_shr/CMakeLists.txt @@ -12,6 +12,7 @@ list(APPEND clm_sources unittestDustEmisInputs.F90 unittestFilterBuilderMod.F90 unittestGlcMec.F90 + unittestInitializeAndFinalize.F90 unittestSimpleSubgridSetupsMod.F90 unittestSubgridMod.F90 unittestTimeManagerMod.F90 diff --git a/src/unit_test_shr/unittestInitializeAndFinalize.F90 b/src/unit_test_shr/unittestInitializeAndFinalize.F90 new file mode 100644 index 0000000000..4aaeb80572 --- /dev/null +++ b/src/unit_test_shr/unittestInitializeAndFinalize.F90 @@ -0,0 +1,55 @@ +module unittestInitializeAndFinalize + + ! Subroutines to do per-executable initialization and finalization (i.e., things that + ! need to happen once per executable, not once per test) + + implicit none + private + + public :: unittest_finalize_esmf ! finalization routine for any tests that might have initialized ESMF + +contains + + !----------------------------------------------------------------------- + subroutine unittest_finalize_esmf() + ! + ! !DESCRIPTION: + ! Finalization routine for any tests that might have initialized ESMF + ! + ! A good rule is: for any pFUnit test that links against esmf (i.e., includes esmf in + ! the LINK_LIBRARIES line in the add_pfunit_ctest call), this finalization routine + ! should be called. (It is safe to call this even if a test didn't initialize ESMF: + ! the finalization is done conditionally.) + ! + ! This can be called by adding the following to the add_pfunit_ctest call: + ! EXTRA_FINALIZE unittest_finalize_esmf + ! EXTRA_USE unittestInitializeAndFinalize + ! + ! !USES: + use ESMF, only : ESMF_SUCCESS, ESMF_Finalize, ESMF_IsInitialized + ! + ! !LOCAL VARIABLES: + logical :: esmf_is_initialized + integer :: rc + + character(len=*), parameter :: subname = 'unittest_finalize_esmf' + !----------------------------------------------------------------------- + + esmf_is_initialized = ESMF_IsInitialized(rc=rc) + if (rc /= ESMF_SUCCESS) then + ! Calling shr_sys_abort from this finalization routine leads to a pFUnit test + ! result of pass instead of fail. Doing a 'stop 1' leads to a failure, as desired. + print *, 'Error in ESMF_IsInitialized' + stop 1 + end if + if (esmf_is_initialized) then + print *, 'Finalizing ESMF' + call ESMF_Finalize(rc=rc) + if (rc /= ESMF_SUCCESS) then + print *, 'Error in ESMF_Finalize' + stop 1 + end if + end if + end subroutine unittest_finalize_esmf + +end module unittestInitializeAndFinalize \ No newline at end of file diff --git a/src/unit_test_shr/unittestSubgridMod.F90 b/src/unit_test_shr/unittestSubgridMod.F90 index 6a1f4d9daf..531f0b041e 100644 --- a/src/unit_test_shr/unittestSubgridMod.F90 +++ b/src/unit_test_shr/unittestSubgridMod.F90 @@ -457,8 +457,12 @@ function get_ltype_special() result(ltype) end do if (.not. found) then + ! We use a 'stop 1' here instead of shr_sys_abort because, in the context of pFUnit + ! testing, a shr_sys_abort will allow the code to continue (after raising a pFUnit + ! exception), which can lead to a cryptic error due to the return value from this + ! function being invalid, giving an array out of bounds error. print *, subname//' ERROR: cannot find a special landunit' - stop + stop 1 end if end function get_ltype_special diff --git a/src/unit_test_shr/unittestTimeManagerMod.F90 b/src/unit_test_shr/unittestTimeManagerMod.F90 index 8e1cdb3f36..e0d5fba42a 100644 --- a/src/unit_test_shr/unittestTimeManagerMod.F90 +++ b/src/unit_test_shr/unittestTimeManagerMod.F90 @@ -26,6 +26,8 @@ module unittestTimeManagerMod ! clm_time_manager. The routines in this unittest-specific file, in contrast, tend to be ! higher-level wrappers. + use shr_sys_mod, only : shr_sys_abort + implicit none private save @@ -48,7 +50,7 @@ subroutine unittest_timemgr_setup(dtime, use_gregorian_calendar) ! Should be called once for every test that uses the time manager. ! ! !USES: - use ESMF, only : ESMF_Initialize, ESMF_SUCCESS + use ESMF, only : ESMF_Initialize, ESMF_IsInitialized, ESMF_SUCCESS use clm_time_manager, only : set_timemgr_init, timemgr_init, NO_LEAP_C, GREGORIAN_C ! ! !ARGUMENTS: @@ -59,6 +61,7 @@ subroutine unittest_timemgr_setup(dtime, use_gregorian_calendar) integer :: l_dtime ! local version of dtime logical :: l_use_gregorian_calendar ! local version of use_gregorian_calendar character(len=:), allocatable :: calendar + logical :: esmf_is_initialized integer :: rc ! return code integer, parameter :: dtime_default = 1800 ! time step (seconds) @@ -68,12 +71,6 @@ subroutine unittest_timemgr_setup(dtime, use_gregorian_calendar) integer, parameter :: ref_ymd = start_ymd integer, parameter :: perpetual_ymd = start_ymd - ! Set current time to be at the start of year 1 - integer, parameter :: curr_yr = 1 - integer, parameter :: curr_mon = 1 - integer, parameter :: curr_day = 1 - integer, parameter :: curr_tod = 0 - character(len=*), parameter :: subname = 'unittest_timemgr_setup' !----------------------------------------------------------------------- @@ -89,9 +86,15 @@ subroutine unittest_timemgr_setup(dtime, use_gregorian_calendar) l_use_gregorian_calendar = .false. end if - call ESMF_Initialize(rc=rc) + esmf_is_initialized = ESMF_IsInitialized(rc=rc) if (rc /= ESMF_SUCCESS) then - stop 'Error in ESMF_Initialize' + call shr_sys_abort(subname//': Error in ESMF_IsInitialized') + end if + if (.not. esmf_is_initialized) then + call ESMF_Initialize(rc=rc) + if (rc /= ESMF_SUCCESS) then + call shr_sys_abort(subname//': Error in ESMF_Initialize') + end if end if if (l_use_gregorian_calendar) then @@ -112,12 +115,6 @@ subroutine unittest_timemgr_setup(dtime, use_gregorian_calendar) call timemgr_init() - call unittest_timemgr_set_curr_date( & - yr = curr_yr, & - mon = curr_mon, & - day = curr_day, & - tod = curr_tod) - end subroutine unittest_timemgr_setup !----------------------------------------------------------------------- @@ -127,6 +124,9 @@ subroutine unittest_timemgr_set_curr_date(yr, mon, day, tod) ! Set the current model date in the time manager. This is the time at the END of the ! time step. ! + ! Note that a side effect of this subroutine is that the time step count is + ! incremented by 1 (because of the method used by for_test_set_curr_date). + ! ! !USES: use clm_time_manager, only : for_test_set_curr_date ! @@ -219,10 +219,12 @@ subroutine unittest_timemgr_teardown call timemgr_reset() - call ESMF_Finalize(rc=rc) - if (rc /= ESMF_SUCCESS) then - stop 'Error in ESMF_Finalize' - end if + ! If this is the end of the executable, we should call + ! ESMF_Finalize. But the timemgr setup and teardown routines can + ! be called multiple times within a single unit test executable, + ! and it's an error to re-call ESMF_Initialize after calling + ! ESMF_Finalize. So for now we just won't attempt to do an + ! ESMF_Finalize. end subroutine unittest_timemgr_teardown diff --git a/src/unit_test_stubs/share_esmf/ExcessIceStreamType.F90 b/src/unit_test_stubs/share_esmf/ExcessIceStreamType.F90 index 60bac5ad28..ed9d75507e 100644 --- a/src/unit_test_stubs/share_esmf/ExcessIceStreamType.F90 +++ b/src/unit_test_stubs/share_esmf/ExcessIceStreamType.F90 @@ -5,7 +5,7 @@ module ExcessIceStreamType ! Stub module for Excess ice streams ! ! !USES - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use abortutils , only : endrun use decompMod , only : bounds_type diff --git a/src/unit_test_stubs/share_esmf/PrigentRoughnessStreamType.F90 b/src/unit_test_stubs/share_esmf/PrigentRoughnessStreamType.F90 index d9ded09c30..03b6708a82 100644 --- a/src/unit_test_stubs/share_esmf/PrigentRoughnessStreamType.F90 +++ b/src/unit_test_stubs/share_esmf/PrigentRoughnessStreamType.F90 @@ -10,7 +10,7 @@ module PrigentRoughnessStreamType ! https://github.com/ESCOMP/CTSM/issues/2381 ! Removing this version will remove testing code duplication. ! !USES - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use clm_varctl , only : iulog use abortutils , only : endrun diff --git a/src/unit_test_stubs/share_esmf/ZenderSoilErodStreamType.F90 b/src/unit_test_stubs/share_esmf/ZenderSoilErodStreamType.F90 index 570fcec05f..09a31904b1 100644 --- a/src/unit_test_stubs/share_esmf/ZenderSoilErodStreamType.F90 +++ b/src/unit_test_stubs/share_esmf/ZenderSoilErodStreamType.F90 @@ -5,7 +5,7 @@ module ZenderSoilErodStreamType ! UNIT-TEST STUB for ZenderSoilErodStreamType ! ! !USES - use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_cl + use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use abortutils , only : endrun use decompMod , only : bounds_type diff --git a/src/utils/clm_time_manager.F90 b/src/utils/clm_time_manager.F90 index f606c2832b..c5a1fa759b 100644 --- a/src/utils/clm_time_manager.F90 +++ b/src/utils/clm_time_manager.F90 @@ -1973,6 +1973,9 @@ subroutine timemgr_reset() ! All unit tests that modify the time manager should call this routine in their ! teardown section. ! + ! It is safe to call this subroutine even if the time manager hasn't been initialized. + ! (In this case, this reset routine won't do anything.) + ! ! Note: we could probably get away with doing much less resetting than is currently ! done here. For example, we could simply set timemgr_set = .false., and deallocate ! anything that needs deallocation. That would provide the benefit of less @@ -2001,6 +2004,13 @@ subroutine timemgr_reset() ! could simply set to time manager instance to a new instance of the derived type. ! ------------------------------------------------------------------------ + if (.not. timemgr_set) then + ! If the time manager hasn't been initialized, then we don't need to do anything. + ! This logic makes it safe to call this reset routine even in cases where the time + ! manager hasn't been initialized. + return + end if + calendar = NO_LEAP_C dtime = uninit_int @@ -2057,10 +2067,21 @@ subroutine for_test_set_curr_date(yr, mon, day, tod) ! !DESCRIPTION: ! Sets the current date - i.e., the date at the end of the time step ! + ! This is done in a way that mimics what would happen if the clock advanced from the + ! previous time step to the specified time (so, in particular, sets the previous time + ! as if that's what happened). + ! + ! Note that, because of the method used to do this setting, it is an error to try to + ! call this with the earliest possible time (yr,mon,day,tod = 1,1,1,0): instead, it + ! needs to be called with a time at least one time step later. + ! + ! Also note that an unavoidable side-effect of this method is that the time step count + ! is incremented by 1. + ! ! *** Should only be used in unit tests!!! *** ! ! !USES: - use ESMF , only : ESMF_ClockSet + use ESMF , only : ESMF_ClockSet, ESMF_ClockAdvance ! ! !ARGUMENTS: integer, intent(in) :: yr ! year @@ -2069,19 +2090,46 @@ subroutine for_test_set_curr_date(yr, mon, day, tod) integer, intent(in) :: tod ! time of day (seconds past 0Z) ! ! !LOCAL VARIABLES: - type(ESMF_Time) :: my_time ! ESMF Time corresponding to the inputs + type(ESMF_Time) :: input_time ! ESMF Time corresponding to the inputs + type(ESMF_TimeInterval) :: interval_dtime + type(ESMF_Time) :: input_time_minus_dtime integer :: rc ! return code character(len=*), parameter :: sub = 'for_test_set_curr_date' !----------------------------------------------------------------------- - call ESMF_TimeSet(my_time, yy=yr, mm=mon, dd=day, s=tod, & + ! Rather than simply setting the clock to the specified date, we instead set it to one + ! time step before the specified date, then advance the clock by a time step. This is + ! needed so that the clock's previous time is set as if we reached the specified time + ! through a typical one-time-step advance of the clock, rather than via an arbitrary + ! jump. + + ! Because of this method of setting the clock, though, it is an error to call this + ! with yr,mon,day,tod = 1,1,1,0; catch that common error here and give a meaningful + ! error message. + if (yr == 1 .and. mon == 1 .and. day == 1 .and. tod == 0) then + call shr_sys_abort(sub//': need to use a time later than yr,mon,day,tod = 1,1,1,0') + end if + + call ESMF_TimeSet(input_time, yy=yr, mm=mon, dd=day, s=tod, & calendar=tm_cal, rc=rc) call chkrc(rc, sub//': error return from ESMF_TimeSet') + + call ESMF_TimeIntervalSet(interval_dtime, s=dtime, rc=rc) + call chkrc(rc, sub//': error return from ESMF_TimeIntervalSet') + + input_time_minus_dtime = input_time - interval_dtime - call ESMF_ClockSet(tm_clock, CurrTime=my_time, rc=rc) + call ESMF_ClockSet(tm_clock, CurrTime=input_time_minus_dtime, rc=rc) call chkrc(rc, sub//': error return from ESMF_ClockSet') + ! Note that this ClockAdvance call increments the time step count; this seems like an + ! unavoidable side effect of this technique of starting with an earlier time and + ! advancing the clock (which we do in order to get the clock's previous time set + ! correctly). + call ESMF_ClockAdvance( tm_clock, rc=rc ) + call chkrc(rc, sub//': error return from ESMF_ClockAdvance') + end subroutine for_test_set_curr_date diff --git a/src/utils/clmfates_paraminterfaceMod.F90 b/src/utils/clmfates_paraminterfaceMod.F90 index ea27f563bf..0366c0610f 100644 --- a/src/utils/clmfates_paraminterfaceMod.F90 +++ b/src/utils/clmfates_paraminterfaceMod.F90 @@ -2,7 +2,7 @@ module CLMFatesParamInterfaceMod ! NOTE(bja, 2017-01) this code can not go into the main clm-fates ! interface module because of circular dependancies with pftvarcon. - use shr_kind_mod, only : r8 => shr_kind_r8, SHR_KIND_CL + use shr_kind_mod, only : r8 => shr_kind_r8 use FatesGlobals, only : fates_log use FatesParametersInterface, only : fates_parameters_type use FatesParametersInterface, only : fates_param_reader_type diff --git a/src/utils/test/annual_flux_dribbler_test/CMakeLists.txt b/src/utils/test/annual_flux_dribbler_test/CMakeLists.txt index 01efdc1e50..45ca43ff1c 100644 --- a/src/utils/test/annual_flux_dribbler_test/CMakeLists.txt +++ b/src/utils/test/annual_flux_dribbler_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(annual_flux_dribbler TEST_SOURCES "test_annual_flux_dribbler.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/utils/test/array_utils_test/CMakeLists.txt b/src/utils/test/array_utils_test/CMakeLists.txt index a6b36304b4..e9441079b1 100644 --- a/src/utils/test/array_utils_test/CMakeLists.txt +++ b/src/utils/test/array_utils_test/CMakeLists.txt @@ -5,4 +5,6 @@ set (pfunit_sources add_pfunit_ctest(array_utils TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/utils/test/clm_time_manager_test/CMakeLists.txt b/src/utils/test/clm_time_manager_test/CMakeLists.txt index 66a62e665d..38ea3c6cee 100644 --- a/src/utils/test/clm_time_manager_test/CMakeLists.txt +++ b/src/utils/test/clm_time_manager_test/CMakeLists.txt @@ -1,3 +1,5 @@ add_pfunit_ctest(clm_time_manager TEST_SOURCES "test_clm_time_manager.pf" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/src/utils/test/clm_time_manager_test/test_clm_time_manager.pf b/src/utils/test/clm_time_manager_test/test_clm_time_manager.pf index d2f984aa5b..00e096c8e5 100644 --- a/src/utils/test/clm_time_manager_test/test_clm_time_manager.pf +++ b/src/utils/test/clm_time_manager_test/test_clm_time_manager.pf @@ -452,7 +452,7 @@ contains londeg = londeg + 0.1_r8 ! Start at 0 Z secs = 0 - call set_date(yr=1, mon=1, day=1, tod=secs) + call set_date(yr=2, mon=1, day=1, tod=secs) do while ( .not. is_end_curr_year() ) @assertEqual( get_local_time( londeg, starttime=0 ), get_local_timestep_time( londeg ) ) call advance_timestep() diff --git a/src/utils/test/numerics_test/CMakeLists.txt b/src/utils/test/numerics_test/CMakeLists.txt index 2e826e2018..ebc85de714 100644 --- a/src/utils/test/numerics_test/CMakeLists.txt +++ b/src/utils/test/numerics_test/CMakeLists.txt @@ -3,4 +3,6 @@ set (pfunit_sources add_pfunit_ctest(numerics TEST_SOURCES "${pfunit_sources}" - LINK_LIBRARIES clm csm_share esmf) + LINK_LIBRARIES clm csm_share esmf + EXTRA_FINALIZE unittest_finalize_esmf + EXTRA_USE unittestInitializeAndFinalize) diff --git a/tools/mksurfdata_esmf/modify_1x1_mexicocityMEX.cfg b/tools/mksurfdata_esmf/modify_1x1_mexicocityMEX.cfg index 191bb1fedb..52fd7182ca 100644 --- a/tools/mksurfdata_esmf/modify_1x1_mexicocityMEX.cfg +++ b/tools/mksurfdata_esmf/modify_1x1_mexicocityMEX.cfg @@ -32,6 +32,8 @@ lnd_lat_2 = 90 lnd_lon_1 = 0 # easternmost longitude for rectangle lnd_lon_2 = 360 +# Upper limit of longitudes, from format being either [-180, 180] or [0, 360] +lon_type = 360 # user-defined mask in a file, as alternative to setting lat/lon values landmask_file = UNSET diff --git a/tools/mksurfdata_esmf/modify_1x1_vancouverCAN.cfg b/tools/mksurfdata_esmf/modify_1x1_vancouverCAN.cfg index bf3ebe5ee0..937ed16a9a 100644 --- a/tools/mksurfdata_esmf/modify_1x1_vancouverCAN.cfg +++ b/tools/mksurfdata_esmf/modify_1x1_vancouverCAN.cfg @@ -32,6 +32,8 @@ lnd_lat_2 = 90 lnd_lon_1 = 0 # easternmost longitude for rectangle lnd_lon_2 = 360 +# Upper limit of longitudes, from format being either [-180, 180] or [0, 360] +lon_type = 360 # user-defined mask in a file, as alternative to setting lat/lon values landmask_file = UNSET diff --git a/tools/modify_input_files/modify_fsurdat_template.cfg b/tools/modify_input_files/modify_fsurdat_template.cfg index 3d18189d51..afe4a174b6 100644 --- a/tools/modify_input_files/modify_fsurdat_template.cfg +++ b/tools/modify_input_files/modify_fsurdat_template.cfg @@ -59,6 +59,8 @@ lnd_lat_2 = 90 lnd_lon_1 = 0 # easternmost longitude for rectangle lnd_lon_2 = 360 +# Upper limit of longitudes, from format being either [-180, 180] or [0, 360] +lon_type = 360 # User-defined mask in a file, as alternative to setting lat/lon values. # If set, lat_dimname and lon_dimname should likely also be set. IMPORTANT: # - lat_dimname and lon_dimname may be left UNSET if they match the expected diff --git a/tools/modify_input_files/modify_mesh_template.cfg b/tools/modify_input_files/modify_mesh_template.cfg index 6773964843..113ea02e40 100644 --- a/tools/modify_input_files/modify_mesh_template.cfg +++ b/tools/modify_input_files/modify_mesh_template.cfg @@ -30,3 +30,4 @@ lat_dimname = FILL_THIS_IN lon_dimname = FILL_THIS_IN lat_varname = FILL_THIS_IN lon_varname = FILL_THIS_IN +lon_type = FILL_THIS_IN