diff --git a/.github/actions/run_tests/entrypoint.sh b/.github/actions/run_tests/entrypoint.sh index 53692ab10..7e0a4b72d 100644 --- a/.github/actions/run_tests/entrypoint.sh +++ b/.github/actions/run_tests/entrypoint.sh @@ -8,6 +8,8 @@ WS_PATH=$RUNNER_WORKSPACE/$REPO_NAME # set CI jobs directory variable to easily move it CI_JOBS_DIR=.github/jobs +PYTESTS_GROUPS_FILEPATH=.github/parm/pytest_groups.txt + source ${GITHUB_WORKSPACE}/${CI_JOBS_DIR}/bash_functions.sh # get branch name for push or pull request events @@ -30,10 +32,8 @@ if [ $? != 0 ]; then ${GITHUB_WORKSPACE}/${CI_JOBS_DIR}/docker_setup.sh fi -# # running unit tests (pytests) -# -if [ "$INPUT_CATEGORIES" == "pytests" ]; then +if [[ "$INPUT_CATEGORIES" == pytests* ]]; then export METPLUS_ENV_TAG="pytest" export METPLUS_IMG_TAG=${branch_name} echo METPLUS_ENV_TAG=${METPLUS_ENV_TAG} @@ -56,14 +56,20 @@ if [ "$INPUT_CATEGORIES" == "pytests" ]; then . echo Running Pytests - command="export METPLUS_PYTEST_HOST=docker; cd internal_tests/pytests; /usr/local/envs/pytest/bin/pytest -vv --cov=../../metplus" + command="export METPLUS_PYTEST_HOST=docker; cd internal_tests/pytests;" + command+="status=0;" + for x in `cat $PYTESTS_GROUPS_FILEPATH`; do + marker="${x//_or_/ or }" + marker="${marker//not_/not }" + command+="/usr/local/envs/pytest/bin/pytest -vv --cov=../../metplus -m \"$marker\"" + command+=";if [ \$? != 0 ]; then status=1; fi;" + done + command+="if [ \$status != 0 ]; then echo ERROR: Some pytests failed. Search for FAILED to review; false; fi" time_command docker run -v $WS_PATH:$GITHUB_WORKSPACE --workdir $GITHUB_WORKSPACE $RUN_TAG bash -c "$command" exit $? fi -# # running use case tests -# # split apart use case category and subset list from input CATEGORIES=`echo $INPUT_CATEGORIES | awk -F: '{print $1}'` diff --git a/.github/jobs/get_use_cases_to_run.sh b/.github/jobs/get_use_cases_to_run.sh index 39c250474..341d1c480 100755 --- a/.github/jobs/get_use_cases_to_run.sh +++ b/.github/jobs/get_use_cases_to_run.sh @@ -1,6 +1,7 @@ #! /bin/bash use_case_groups_filepath=.github/parm/use_case_groups.json + # set matrix to string of an empty array in case no use cases will be run matrix="[]" @@ -31,12 +32,14 @@ fi if [ "$run_unit_tests" == "true" ]; then echo Adding unit tests to list to run + pytests="\"pytests\"," + # if matrix is empty, set to an array that only includes pytests if [ "$matrix" == "[]" ]; then - matrix="[\"pytests\"]" + matrix="[${pytests:0: -1}]" # otherwise prepend item to list else - matrix="[\"pytests\", ${matrix:1}" + matrix="[${pytests}${matrix:1}" fi fi diff --git a/.github/parm/pytest_groups.txt b/.github/parm/pytest_groups.txt new file mode 100644 index 000000000..374b99da8 --- /dev/null +++ b/.github/parm/pytest_groups.txt @@ -0,0 +1,6 @@ +util +wrapper +wrapper_a +wrapper_b +wrapper_c +plotting_or_long diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index d7f55c334..309205c39 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -174,6 +174,16 @@ "index_list": "12", "run": false }, + { + "category": "s2s", + "index_list": "13", + "run": false + }, + { + "category": "s2s", + "index_list": "14", + "run": false + }, { "category": "space_weather", "index_list": "0-1", diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1b4790987..deff878b8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -139,24 +139,24 @@ jobs: # copy logs with errors to error_logs directory to save as artifact - name: Save error logs id: save-errors - if: ${{ always() && steps.run_tests.conclusion == 'failure' && matrix.categories != 'pytests' }} + if: ${{ always() && steps.run_tests.conclusion == 'failure' && !startsWith(matrix.categories,'pytests') }} run: .github/jobs/save_error_logs.sh # run difference testing - name: Run difference tests id: run-diff - if: ${{ needs.job_control.outputs.run_diff == 'true' && steps.run_tests.conclusion == 'success' && matrix.categories != 'pytests' }} + if: ${{ needs.job_control.outputs.run_diff == 'true' && steps.run_tests.conclusion == 'success' && !startsWith(matrix.categories,'pytests') }} run: .github/jobs/run_difference_tests.sh ${{ matrix.categories }} ${{ steps.get-artifact-name.outputs.artifact_name }} # copy output data to save as artifact - name: Save output data id: save-output - if: ${{ always() && steps.run_tests.conclusion != 'skipped' && matrix.categories != 'pytests' }} + if: ${{ always() && steps.run_tests.conclusion != 'skipped' && !startsWith(matrix.categories,'pytests') }} run: .github/jobs/copy_output_to_artifact.sh ${{ steps.get-artifact-name.outputs.artifact_name }} - name: Upload output data artifact uses: actions/upload-artifact@v2 - if: ${{ always() && steps.run_tests.conclusion != 'skipped' && matrix.categories != 'pytests' }} + if: ${{ always() && steps.run_tests.conclusion != 'skipped' && !startsWith(matrix.categories,'pytests') }} with: name: ${{ steps.get-artifact-name.outputs.artifact_name }} path: artifact/${{ steps.get-artifact-name.outputs.artifact_name }} diff --git a/docs/Contributors_Guide/add_use_case.rst b/docs/Contributors_Guide/add_use_case.rst index 58f8f7a8e..c2a47054f 100644 --- a/docs/Contributors_Guide/add_use_case.rst +++ b/docs/Contributors_Guide/add_use_case.rst @@ -33,11 +33,11 @@ Use Case Categories =================== New MET tool wrapper use cases will be put in the repository under -parm/use_cases/met_tool_wrapper/ where +*parm/use_cases/met_tool_wrapper/* where ** is the name of the MET tool being wrapped. New model applications use cases will be put in the repository under -parm/use_cases/model_applications/ where is +*parm/use_cases/model_applications/* where ** is one of the following: * air_quality_and_comp @@ -53,11 +53,11 @@ one of the following: * precipitation * s2s (Subseasonal to Seasonal) * space_weather -* tc_and_extra_tc (Tropcial Cyclone and Extra Tropical Cyclone) +* tc_and_extra_tc (Tropical Cyclone and Extratropical Cyclone) -If you feel that the new use case does not fall into any of these categories -or are unsure which category is the most appropriate, please create a post in -the +If the new use case does not fall into any of these categories +or it is unclear which category is the most appropriate, +please create a post in the `METplus GitHub Discussions Forum `_. Use Case Content @@ -67,36 +67,37 @@ Configure New Use Case ---------------------- If creating a new MET tool wrapper use case, in the MET tool name -sub-directory (parm/use_cases/met_tool_wrapper/), each +sub-directory (*parm/use_cases/met_tool_wrapper/*), each use case should have the following: * A METplus configuration file where the MET tool name follows PascalCase, - e.g. GridStat.conf or ASCII2NC.conf. + e.g. **GridStat.conf** or **ASCII2NC.conf**. If the use case uses a Python embedding script, it should be indicated by adding "_python_embedding" to the MET tool name. - e.g. GridStat_python_embedding.conf + e.g. **GridStat_python_embedding.conf**. If creating a new model applications use case, in the category sub-directory -(parm/use_cases/model_applications/), each use case should have the +(*parm/use_cases/model_applications/*), each use case should have the following: * A METplus configuration file named - \_fcst\_obs\_cilmo\\.conf where + *\_fcst\_obs\_cilmo\\.conf* + where - * **** is the MET tool that performs the primary statistical - analysis, i.e. GridStat or SeriesAnalysis + * ** is the MET tool that performs the primary statistical + analysis, i.e. GridStat or SeriesAnalysis. - * **** is the name of the forecast input data source (this can be - excluded if no forecast data is used) + * ** is the name of the forecast input data source (this can be + excluded if no forecast data is used). - * **** is the name of the observation input data source (this can be - excluded if no observation data is used) + * ** is the name of the observation input data source (this can be + excluded if no observation data is used). - * **** is the optional climotology input data source (this can be - excluded if no climatology data is used) + * ** is the optional climatology input data source (this can be + excluded if no climatology data is used). - * **** is an optional description that can include field - category, number of fields, statistical types, and file formats + * ** is an optional description that can include field + category, number of fields, statistical types, and file formats. If the use case uses a Python Embedding script or any other additional files (besides input data), then put them in a sub-directory that matches the METplus @@ -111,7 +112,7 @@ Use Case Rules - The name of the use case files should conform to the guidelines listed above in Use Case Content. -- The use case METplus configuration file should not **set** any variables that +- The use case METplus configuration file should not **set** any variables specific to the user's environment, such as INPUT_BASE, OUTPUT_BASE, and PARM_BASE, METPLUS_CONF, etc. - A limited number of run times should be processed so that they use case runs @@ -123,8 +124,8 @@ Use Case Rules - All data that is input to the use case (not generated by METplus) should be referenced relative to {INPUT_BASE} and the directory structure of the use case. For example, if adding a new model application use case found under - model_applications/precipitation, the input directory should be relative to - {INPUT_BASE}/model_applications/precipitation:: + *model_applications/precipitation*, the input directory should be relative to + *{INPUT_BASE}/model_applications/precipitation*:: FCST_GRID_STAT_INPUT_DIR = {INPUT_BASE}/model_applications/precipitation @@ -138,8 +139,8 @@ Use Case Rules - The Sphinx documentation file should be as complete as possible, listing as much relevant information about the use case as possible. Keyword tags should be used so that users can locate other use cases that exhibit common - functionality/data sources/tools/etc. If a new keyword is used, it should be - added to the Quick Search Guide (docs/Users_Guide/quicksearch.rst). More + *functionality/data sources/tools/etc*. If a new keyword is used, it should + be added to the Quick Search Guide (*docs/Users_Guide/quicksearch.rst*). More information can be found :ref:`here `. - The use case should be run by someone other than the author to ensure that it runs smoothly outside of the development environment set up by the author. @@ -149,12 +150,12 @@ Use Case Rules Use Cases That Exceed Github Actions Memory Limit ------------------------------------------------- -Below is a list of use cases in the repository that cannot be run in Github Actions -due to their excessive memory usage. They have been tested and cleared by reviewers -of any other issues and can be used by METplus users in the same manner as all -other use cases. +Below is a list of use cases in the repository that cannot be run in Github +Actions due to their excessive memory usage. They have been tested and +cleared by reviewers of any other issues and can be used by METplus users in +the same manner as all other use cases. -- model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst +- *model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst* .. _use_case_documentation: @@ -164,20 +165,20 @@ Document New Use Case Create a New Model Applications Docs Directory ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -**If the use case falls under an existing Model Applications category, you can +**If the use case falls under an existing Model Applications category, skip this section.** If the use case is the first in a new Model Applications category, create the -directory under **docs**/use_cases/model_applications if it does not already -exist. Inside this directory, create a file called README.rst. Inside this file -add the following each on a single line: +directory under **docs**/*use_cases/model_applications* if it does not already +exist. Inside this directory, create a file called **README.rst**. +Inside this file add the following each on a single line: * Title of category -* Dashes (-) that are the exact same lengh as the title +* Dashes (-) that are the exact same length as the title * A short description of the category For example, -docs/use_cases/model_applications/**air_quality_and_comp/README.rst** +*docs/use_cases/model_applications*/**air_quality_and_comp/README.rst** would look something like this:: Air Quality and Composition @@ -193,9 +194,9 @@ Add Sphinx Documentation File ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In the corresponding documentation MET tool name directory -(**docs**/use_cases/met_tool_wrapper/) for a met_tool_wrappers +(**docs**/*use_cases/met_tool_wrapper/*) for a met_tool_wrappers use case OR category directory for a model_applications use case -(**docs**/use_cases/model_applications/), add: +(**docs**/*use_cases/model_applications/*), add: * A Python Sphinx Documentation (.py) file with the same name as the METplus configuration file @@ -230,10 +231,11 @@ use case OR category directory for a model_applications use case https://metplus.readthedocs.io/en/latest/search.html?q=**ASCII2NCToolUseCase**. * Add an image to use as the thumbnail (if desired). Images can be added - to the docs/_static directory and should be named -.png + to the *docs/_static* directory and should be named + -.png where is the use case category and is the name of the configuration file, i.e. - air_quality_and_comp-EnsembleStat_fcstICAP_obsMODIS_aod.png. + **air_quality_and_comp-EnsembleStat_fcstICAP_obsMODIS_aod.png.** The image can be referenced in the documentation file with this syntax: :: @@ -248,12 +250,12 @@ use case OR category directory for a model_applications use case Accessing the Documentation --------------------------- -It is important to ensure that the new use case files is displayed and the +It is important to ensure that the new use case files are displayed and the formatting looks correct. Prior to the release of METplus v4.0.0 contributors were required to build the documentation manually. However, the METplus components now use Read the Docs to build and display the documentation. For more information on how to view the newly added use case, see the -:ref:`Read the Docs METplus Documenation `. Contributors can +:ref:`Read the Docs METplus Documentation `. Contributors can still build the documentation manually if desired. See the :ref:`Build the Documentation Manually ` section below for more information. @@ -278,10 +280,10 @@ or conda activate /home/met_test/.conda/envs/sphinx_env .. note:: - If conda is not already in your path, you will have to find it and run it - from the full path. + If conda is not already in PATH, find it and run it + with the full path. -or you can create your own conda environment and install the packages:: +Or create a conda environment and install the packages:: conda create --name sphinx_env python=3.6 conda activate sphinx_env @@ -290,14 +292,16 @@ or you can create your own conda environment and install the packages:: pip install git+https://github.com/ESMCI/sphinx_rtd_theme@version-dropdown-with-fixes .. note:: - The specific version of sphinx_rtd_theme is needed to build the documentation - with the version selector. If you are building the docs locally, you don't - necessarily need this version. If it is easier, you can run 'conda install - sphinx_rtd_theme' instead of the pip from git command to install the package + The specific version of sphinx_rtd_theme is needed to build the + documentation with the version selector. + If the docs are being built locally, this version is not + necessarily needed. If it is easier, run 'conda install + sphinx_rtd_theme' instead of the pip from git command + to install the package. -To build the docs, run the build_docs.py script from the docs directory. Make -sure your conda environment is activated or the required packages are available -in your Python 3 environment:: +To build the docs, run the **build_docs.py** script from the docs directory. +Make sure the conda environment is activated or the required packages +are available in the Python3 environment:: cd ~/METplus/docs ./build_docs.py @@ -308,9 +312,9 @@ Input Data ========== Sample input data needed to run the use case should be provided. Please try to -limit your input data to the minimum that is -needed to demonstrate your use case effectively. GRIB2 files can be pared down -to only contain the fields and/or vertical levels that are needed using +limit the input data to the minimum that is +needed to demonstrate the use case effectively. GRIB2 files can be pared down +to only contain the fields and/or vertical levels that are needed for using `wgrib2 `_. Example: To create a file called subset.grib2 that only contains TMP data from @@ -325,19 +329,20 @@ the file(s). Providing new data ------------------ -Log into the computer where your input data resides -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Log into the computer where the input data resides +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Switch to Bash ^^^^^^^^^^^^^^ -If you are using a shell other than bash, run "bash" to activate a bash -shell. This will make the instructions you need to run on the DTC web server -as the met_test user easier because met_test's default shell is bash:: +Run "bash" to activate a bash shell. This step isn't necessary if bash +is already the default shell. The met_test user's default shell is bash. +The instructions needed to run +on the DTC web server will run smoothly in bash: bash -If you are unsure which shell you use, run the following command:: +Run the following command to see which shell is currently in use:: echo $SHELL @@ -346,15 +351,15 @@ If you are unsure which shell you use, run the following command:: running these instructions easier. Make sure they are set to the correct values that correspond to the use case being added before copy/pasting any of these commands or there may be unintended consequences. - Copy and paste these values after you have modified them into a text file - that you can copy and paste into the terminal. + Copy and paste these values after they have been modified into a text file + that can be copied and pasted into the terminal. Download the template environment file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This file is available on the DTC web server. You can use wget to download the -file to your current working directory, or visit the URL in a browser and save -it to your computer:: +This file is available on the DTC web server. Use 'wget' to download the +file to the current working directory, or visit the URL in a browser and save +it on the computer:: wget https://dtcenter.ucar.edu/dfiles/code/METplus/METplus_Data/add_use_case_env.bash @@ -363,7 +368,7 @@ Or click this `link / where - is the value you set for ${METPLUS_USE_CASE_CATEGORY} and - is the value you set for ${METPLUS_USE_CASE_NAME}. For a new -met_tool_wrapper use case, use {INPUT_BASE}/met_test/new. -You can set {INPUT_BASE} to your local directory to test that the use case +i.e *{INPUT_BASE}/model_applications//* where + is the value that has been set for ${METPLUS_USE_CASE_CATEGORY} and + is the value that has been set for ${METPLUS_USE_CASE_NAME}. +For a new met_tool_wrapper use case, use *{INPUT_BASE}/met_test/new*. +Set {INPUT_BASE} to the local directory to test that the use case still runs properly. Create new data tarfile ^^^^^^^^^^^^^^^^^^^^^^^ -Create a tarfile on your development machine with the new dataset. Make sure +Create a tarfile on the development machine with the new dataset. Make sure the tarfile contains directories, i.e. -model_applications/${METPLUS_USE_CASE_CATEGORY}:: +*model_applications/${METPLUS_USE_CASE_CATEGORY}*:: tar czf ${METPLUS_NEW_DATA_TARFILE} model_applications/${METPLUS_USE_CASE_CATEGORY}/${METPLUS_USE_CASE_NAME} @@ -455,7 +460,7 @@ Verify that the correct directory structure is found inside the tarfile:: tar tzf ${METPLUS_NEW_DATA_TARFILE} The output should show that all of the data is found under the -model_applications// directory. For example:: +*model_applications//* directory. For example:: model_applications/marine_and_cryosphere/ model_applications/marine_and_cryosphere/PlotDataPlane_obsHYCOM_coordTripolar/ @@ -474,7 +479,8 @@ the environment file to the staging directory: scp ${METPLUS_NEW_DATA_TARFILE} |dtc_web_server|:|metplus_staging_dir|/ scp ${METPLUS_USER_ENV_FILE} |dtc_web_server|:|metplus_staging_dir|/ -If you do not, upload the files to the RAL FTP:: +If you do not have access to the internal DTC web server, +upload the files to the RAL FTP server:: ftp -p ftp.rap.ucar.edu @@ -485,19 +491,20 @@ For an example on how to upload data to the ftp site see Adding new data to full sample data tarfile ------------------------------------------- -If you are unable to access the DTC web server to upload data or if you do -not have permission to use the met_test shared user account, someone from the +If you are unable to access the DTC web server to upload data or if +permission has not been granted to use the met_test shared user +account, someone from the METplus development team will have to complete the instructions in this -section. Please let one of the team members know if this is the case. +section. Please let one of the team members know if this is necessary. Comment on the GitHub issue associated with this use case and/or email the team -member(s) you have been coordinating with for this work. If you are unsure who -to contact, then please create a post in the +member(s) that have been coordinating with this work. If it is unclear who to +contact, please create a post in the `METplus GitHub Discussions Forum `_. Log into the DTC Web Server with SSH ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The web server is only accessible if you are on the NCAR VPN. +The web server is only accessible on the NCAR VPN. .. parsed-literal:: @@ -517,7 +524,7 @@ Setup the environment to run commands on web server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Change directory to the data staging dir, -source the environment file you created, and make sure the environment +source the environment file that was created, and make sure the environment variables are set properly. .. parsed-literal:: @@ -562,23 +569,26 @@ or develop directories. Add contents of existing tarfile to feature branch directory (if applicable) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** -**CONDITION 1: IF you have determined that there is an existing tarfile +**CONDITION 1: If there is an existing tarfile for the category (from the previous step)**, then untar the sample data tarball into the feature branch directory:: tar zxf ${METPLUS_EXISTING_DATA_TARFILE} -C ${METPLUS_DATA_TARFILE_DIR}/${METPLUS_FEATURE_BRANCH} -**CONDITION 2: If no tarfile exists yet, you can skip this step** +**CONDITION 2: If no tarfile exists yet, skip this step.** Rename or modify existing data or data structure (if applicable) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -**If the reason for your feature branch is to adjust an existing use case, such as renaming a use case -or changing the data file,** then adjust the directory structure and/or the data files which should now -be in your feature branch directory (from your last step). Changes to a use case name or input data for -a preexisting use case should be separately verified to run successfully, and noted in the Pull Request form +**If the reason for the feature branch is to adjust an existing use case, +such as renaming a use case or changing the data file, then adjust the +directory structure and/or the data files which should now be in the +feature branch directory (from the last step).** Changes to a +use case name or input data for +a pre-existing use case should be separately verified to run successfully, +and noted in the Pull Request form (described later). Add new data to feature branch directory @@ -589,13 +599,13 @@ Untar the new data tarball into the feature branch directory:: tar zxf ${METPLUS_DATA_STAGING_DIR}/${METPLUS_NEW_DATA_TARFILE} -C ${METPLUS_DATA_TARFILE_DIR}/${METPLUS_FEATURE_BRANCH} Verify that all of the old and new data exists in the directory that was -created (i.e. model_applications/). +created (i.e. *model_applications/*). Create the new tarfile ^^^^^^^^^^^^^^^^^^^^^^ Create the new sample data tarball. -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** **CONDITION 1:** Model Application Use Case Example:: @@ -626,14 +636,14 @@ created and tested. Trigger Input Data Ingest ------------------------- -If working in the dtcenter/METplus repository, please skip this step. +If working in the *dtcenter/METplus repository*, please skip this step. If working in a forked METplus repository, the newly added input data will not become available for the tests unless it is triggered from the dtcenter repository. A METplus developer will need to run the following steps. Please -provide them with the name of your forked repository and the branch that will +provide them with the name of the forked repository and the branch that will be used to create the pull request with the new use case. In this example, -the branch feature_XYZ exists in the my_fake_user/METplus repository. First, -clone the dtcenter/METplus repository, the run the following:: +the branch feature_XYZ exists in the *my_fake_user/METplus* repository. First, +clone the *dtcenter/METplus* repository, the run the following:: git remote add my_fake_user https://github.com/my_fake_user/METplus git checkout develop @@ -645,17 +655,18 @@ clone the dtcenter/METplus repository, the run the following:: These commands will add a new remote to the forked repository, create a branch off of the develop branch with the same name as the branch on the fork, pull in the changes from the forked branch, then push the new branch up to -dtcenter/METplus on GitHub. Finally, the remote is removed to avoid clutter. +*dtcenter/METplus* on GitHub. Finally, the remote is removed to avoid clutter. -Once these steps have been completed, go to dtcenter/METplus on GitHub in a web -browser and navigate to the +Once these steps have been completed, go to *dtcenter/METplus* on GitHub +in a web browser and navigate to the `Actions tab `_. Click on the job named "Docker Setup - Update Data Volumes" then click on "Update Data Volumes" and verify that the new data tarfile was found on the DTC web server and the new Docker data volume was created successfully. See :ref:`verify-new-input-data-was-found`. If the input data was ingested -properly, then delete the feature branch from dtcenter/METplus. This will avoid +properly, then delete the feature branch from *dtcenter/METplus*. +This will avoid confusion if this branch diverges from the branch on the forked repository that will be used in the final pull request. @@ -664,7 +675,7 @@ will be used in the final pull request. Add use case to the test suite ------------------------------ -The **internal_tests/use_cases/all_use_cases.txt** file in the METplus +The *internal_tests/use_cases/all_use_cases.txt* file in the METplus repository contains the list of all use cases. Add the new use case to this file so it will be available in the tests. See the :ref:`cg-ci-all-use-cases` section for details. @@ -674,7 +685,7 @@ the tests. See the :ref:`cg-ci-all-use-cases` section for details. Add new category to test runs ----------------------------- -The **.github/parm/use_case_groups.json** file in the METplus repository +The *.github/parm/use_case_groups.json* file in the METplus repository contains a list of the use case groups to run together. Add a new entry to the list that includes the category of the new use case, the list of indices that correspond to the index number described in the @@ -703,7 +714,7 @@ with index 2 and is marked to "run" for every push. New use cases are added as a separate item to make reviewing the test results easier. A new use case will produce new output data that is not found in the -"truth" data set which is compared the output of the use case runs to check +"truth" data set which is compared to the output of the use case runs to check if code changes altered the final results. Isolating the new output will make it easier to verify that the only differences are caused by the new data. It also makes it easier to check the size of the output data and length of time @@ -717,12 +728,13 @@ All of the use cases in the METplus repository are run via GitHub Actions to ensure that everything runs smoothly. If the above instructions to add new data were followed correctly, then GitHub Actions will automatically obtain the -new data and use it for the tests when you push your changes to GitHub. -Adding the use case to the test suite will allow you to check that the data +new data and use it for the tests when the changes are pushed to GitHub. +Adding the use case to the test suite will allow the ability to check +that the data was uploaded correctly and that the use case runs in the Python environment created in Docker. The status of the tests can be viewed on GitHub under the `Actions tab `_. -Your feature branch should be found in the list of results near the top. +The feature branch should be found in the list of results near the top. At the far left of the entry will be a small status icon: - A yellow circle that is spinning indicates that the build is currently @@ -731,7 +743,7 @@ At the far left of the entry will be a small status icon: waiting to be run. - A green check mark indicates that all of the jobs ran successfully. - A red X indicates that something went wrong. -- A grey octagon with an exclamatory mark (!) inside means it was cancelled. +- A gray octagon with an exclamation mark (!) inside means it was canceled. Click on the text next to the icon (last commit message) to see more details. @@ -748,7 +760,7 @@ Click on the job titled "Docker Setup - Update Data Volumes" On this page, click the item labeled "Update Data Volumes" to view the log output. If the new data was found properly, there will be output saying "Will pull data from..." followed by the path to the feature branch directory. -It will also list the dataset category that will be added +It will also list the dataset category that will be added. .. figure:: figure/data_volume_pull.png @@ -772,28 +784,34 @@ for assistance. Verify that the use case ran successfully ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You should verify that the use case was +Please verify that the use case was actually run by referring to the appropriate section under "Jobs" that starts with "Use Case Tests." Click on the job and search for the use case config filename in the log output by using the search box on the top right of the log output. -If the use case fails in GitHub Actions but runs successfully in the user's environment, -potential reasons include: +If the use case fails in GitHub Actions but runs successfully in the user's +environment, potential reasons include: - Errors providing input data (see :ref:`use_case_input_data`) - Using hard-coded paths from the user's machine -- Referencing variables set in the user's configuration file or local environment -- Memory usuage of the use case exceeds the available memory in hte Github Actions environment - -Github Actions has `limited memory `_ -available and will cause the use case to fail when exceeded. A failure caused by exceeding -the memory allocation in a Python Embedding script may result in an unclear error message. -If you suspect that this is the case, consider utilizing a Python memory profiler to check the -Python script's memory usage. If your use case exceeds the limit, try to pare +- Referencing variables set in the user's configuration file or local + environment +- Memory usage of the use case exceeds the available memory in the + Github Actions environment + +Github Actions has +`limited memory `_ +available and will cause the use case to fail when exceeded. A failure +caused by exceeding the memory allocation in a Python Embedding script +may result in an unclear error message. +If it is suspected that this is the case, consider utilizing a Python +memory profiler to check the +Python script's memory usage. If the use case exceeds the limit, try to pare down the data held in memory and use less memory intensive Python routines. -If memory mitigation cannot move the use case’s memory usage below the Github Actions limit, +If memory mitigation cannot move the use case’s memory usage below the +Github Actions limit, see :ref:`exceeded-Github-Actions` for next steps. Verify that the use case ran in a reasonable amount of time @@ -825,18 +843,18 @@ steps were unsuccessful in lowering memory usage, please take the following step Utilize a Python memory profiler to identify as specifically as possible where the script exceeds the memory limit. - Add the use case to the :ref:`memory-intense-use-cases` list. -- In the internal_tests/use_cases/all_use_cases.txt file, ensure that the +- In the *internal_tests/use_cases/all_use_cases.txt* file, ensure that the use case is listed as the lowest-listed use case in its respective category. - Change the number in front of the new use case to an 'X', preceeded + Change the number in front of the new use case to an 'X', preceded by the ‘#’ character:: #X::GridStat_fcstRTOFS_obsGHRSST_climWOA_sst::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst.conf, model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst/ci_overrides.conf:: icecover_env, py_embed -- In the **.github/parm/use_case_groups.json** file, remove the entry that +- In the *.github/parm/use_case_groups.json* file, remove the entry that was added during the :ref:`add_new_category_to_test_runs` for the new use case. This will stop the use case from running on a pull request. -- Push these two updated files to your branch in Github and confirm that it - now compiles successfully. +- Push these two updated files to the working branch in Github and + confirm that it now compiles successfully. - During the :ref:`create-a-pull-request` creation, inform the reviewer of the Github Actions failure. The reviewer should confirm the use case is successful when run manually, that the memory profiler output confirms that @@ -848,10 +866,13 @@ steps were unsuccessful in lowering memory usage, please take the following step Create a Pull Request ===================== -Create a pull request to merge the changes from your branch into the develop +Create a pull request to merge the changes from the working branch +into the develop branch. More information on this process can be found in the -:ref:`GitHub Workflow ` chapter under -"Open a pull request using your browser." +:ref:`GitHub Workflow ` +chapter under +:ref:`Open a pull request using a browser `. + Pull Request Reviewer Instructions ================================== @@ -864,17 +885,11 @@ was run successfully using the new data, they will need to update the links on the DTC web server before the pull request is merged so that the develop branch will contain the new data. -.. warning:: - Check if there are multiple feature branch directories that have data for - the same model_applications category. If there are more than one, then - you will need to be careful not to overwrite the final tarfile so that - one or more of the new data files are lost! These instructions need - to be updated to handle this situation. Log into the DTC Web Server with SSH ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The web server is only accessible if you are on the NCAR VPN. +The web server is only accessible on the NCAR VPN. .. parsed-literal:: @@ -906,7 +921,7 @@ Compare the feature branch file to the upcoming METplus version directory file:: diff ${METPLUS_FEATURE_BRANCH}/volume_mount_directories v${METPLUS_VERSION}/volume_mount_directories -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** **CONDITION 1: IF there is a new entry or change in the feature version**, copy the feature file into the upcoming METplus version directory and the develop directory:: @@ -919,7 +934,7 @@ Copy data from the feature directory into the next version directory **Make sure the paths are correct before copying.** -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** **CONDITION 1:** Model Applications Use Cases:: @@ -943,7 +958,7 @@ Copy data from the feature directory into the next version directory echo $to_directory ls $to_directory -Once you have verified the correct directories are set, copy the files:: +After verifying the directories are correct, copy the files:: cp -r $from_directory $to_directory/ @@ -952,7 +967,7 @@ List the tarfile for the use case category in the next release version directory cd ${METPLUS_DATA_TARFILE_DIR}/v${METPLUS_VERSION} ls -lh sample_data-${METPLUS_USE_CASE_CATEGORY}* -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** **CONDITION 1: IF the latest version of the tarfile is in this directory**, then rename the existing sample data tarball for @@ -962,7 +977,8 @@ the use case category just in case something goes wrong:: **OR** -**CONDITION 2: IF the sample data tarfile for the category is a link to another METplus +**CONDITION 2: IF the sample data tarfile for the category is a link to +another METplus version**, then simply remove the tarfile link:: unlink sample_data-${METPLUS_USE_CASE_CATEGORY}.tgz @@ -975,7 +991,7 @@ still needed. Create the new sample data tarfile. -**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO YOUR USE CASE. READ CAREFULLY!** +**ONLY RUN THE COMMAND THAT IS APPROPRIATE TO THE USE CASE. READ CAREFULLY!** **CONDITION 1:** Model Applications Use Cases:: @@ -1057,16 +1073,16 @@ The addition of a new use case results in new output data. When this happens, the reference branch needs to be updated so that future pull requests will compare their results to a "truth" data set that contains the new files. Create a pull request with "develop" as the source branch and "develop-ref" as -the destination branch. This is done so we can reference the pull request -number that is responsible for the changes in the truth data to easily track -where differences occurred. +the destination branch. This is done so that the pull request number +responsible for the changes in the truth data can be referenced to easily +track where differences occurred. -Merging develop into develop-ref often causes strange conflicts. We really -want to update develop-ref with the latest content of develop, so follow -these command line instructions in the METplus repository to reconcile the -conflicts before creating the pull request. +Merging develop into develop-ref often causes strange conflicts. It really is +necessary and important to update develop-ref with the latest content of +develop. Follow these command line instructions in the METplus repository to +reconcile the conflicts before creating the pull request. -* Reconcile conflicts between develop and develop-ref branches +* Reconcile conflicts between develop and develop-ref branches. :: @@ -1079,12 +1095,12 @@ conflicts before creating the pull request. * Next click `here `_ - and click the green "Create pull request" button to create the pull request + and click the green "Create pull request" button to create the pull request. .. figure:: figure/develop_into_develop-ref.png * Set the name of the pull request to "Update develop-ref after #XXXX" where - XXXX is the pull request number that introduced the differences + XXXX is the pull request number that introduced the differences. * Delete the template content and add the pull request number (formatted #XXXX) and a brief description of what has changed. The description is optional @@ -1092,7 +1108,7 @@ conflicts before creating the pull request. * Add the appropriate project and milestone values on the right hand side. -* Create the pull request +* Create the pull request. * Squash and merge the pull request. It is not necessary to wait for the automation checks to complete for this step. diff --git a/docs/Contributors_Guide/continuous_integration.rst b/docs/Contributors_Guide/continuous_integration.rst index 2ad3091b6..e3fb5ccfe 100644 --- a/docs/Contributors_Guide/continuous_integration.rst +++ b/docs/Contributors_Guide/continuous_integration.rst @@ -16,7 +16,7 @@ are pushed to GitHub. These tasks include: GitHub Actions Workflows ======================== -GitHub Actions runs workflows defined by files in the **.github/workflows** +GitHub Actions runs workflows defined by files in the *.github/workflows* directory of a GitHub repository. Files with the .yml suffix are parsed and GitHub Actions will trigger a workflow run if the triggering criteria is met. @@ -36,7 +36,6 @@ Many useful actions are provided by GitHub and external collaborators. Developers can also write their own custom actions to perform complex tasks to simplify a workflow. -**TODO Add screenshots** Testing (testing.yml) --------------------- @@ -81,7 +80,7 @@ at the bottom of the workflow summary page when the workflow has completed. Release Published (release_published.yml) - DEPRECATED ------------------------------------------------------ -**This workflow is no longer be required, as Slack now has GitHub integration +**This workflow is no longer required, as Slack now has GitHub integration to automatically create posts on certain events.** The workflow YAML file is still found in the repository for reference, but the workflow has been disabled via the Actions tab of the METplus GitHub webpage. @@ -101,7 +100,7 @@ Name ---- The name of a workflow can be specified to describe an overview of what is run. -The following line in the testing.yml file:: +The following line in the **testing.yml** file:: name: Testing @@ -167,9 +166,9 @@ Push This configuration tells GitHub Actions to trigger the workflow when changes are pushed to the repository and the following criteria are met: -* The branch is named **develop** or **develop-ref** -* The branch starts with **feature\_**, **main\_**, or **bugfix\_** -* Changes were made to at least one file that is not in the **docs** directory. +* The branch is named **develop** or **develop-ref**. +* The branch starts with **feature\_**, **main\_**, or **bugfix\_**. +* Changes were made to at least one file that is not in the *docs* directory. Pull Request ^^^^^^^^^^^^ @@ -185,7 +184,7 @@ This configuration tells GitHub Actions to trigger the workflow for pull requests in the repository and the following criteria are met: * The pull request was opened, reopened, or synchronized. -* Changes were made to at least one file that is not in the **docs** directory. +* Changes were made to at least one file that is not in the *docs* directory. The **synchronize** type triggers a workflow for every push to a branch that is included in an open pull request. @@ -224,10 +223,10 @@ to trigger this workflow. It lists the input values that are passed from the external repository. The inputs include: -* The repository that triggered the workflow, such as dtcenter/MET +* The repository that triggered the workflow, such as *dtcenter/MET* * The commit hash in the external repository that triggered the event * The reference (or branch) that triggered the event, such as - refs/heads/develop + *refs/heads/develop* * The GitHub username that triggered the event in the external repository (optional) @@ -238,7 +237,7 @@ develop branch. Future work is planned to support main_v* branches, which will involve using the 'ref' input to determine what to obtain in the workflow. -For example, changes pushed to dtcenter/MET main_v10.1 should trigger a +For example, changes pushed to *dtcenter/MET* main_v10.1 should trigger a testing workflow that runs on the METplus main_v4.1 branch. Jobs @@ -359,8 +358,10 @@ syntax:: ${{ steps.job_status.outputs.run_get_image }} The ID of the step is needed to reference the outputs for that step. -**Note that this notation should be referenced directly in the workflow YAML -file and not inside a script that is called by the workflow.** + +.. note:: + This notation should be referenced directly in the workflow YAML + file and not inside a script that is called by the workflow. To make the variable available to other jobs in the workflow, it will need to be set in the **outputs** section of the job:: @@ -393,12 +394,12 @@ On Push When a push event occurs the default behavior is to run the following: -* Create/Update the METplus Docker image -* Look for new input data -* Run unit tests -* Run any use cases marked to run (see :ref:`cg-ci-use-case-tests`) +* Create/Update the METplus Docker image. +* Look for new input data. +* Run unit tests. +* Run any use cases marked to run (see :ref:`cg-ci-use-case-tests`). -If the push is on the **develop** or a **main_vX.Y** branch, then all +If the push is on the *develop* or a *main_vX.Y* branch, then all of the use cases are run. Default behavior for push events can be overridden using @@ -407,8 +408,8 @@ Default behavior for push events can be overridden using On Pull Request """"""""""""""" -When a pull request is created into the **develop** branch or -a **main_vX.Y** branch, additional jobs are run in automation. +When a pull request is created into the *develop* branch or +a *main_vX.Y* branch, additional jobs are run in automation. In addition to the jobs run for a push, the scripts will: * Run all use cases @@ -419,13 +420,13 @@ In addition to the jobs run for a push, the scripts will: On Push to Reference Branch """"""""""""""""""""""""""" -Branches with a name that ends with **-ref** contain the state of the +Branches with a name that ends with *-ref* contain the state of the repository that will generate output that is considered "truth" data. In addition to the jobs run for a push, the scripts will: -* Run all use cases +* Run all use cases. * Create/Update Docker data volumes that store truth data with the use case - output + output. See :ref:`cg-ci-create-output-data-volumes` for more information. @@ -438,14 +439,14 @@ The automation logic reads the commit message for the last commit before a push. Keywords in the commit message can override the default behavior. Here is a list of the currently supported keywords and what they control: -* **ci-skip-all**: Don't run anything - skip all automation jobs -* **ci-skip-use-cases**: Don't run any use cases -* **ci-skip-unit-tests**: Don't run the Pytest unit tests -* **ci-run-all-cases**: Run all use cases +* **ci-skip-all**: Don't run anything - skip all automation jobs. +* **ci-skip-use-cases**: Don't run any use cases. +* **ci-skip-unit-tests**: Don't run the Pytest unit tests. +* **ci-run-all-cases**: Run all use cases. * **ci-run-diff**: Obtain truth data and run diffing logic for - use cases that are marked to run + use cases that are marked to run. * **ci-run-all-diff**: Obtain truth data and run diffing logic for - all use cases + all use cases. .. _cg-ci-get-image: @@ -475,7 +476,7 @@ This job calls the **docker_setup.sh** script. This script builds a METplus Docker image and pushes it to DockerHub. The image is pulled instead of built in each test job to save execution time. The script attempts to pull the appropriate Docker image from DockerHub -(dtcenter/metplus-dev:*BRANCH_NAME*) if it already exists so that unchanged +(*dtcenter/metplus-dev:BRANCH_NAME*) if it already exists so that unchanged components of the Docker image do not need to be rebuilt. This reduces the time it takes to rebuild the image for a given branch on a subsequent workflow run. @@ -498,7 +499,7 @@ i.e. METplotpy or METviewer, until a corresponding change is made to that component. If this occurs then some of the METplus use cases may break. To allow the tests to run successfully in the meantime, an option was added to force the version of the MET tag that is used to build the METplus Docker image -that is used for testing. In the testing.yml workflow file, +that is used for testing. In the **testing.yml** workflow file, there is a commented variable called MET_FORCE_TAG that can be uncommented and set to force the version of MET to use. This variable is found in the **get_image** job under the **env** section @@ -532,9 +533,9 @@ Create/Update Docker Data Volumes The METplus use case tests obtain input data from Docker data volumes. Each use case category that corresponds to a directory in -**parm/use_cases/model_applications** has its own data volume that contains +*parm/use_cases/model_applications* has its own data volume that contains all of the data needed to run those use cases. The MET Tool Wrapper use cases -found under **parm/use_cases/met_tool_wrapper** also have a data volume. +found under *parm/use_cases/met_tool_wrapper* also have a data volume. These data are made available on the DTC web server. This job utilizes the @@ -557,6 +558,38 @@ process can be found in the :ref:`use_case_input_data` section of the Add Use Cases chapter of the Contributor's Guide. +.. _cg-ci-unit-tests: + +Unit Tests +---------- + +Unit tests are run via pytest. +Groups of pytests are run in the 'pytests' job. +The list of groups that will be run in the automated tests are found in +.github/parm/pytest_groups.txt. +See :ref:`cg-unit-tests` for more information on pytest groups. + +Items in pytest_groups.txt can include:: + + * A single group marker name, i.e. wrapper_a + * Multiple group marker names separated by _or_, i.e. plotting_or_long + * A group marker name to exclude starting with not_, i.e. not_wrapper + +All pytest groups are currently run in a single GitHub Actions job. +This was done because the existing automation logic builds a Docker +environment to run the tests and each testing environment takes a few minutes +to create (future improvements may speed up execution time by running the +pytests directly in the GitHub Actions environment instead of Docker). +Running the pytests in smaller groups serially takes substantially less time +than calling all of the existing pytests in a single call to pytest, +so dividing tests into groups is recommended to improve performance. +Searching for the string "deselected in" in the pytests job log can be used +to see how long each group took to run. + +Future enhancements could be made to save and parse this information for each +run to output a summary at the end of the log file to more easily see which +groups could be broken up to improve performance. + .. _cg-ci-use-case-tests: Use Case Tests @@ -568,7 +601,7 @@ All Use Cases ^^^^^^^^^^^^^ All of the existing use cases are listed in **all_use_cases.txt**, -found in internal_tests/use_cases. +found in *internal_tests/use_cases*. The file is organized by use case category. Each category starts a line that following the format:: @@ -576,9 +609,10 @@ a line that following the format:: Category: where ** is the name of the use case category. -See :ref:`use_case_categories` for more information. If you are adding a -use case that will go into a new category, you will have to add a new category -definition line to this file and add your new use case under it. Each use case +See :ref:`use_case_categories` for more information. If a use case +is being added will go into a new category, +a new category definition line will have to be added +to this file and the new use case added under it. Each use case in that category will be found on its own line after this line. The use cases can be defined using the following formats:: @@ -592,7 +626,7 @@ The index is the number associated with the use case so it can be referenced easily. The first index number in a new category should be 0. Each use case added should have an index that is one greater than the previous. If it has been determined that a use case cannot run in the automated tests, -then the index number should be replaced with "#X" so that is it included +then the index number should be replaced with "#X" so that it is included in the list for reference but not run by the tests. name @@ -610,7 +644,7 @@ config_args """"""""""" This is the path of the config file used for the use case relative to -**parm/use_cases**. +*parm/use_cases*. Example:: @@ -651,7 +685,7 @@ images using Conda that can be used to run use cases. These images are stored on DockerHub in *dtcenter/metplus-envs* and are named with a tag that corresponds to the keyword without the **_env** suffix. The environments were created using Docker commands via scripts that are found -in **scripts/docker/docker_env**. +in *scripts/docker/docker_env*. Existing keywords that set up Conda environments used for use cases are: * cfgrib_env @@ -670,7 +704,7 @@ Example:: spacetime_env The above example uses the Conda environment -in dtcenter/metplus-envs:**spacetime** to run a user script. +in *dtcenter/metplus-envs*:**spacetime** to run a user script. Note that only one dependency that contains the **_env** suffix can be supplied to a given use case. @@ -680,10 +714,11 @@ Other Environments A few of the environments do not contain Conda environments and are handled a little differently. -* **gempak_env** - Used if GempakToCF.jar is needed for a use case to convert +* **gempak_env** - Used if **GempakToCF.jar** is needed for a use + case to convert GEMPAK data to NetCDF format so it can be read by the MET tools. Instead of creating a Python environment to use for the use case, - this Docker image installs Java and obtains the GempakToCF.jar file. + this Docker image installs Java and obtains the **GempakToCF.jar** file. When creating the Docker container to run the use cases, the necessary Java files are copied over into the container that runs the use cases so that the JAR file can be run by METplus wrappers. @@ -701,13 +736,13 @@ to run a use case: * **py_embed** - Used if a different Python environment is required to run a Python Embedding script. If this keyword is included with a Python environment, then the MET_PYTHON_EXE environment variable will be set to - specify the version of Python3 that is included in that environment + specify the version of Python3 that is included in that environment. Example:: pygrib_env,py_embed -In this example, the dtcenter/metplus-envs:**pygrib** environment is used to +In this example, the *dtcenter/metplus-envs*:**pygrib** environment is used to run the use case. Since **py_embed** is also included, then the following will be added to the call to run_metplus.py so that the Python embedding script will use the **pygrib** environment to run:: @@ -736,14 +771,15 @@ for more information on how to use Python Embedding. * **cartopy** - Used if cartopy 0.18.0 is needed in the Conda environment. Cartopy uses shapefiles that are downloaded as needed. The URL that is used - to download the files has changed since cartopy 0.18.0 and we have run into - issues where the files cannot be obtained. To remedy this issue, we modified - the scripts that generate the Docker images that contain the Conda - environments that use cartopy to download these shape files so they will - always be available. These files need to be copied from the Docker + to download the files has changed since cartopy 0.18.0 and there have been + issues where the files cannot be obtained. + To remedy this issue, the METplus Docker images, which contain the + Conda environments, including Cartopy, have been modified to download + the necessary shape files so that they will always be available. These + files need to be copied from the Docker environment image into the testing image. When this keyword is found in the - dependency list, a different Dockerfile (Dockerfile.run_cartopy found in - .github/actions/run_tests) is used to create the testing environment and + dependency list, a different Dockerfile (**Dockerfile.run_cartopy** found in + *.github/actions/run_tests*) is used to create the testing environment and copy the required shapefiles into place. @@ -757,14 +793,14 @@ packages take a very long time to install in Docker. The new approach involves creating Docker images that use Conda to create a Python environment that can run the use case. To see what is available in each of the existing Python environments, refer to the comments in the scripts found in -**scripts/docker/docker_env/scripts**. +*scripts/docker/docker_env/scripts*. New environments must be added by a METplus developer, so please create a discussion on the `METplus GitHub Discussions `_ forum if none of these environments contain the package requirements needed to run a new use case. -A **README.md** file can be found in **scripts/docker/docker_env** that +A **README.md** file can be found in *scripts/docker/docker_env* that provides commands that can be run to recreate a Docker image if the conda environment needs to be updated. Please note that Docker must be installed on the workstation used to create new Docker images and @@ -807,7 +843,7 @@ Example:: The cartopy python package automatically attempts to download shapefiles as needed. The URL that is used in cartopy version 0.18.0 and earlier no longer -exists, so use cases that needs these files will fail if they are +exists, so use cases that need these files will fail if they are not found locally. If a conda environment uses cartopy, these shapefiles may need to be downloaded by the user running the use case even if the conda environment was created by another user. @@ -826,10 +862,10 @@ Use Case Groups The use cases that are run in the automated test suite are divided into groups that can be run concurrently. -The **use_case_groups.json** file (found in **.github/parm**) +The **use_case_groups.json** file (found in *.github/parm*) contains a list of the use case groups to run together. In METplus version 4.0.0 and earlier, this list was -found in the .github/workflows/testing.yml file. +found in the *.github/workflows/testing.yml* file. Each use case group is defined with the following format:: @@ -904,7 +940,7 @@ It also supports a range of numbers separated with a dash. Example:: The above example will run a job with data_assimilation 0, 1, 2, and 3, then another job with data_assimilation 4 and 5. -You can also use a combination of commas and dashes to define the list of cases +Use a combination of commas and dashes to define the list of cases to run. Example:: { @@ -993,7 +1029,7 @@ After all of the use cases in a group have finished running, the output that was generated is compared to the truth data to determine if any of the output was changed. The truth data for each use case group is stored in a Docker data volume on DockerHub. The **diff_util.py** script -(found in **metplus/util**) is run to compare all of the output files in +(found in *metplus/util*) is run to compare all of the output files in different ways depending on the file type. The logic in this script could be improved to provide more robust testing. diff --git a/docs/Contributors_Guide/documentation.rst b/docs/Contributors_Guide/documentation.rst index 98269f3f3..2585a31bd 100644 --- a/docs/Contributors_Guide/documentation.rst +++ b/docs/Contributors_Guide/documentation.rst @@ -30,8 +30,8 @@ documentation: * sphinx_rtd_theme-0.4.3 Which versions are being used by the current METplus release can be viewed -by looking at either environment.yml or requirements.txt, both of which -are found in the METplus/ directory. If the desire is to replicate all the +by looking at either **environment.yml** or **requirements.txt**, both of which +are found in the *METplus/* directory. If the desire is to replicate all the packages employed by METplus, please refer to :numref:`conda_env` of the Contributor's Guide. @@ -53,7 +53,7 @@ Documentation for the use cases is found in the following directories: * *METplus/docs/use_cases/model_applications* * This directory contains documentation pertaining to use cases that are - based on model data, and utilize more than one MET tool/METplus + based on model data, and utilize more than one MET *tool/METplus* wrapper. Please refer to the :ref:`Document New Use Case ` @@ -87,11 +87,11 @@ User's Guide: * Modify any of the affected sections from the *METplus/docs/Users_Guide* directory: - * glossary.rst (Glossary) - * references.rst (Reference) - * systemconfiguration.rst (System Configuration) - * usecases.rst (Use cases) - * wrappers.rst (METplus wrappers) + * **glossary.rst** (Glossary) + * **references.rst** (Reference) + * **systemconfiguration.rst** (System Configuration) + * **usecases.rst** (Use cases) + * **wrappers.rst** (METplus wrappers) Contributor's Guide: ~~~~~~~~~~~~~~~~~~~~ @@ -100,21 +100,21 @@ Contributor's Guide: * Modify any of the affected sections from the *METplus/docs/Contributors_Guide* directory: - * add_use_case.rst (How to add new use cases) - * basic_components.rst (The basic components of a METplus wrapper) - * coding_standards.rst (The coding standards currently in use) - * conda_env.rst (How to set up the conda environment for + * **add_use_case.rst** (How to add new use cases) + * **basic_components.rst** (The basic components of a METplus wrapper) + * **coding_standards.rst** (The coding standards currently in use) + * **conda_env.rst** (How to set up the conda environment for running METplus) - * continuous_integration.rst (How to set up a continuous integration - workflow) - * create_wrapper.rst (How to create a new METplus wrapper) - * deprecation.rst (What to do to deprecate a variable) - * documentation.rst (Describing the documentation process and files) - * github_workflow.rst (A description of how releases are made, + * **continuous_integration.rst** (How to set up a continuous integration + workflow) + * **create_wrapper.rst** (How to create a new METplus wrapper) + * **deprecation.rst** (What to do to deprecate a variable) + * **documentation.rst** (Describing the documentation process and files) + * **github_workflow.rst** (A description of how releases are made, how to to obtain source code from the GitHub repository) - * index.rst (The page that shows all the 'chapters/sections' + * **index.rst** (The page that shows all the 'chapters/sections' of the Contributor's Guide) - * testing.rst (A description of how to set up testing the + * **testing.rst** (A description of how to set up testing the wrapper code) Release Guide: @@ -124,40 +124,40 @@ Release Guide: any METplus component, including official, bugfix, and development releases. -* Each METplus component has a top level file (e.g. metplus.rst) +* Each METplus component has a top level file (e.g. **metplus.rst**) which simply contains references to files for each of the - releases. For example, metplus.rst contains references to: + releases. For example, **metplus.rst** contains references to: - * metplus_official - * metplus_bugfix - * metplus_development + * metplus_official. + * metplus_bugfix. + * metplus_development. -* Each release file (e.g. metplus_official.rst, metplus_bugfix.rst, - metplus_development.rst) contains, at a minimum, a replacement +* Each release file (e.g. **metplus_official.rst**, **metplus_bugfix.rst**, + **metplus_development.rst**) contains, at a minimum, a replacement value for the projectRepo variable and include statements for each release step. These individual steps - (e.g. open_release_issue.rst, clone_project_repository.rst, etc.) + (e.g. **open_release_issue.rst**, **clone_project_repository.rst**, etc.) may be common to multiple METplus components. These common steps are located in the *release_steps* directory. However, a METplus - component may have different instructions from other componenets - (e.g. For METplus wrappers, update_version.rst, - create_release_extra.rst, etc.). In this case, the instructions + component may have different instructions from other components + (e.g. For **METplus wrappers**, **update_version.rst**, + **create_release_extra.rst**, etc.). In this case, the instructions that are specific to that component are located in a subdirectory of *release_steps*. For example, files that are specific to METplus wrappers are located in *release_steps/metplus*, files that are specific to METcalcpy are located in *release_steps/metcalcpy*. -* The file for each individual step (e.g. open_release_issue.rst, - update_version.rst, etc.) contains the instructions for +* The file for each individual step (e.g. **open_release_issue.rst**, + **update_version.rst**, etc.) contains the instructions for completing that step for the release. Verification Datasets Guide: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* To add/modify any relevant datasets in attempt to create a - centralized catalogue of verification datasets to provide the model +* To add/modify any relevant datasets in an attempt to create a + centralized catalog of verification datasets to provide the model verification community with relevant "truth" datasets. See the `Verification Datasets Guide Overview `_ for more information. @@ -172,11 +172,11 @@ build and display the documentation. Read the Docs simplifies the documentation process by building, versioning, and hosting the documentation. Read the Docs supports multiple versions for each repository. For the METplus -compoents, the "latest" version will point to the latest official (stable) +components, the "latest" version will point to the latest official (stable) release. The "develop" or "development" version will point to the most up to date development code. There may also be other previous versions of the software available in the version selector menu, which is accessible by -clicking in the bottom left corner of the the documentation pages. +clicking in the bottom left corner of the documentation pages. Automation rules allow project maintainers to automate actions on new branches and tags on repositories. For the METplus components, documentation is @@ -191,14 +191,16 @@ The documentation of these "versions" are automatically hidden, however, the documentation can be accessed by directly modifying the URL. For example, to view "feature_836_rtd_doc" for the METplus repository the URL would be: - **https://metplus.readthedocs.io/en/feature_836_rtd_doc** + *https://metplus.readthedocs.io/en/feature_836_rtd_doc* (Note that this link is not valid as this branch does not currently exist, - however contributors can replace the "feature_836_rtd_doc" with the + however contributors can replace the "*feature_836_rtd_doc*" with the appropriate branch name.) -The URL branch name will be lowercase regardless of the actual branch letter casing, -i.e. "feature_836_RTD_Doc" branch would be accessed by the above mentioned URL. +The URL branch name will be lowercase regardless of the actual branch +letter casing, +i.e. "*feature_836_RTD_Doc*" branch would be accessed by the +above-mentioned URL. Read the Docs will automatically delete the documentation for a feature branch and a bugfix branch when the branch is deleted. @@ -242,7 +244,7 @@ This script does the following: * Builds the Sphinx documentation * Builds the doxygen documentation * Removes unwanted text from use case documentation -* Copies doxygen files into _build/html for easy deployment +* Copies doxygen files into* _build/html* for easy deployment * Creates symbolic links under Users_Guide to the directories under 'generated' to preserve old URL paths diff --git a/docs/Contributors_Guide/github_workflow.rst b/docs/Contributors_Guide/github_workflow.rst index dd748a672..3d9134076 100644 --- a/docs/Contributors_Guide/github_workflow.rst +++ b/docs/Contributors_Guide/github_workflow.rst @@ -293,7 +293,10 @@ Push the feature branch to GitHub to push the changes to the origin (i.e. to the *https://github.com//METplus* repository). + +.. _pull-request-browser: + Open a pull request using a browser ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/Contributors_Guide/testing.rst b/docs/Contributors_Guide/testing.rst index 3db6f6ff7..93c2f9627 100644 --- a/docs/Contributors_Guide/testing.rst +++ b/docs/Contributors_Guide/testing.rst @@ -4,19 +4,59 @@ Testing Test scripts are found in the GitHub repository in the internal_tests directory. +.. _cg-unit-tests: + Unit Tests ---------- Unit tests are run with pytest. They are found in the *pytests* directory. Each tool has its own subdirectory containing its test files. -**run_pytests.sh** is a bash script that can be run to execute all of the -pytests. A report will be output showing which pytest categories failed. -When running on a new computer, a -**minimum_pytest..sh** +Unit tests can be run by running the 'pytest' command from the +internal_tests/pytests directory of the repository. +The 'pytest' Python package must be available. +A report will be output showing which pytest categories failed. +When running on a new computer, a **minimum_pytest..sh** file must be created to be able to run the script. This file contains information about the local environment so that the tests can run. +All unit tests must include one of the custom markers listed in the +internal_tests/pytests/pytest.ini file. Some examples include: + + * util + * wrapper_a + * wrapper_b + * wrapper_c + * wrapper + * long + * plotting + +To apply a marker to a unit test function, add the following on the line before +the function definition:: + + @pytest.mark. + +where is one of the custom marker strings listed in pytest.ini. + +New pytest markers should be added to the pytest.ini file with a brief +description. If they are not added to the markers list, then a warning will +be output when running the tests. + +There are many unit tests for METplus and false failures can occur if all of +the are attempted to run at once. +To run only tests with a given marker, run:: + + pytest -m + +To run all tests that do not have a given marker, run:: + + pytest -m "not " + +Multiple marker groups can be run by using the 'or' keyword:: + + pytest -m " or " + + Use Case Tests -------------- diff --git a/docs/_static/s2s-GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.png b/docs/_static/s2s-GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.png new file mode 100644 index 000000000..6d9775a2a Binary files /dev/null and b/docs/_static/s2s-GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.png differ diff --git a/docs/_static/s2s-SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.png b/docs/_static/s2s-SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.png new file mode 100644 index 000000000..4de9c9f44 Binary files /dev/null and b/docs/_static/s2s-SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.png differ diff --git a/docs/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.py b/docs/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.py new file mode 100644 index 000000000..79502d2b3 --- /dev/null +++ b/docs/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.py @@ -0,0 +1,144 @@ +""" +GridStat: Determine dominant ensemble members terciles and calculate categorical outputs +======================================================================================== + +model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf + +""" +############################################################################## +# Scientific Objective +# -------------------- +# This use case ingests a CFSv2 Ensemble forecast, with all ensemble members in a single file for a given year. +# 29 years of forecast ensembles are used to create probabilities for each tercile, which is accomplished by a Python script. +# Of the terciles, each gridpoint is assigned a value corresponding to the tercile that is most likely to occur. This is compared to an observation set +# that contains the tercile data and MCTS line type is requested. +# This use case highlights the inclusion of tercile data for calculating HSS; in particular, how to utilize the hss_ec_value option to +# preset the expected values rather than relying on categorical values. + +############################################################################## +# Datasets +# --------------------- +# +# | **Forecast:** 29 CFSv2 Ensemble files, 2m temperature fields +# +# | **Observations:** GHCNCAMS, 2m temperature field +# +# +# | **Location:** All of the input data required for this use case can be found in the met_test sample data tarball. Click here to the METplus releases page and download sample data for the appropriate release: https://github.com/dtcenter/METplus/releases +# | This tarball should be unpacked into the directory that you will set the value of INPUT_BASE. See `Running METplus`_ section for more information. +# +# | **Data Source:** CPC + +############################################################################## +# METplus Components +# ------------------ +# +# This use case calls a Python script 29 times, once for each year of data of the CFSv2 ensemble. +# Each time a successful call to the script is made, a grid of 1s, 2s, and 3s is returned, representing which tercile was dominant for the gridpoint. +# GridStat processes the forecast and observation fields, and outputs the requested line types. + +############################################################################## +# METplus Workflow +# ---------------- +# +# This use case utilizes 29 years of forecast data, with 24 members in each ensemble forecast. +# The following boundary times are used for the entire script: +# +# | **Init Beg:** 1982-01-01 +# | **Init End:** 2010-01-02 +# +# Because the increment is 1 year, all January 1st from 1982 to 2010 are processed for a total of 29 years. +# + +############################################################################## +# METplus Configuration +# --------------------- +# +# METplus first loads all of the configuration files found in parm/metplus_config, +# then it loads any configuration files passed to METplus via the command line +# i.e. -c parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf +# + +############################################################################## +# MET Configuration +# --------------------- +# +# METplus sets environment variables based on the values in the METplus configuration file. These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the ‘User Defined Config’ section on the ‘System Configuration’ page of the METplus User’s Guide for more information. +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/met_config/GridStatConfig_wrapped +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case can be run two ways: +# +# 1) Passing in GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf then a user-specific system configuration file:: +# +# run_metplus.py /path/to/METplus/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile:: +# +# run_metplus.py /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf +# +# The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [config] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# +# + +############################################################################## +# Expected Output +# --------------- +# +# A successful run will output the following both to the screen and to the logfile:: +# +# INFO: METplus has successfully finished running. +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. +# Output for the use case will be found in 29 folders(relative to **OUTPUT_BASE**). +# The output will follow the time information of the run. Specifically: +# +# * YYYY01 +# +# where YYYY will be replaced by values corresponding to each of the years (1982 through 2010). +# Each of those folders will have the following files: +# +# * grid_stat_000000L_19820101_000000V_pairs.nc +# * grid_stat_000000L_19820101_000000V_mctc.txt +# * grid_stat_000000L_19820101_000000V_mcts.txt +# * grid_stat_000000L_19820101_000000V.stat +# + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * GridStatUseCase +# * ProbabilityVerificationUseCase +# * PythonEmbeddingFileUseCase +# * S2SAppUseCase +# * NETCDFFileUseCase +# +# Navigate to the :ref:`quick-search` page to discover other similar use cases. +# +# +# +# sphinx_gallery_thumbnail_path = '_static/s2s-GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.png' + diff --git a/docs/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.py b/docs/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.py new file mode 100644 index 000000000..7be759ada --- /dev/null +++ b/docs/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.py @@ -0,0 +1,163 @@ +""" +SeriesAnalysis: Standardize ensemble members and calculate probabilistic outputs +================================================================================ + +model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf + +""" +############################################################################## +# Scientific Objective +# -------------------- +# This use case ingests a CFSv2 Ensemble forecast, with all ensemble members in a single file for a given year. +# 29 years of forecast ensembles are used to create climatologies for each ensemble member. These climatologies +# are then used to normalize each ensemble member via the Gen-Ens-Prod tool, allowing a meaningful comparison to +# the observation dataset, which is presented as normalized. The forecast to observation verification are completed across both the temporal and spatial. +# This use case highlights several important features within METplus; in particular, how to create climatologies for ensemble members using SeriesAnalysis, +# how those climatologies can be used by GenEnsProd to normalize each ensemble member to its corresponding climatology, +# and calculating probabilistic verfication on s2s data, which is a frequent request from climatological centers. + +############################################################################## +# Datasets +# --------------------- +# +# | **Forecast:** 29 CFSv2 Ensemble files, 2m temperature fields +# +# | **Observations:** GHCNCAMS, 2m temperature field +# +# +# | **Location:** All of the input data required for this use case can be found in the met_test sample data tarball. Click here to the METplus releases page and download sample data for the appropriate release: https://github.com/dtcenter/METplus/releases +# | This tarball should be unpacked into the directory that you will set the value of INPUT_BASE. See `Running METplus`_ section for more information. +# +# | **Data Source:** CPC + +############################################################################## +# METplus Components +# ------------------ +# +# This use case initially runs SeriesAnalysis 24 times, once for each member of the CFSv2 ensemble, across the entire 29 years for forecast data. +# The resulting 24 outputs are read in by GenEnsProd, which is called 29 times (once for each year). GenEnsProd uses the **normalize** option +# and the SeriesAnalysis outputs to normalize each of the ensemble members relative to its climatology (FBAR) and standard deviation (FSTDEV). +# The output from GenEnsProd are 29 files containing the uncalibrated probability forecasts for the lower tercile of January for each year. +# The final probability verification is done across the temporal scale in SeriesAnalysis, and the spatial scale in GridStat. + +############################################################################## +# METplus Workflow +# ---------------- +# +# This use case utilizes 29 years of forecast data, with 24 members in each ensemble forecast. +# The following boundary times are used for the entire script: +# +# | **Init Beg:** 1982-01-01 +# | **Init End:** 2010-01-02 +# +# Because the increment is 1 year, all January 1st from 1982 to 2010 are processed for a total of 29 years. +# + +############################################################################## +# METplus Configuration +# --------------------- +# +# METplus first loads all of the configuration files found in parm/metplus_config, +# then it loads any configuration files passed to METplus via the command line +# i.e. -c parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf +# + +############################################################################## +# MET Configuration +# --------------------- +# +# METplus sets environment variables based on the values in the METplus configuration file. These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the ‘User Defined Config’ section on the ‘System Configuration’ page of the METplus User’s Guide for more information. +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/met_config/SeriesAnalysisConfig_wrapped +# .. literalinclude:: ../../../../parm/met_config/GenEnsProdConfig_wrapped +# .. literalinclude:: ../../../../parm/met_config/GridStatConfig_wrapped +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case can be run two ways: +# +# 1) Passing in SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf then a user-specific system configuration file:: +# +# run_metplus.py /path/to/METplus/parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf:: +# +# run_metplus.py /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf +# +# The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [config] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# +# + +############################################################################## +# Expected Output +# --------------- +# +# A successful run will output the following both to the screen and to the logfile:: +# +# INFO: METplus has successfully finished running. +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. +# Output for use case will be found in 4 distinct folders (relative to **OUTPUT_BASE**). +# The output from the first SeriesAnalysis call goes to **SA_run1** will contain the following files: +# +# * mem??_output.nc +# +# where ?? will be replaced by values corresponding to each of the ensemble members (0 through 23). +# The output for GenEnsProd goes into **GEP** and contains the following files: +# +# * gen_ens_prod_YYYY01_ens.nc +# +# where YYYY will be replaced by each year of the forecast data being processed (1982 through 2010). +# The output from the second SeriesAnalysis call goes to **SA_run2** and contains the following files: +# +# * 198201to201002_CFSv2_SA.nc +# +# Finally, the output from GridStat will be in **GridStat** and will contain 29 folders of the following format: +# +# * ????01 +# +# where ???? will correspond to each year of the forecast data being processed (1982 through 2010). +# Each of those folders will have the following files: +# +# * grid_stat_198201_000000L_19700101_000000V_pairs.nc +# * grid_stat_198201_000000L_19700101_000000V_pstd.txt +# * grid_stat_198201_000000L_19700101_000000V.stat +# + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * SeriesAnalysisUseCase +# * GenEnsProdUseCase +# * GridStatUseCase +# * ProbabilityVerificationUseCase +# * S2SAppUseCase +# * NETCDFFileUseCase +# +# Navigate to the :ref:`quick-search` page to discover other similar use cases. +# +# +# +# sphinx_gallery_thumbnail_path = '_static/s2s-SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.png' + diff --git a/internal_tests/pytests/plotting/make_plots/test_make_plots_wrapper.py b/internal_tests/pytests/plotting/make_plots/test_make_plots_wrapper.py index f1a2e54d3..2be97153a 100644 --- a/internal_tests/pytests/plotting/make_plots/test_make_plots_wrapper.py +++ b/internal_tests/pytests/plotting/make_plots/test_make_plots_wrapper.py @@ -1,47 +1,14 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -import os -import datetime -import sys -import logging import pytest -import datetime -import produtil.setup +import os from metplus.wrappers.make_plots_wrapper import MakePlotsWrapper -from metplus.util import met_util as util - -# -# These are tests (not necessarily unit tests) for the -# wrapper to make plots, make_plots_wrapper.py -# NOTE: This test requires pytest, which is NOT part of the standard Python -# library. -# These tests require one configuration file in addition to the three -# required METplus configuration files: test_make_plots.conf. This contains -# the information necessary for running all the tests. Each test can be -# customized to replace various settings if needed. -# - -# -# -----------Mandatory----------- -# configuration and fixture to support METplus configuration files beyond -# the metplus_data, metplus_system, and metplus_runtime conf files. -# +METPLUS_BASE = os.getcwd().split('/internal_tests')[0] -# Add a test configuration -def pytest_addoption(parser): - parser.addoption("-c", action="store", help=" -c ") -# @pytest.fixture -def cmdopt(request): - return request.config.getoption("-c") - -# -# ------------Pytest fixtures that can be used for all tests --------------- -# -#@pytest.fixture def make_plots_wrapper(metplus_config): """! Returns a default MakePlotsWrapper with /path/to entries in the metplus_system.conf and metplus_runtime.conf configuration @@ -55,35 +22,8 @@ def make_plots_wrapper(metplus_config): config = metplus_config(extra_configs) return MakePlotsWrapper(config) -# ------------------TESTS GO BELOW --------------------------- -# - -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# To test numerous files for filesize, use parametrization: -# @pytest.mark.parametrize( -# 'key, value', [ -# ('/usr/local/met-6.1/bin/point_stat', 382180), -# ('/usr/local/met-6.1/bin/stat_analysis', 3438944), -# ('/usr/local/met-6.1/bin/pb2nc', 3009056) -# -# ] -# ) -# def test_file_sizes(key, value): -# st = stat_analysis_wrapper() -# # Retrieve the value of the class attribute that corresponds -# # to the key in the parametrization -# files_in_dir = [] -# for dirpath, dirnames, files in os.walk("/usr/local/met-6.1/bin"): -# for name in files: -# files_in_dir.append(os.path.join(dirpath, name)) -# if actual_key in files_in_dir: -# # The actual_key is one of the files of interest we retrieved from -# # the output directory. Verify that it's file size is what we -# # expected. -# assert actual_key == key -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -METPLUS_BASE = os.getcwd().split('/internal_tests')[0] +@pytest.mark.plotting def test_get_command(metplus_config): # Independently test that the make_plots python # command is being put together correctly with @@ -98,6 +38,8 @@ def test_get_command(metplus_config): test_command = mp.get_command() assert(expected_command == test_command) + +@pytest.mark.plotting def test_create_c_dict(metplus_config): # Independently test that c_dict is being created # and that the wrapper and config reader diff --git a/internal_tests/pytests/plotting/plot_util/test_plot_util.py b/internal_tests/pytests/plotting/plot_util/test_plot_util.py index 386c584bb..d22ce6b7a 100644 --- a/internal_tests/pytests/plotting/plot_util/test_plot_util.py +++ b/internal_tests/pytests/plotting/plot_util/test_plot_util.py @@ -1,48 +1,23 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +import pytest import os -import datetime import sys -import logging -import pytest import datetime +import logging + import numpy as np import pandas as pd -import produtil.setup -# ------------------TESTS GO BELOW --------------------------- -# - -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# To test numerous files for filesize, use parametrization: -# @pytest.mark.parametrize( -# 'key, value', [ -# ('/usr/local/met-6.1/bin/point_stat', 382180), -# ('/usr/local/met-6.1/bin/stat_analysis', 3438944), -# ('/usr/local/met-6.1/bin/pb2nc', 3009056) -# -# ] -# ) -# def test_file_sizes(key, value): -# st = stat_analysis_wrapper() -# # Retrieve the value of the class attribute that corresponds -# # to the key in the parametrization -# files_in_dir = [] -# for dirpath, dirnames, files in os.walk("/usr/local/met-6.1/bin"): -# for name in files: -# files_in_dir.append(os.path.join(dirpath, name)) -# if actual_key in files_in_dir: -# # The actual_key is one of the files of interest we retrieved from -# # the output directory. Verify that it's file size is what we -# # expected. -# assert actual_key == key -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! METPLUS_BASE = os.getcwd().split('/internal_tests')[0] sys.path.append(METPLUS_BASE+'/ush/plotting_scripts') import plot_util logger = logging.getLogger('~/metplus_pytest_plot_util.log') + +@pytest.mark.plotting def test_get_date_arrays(): # Independently test the creation of # the date arrays, one used for plotting @@ -209,6 +184,8 @@ def test_get_date_arrays(): assert(test_expected_stat_file_dates[l] == expected_expected_stat_file_dates[l]) + +@pytest.mark.plotting def test_format_thresh(): # Independently test the formatting # of thresholds @@ -297,6 +274,8 @@ def test_format_thresh(): assert(test_thresh_symbol == expected_thresh_symbol) assert(test_thresh_letter == expected_thresh_letter) + +@pytest.mark.plotting def test_get_stat_file_base_columns(): # Independently test getting list # of the base MET version .stat file columns @@ -332,6 +311,8 @@ def test_get_stat_file_base_columns(): ) assert(test_stat_file_base_columns == expected_stat_file_base_columns) + +@pytest.mark.plotting def test_get_stat_file_line_type_columns(): # Independently test getting list # of the line type MET version .stat file columns @@ -441,6 +422,8 @@ def test_get_stat_file_line_type_columns(): assert(test_stat_file_line_type_columns == expected_stat_file_line_type_columns) + +@pytest.mark.plotting def get_clevels(): # Independently test creating an array # of levels centered about 0 to plot @@ -453,6 +436,8 @@ def get_clevels(): test_clevels = plot_util.get_clevels(data) assert(test_clevels == expected_clevels) + +@pytest.mark.plotting def test_calculate_average(): # Independently test getting the average # of a data array based on method @@ -558,7 +543,9 @@ def test_calculate_average(): assert(len(test_average_array) == len(expected_average_array)) for l in range(len(test_average_array)): assert(round(test_average_array[l],6) == expected_average_array[l]) - + + +@pytest.mark.long def test_calculate_ci(): pytest.skip("Takes far too long to run") # Independently test getting the @@ -691,6 +678,8 @@ def test_calculate_ci(): stat, average_method, randx) assert(test_intvl == expected_intvl) + +@pytest.mark.plotting def test_get_stat_plot_name(): # Independently test getting the # a more formalized statistic name @@ -730,6 +719,8 @@ def test_get_stat_plot_name(): test_stat_plot_name = plot_util.get_stat_plot_name(logger, stat) assert(test_stat_plot_name == expected_stat_plot_name) + +@pytest.mark.plotting def test_calculate_stat(): # Independently test calculating # statistic values diff --git a/internal_tests/pytests/tcmpr_plotter/test_tcmpr_plotter.py b/internal_tests/pytests/plotting/tcmpr_plotter/test_tcmpr_plotter.py similarity index 99% rename from internal_tests/pytests/tcmpr_plotter/test_tcmpr_plotter.py rename to internal_tests/pytests/plotting/tcmpr_plotter/test_tcmpr_plotter.py index e50c64ce2..519bcbb94 100644 --- a/internal_tests/pytests/tcmpr_plotter/test_tcmpr_plotter.py +++ b/internal_tests/pytests/plotting/tcmpr_plotter/test_tcmpr_plotter.py @@ -1,12 +1,8 @@ #!/usr/bin/env python3 -import os -import sys -import re import pytest -from datetime import datetime -import produtil +import os from metplus.wrappers.tcmpr_plotter_wrapper import TCMPRPlotterWrapper @@ -18,6 +14,7 @@ TIME_FMT = '%Y%m%d%H' RUN_TIME = '20141214' + def set_minimum_config_settings(config): # set config variables to prevent command from running and bypass check # if input files actually exist @@ -37,6 +34,7 @@ def set_minimum_config_settings(config): config.set('config', 'TCMPR_PLOTTER_PLOT_OUTPUT_DIR', '{OUTPUT_BASE}/TCMPRPlotter/tcmpr_plots') + @pytest.mark.parametrize( 'config_overrides,expected_loop_args', [ # 0: no loop args @@ -99,6 +97,7 @@ def set_minimum_config_settings(config): 'plot': [('pitem1', 'P Label 1'), ('pitem2', 'P Label 2')]}), ] ) +@pytest.mark.plotting def test_read_loop_info(metplus_config, config_overrides, expected_loop_args): config = metplus_config() @@ -111,6 +110,7 @@ def test_read_loop_info(metplus_config, config_overrides, expected_loop_args): wrapper = TCMPRPlotterWrapper(config) assert wrapper.read_loop_info() == expected_loop_args + @pytest.mark.parametrize( 'config_overrides,expected_strings', [ # 0: no optional arguments @@ -178,7 +178,7 @@ def test_read_loop_info(metplus_config, config_overrides, expected_loop_args): '-dep ditem1 -plot pitem1')]), ] ) - +@pytest.mark.plotting def test_tcmpr_plotter_loop(metplus_config, config_overrides, expected_strings): config = metplus_config() @@ -271,7 +271,7 @@ def test_tcmpr_plotter_loop(metplus_config, config_overrides, ({'TCMPR_PLOTTER_PLOT_TYPES': 'item1'}, '-plot item1'), ] ) - +@pytest.mark.plotting def test_tcmpr_plotter(metplus_config, config_overrides, expected_string): # add a space before value if expected string has a value if expected_string: diff --git a/internal_tests/pytests/produtil/README b/internal_tests/pytests/produtil/README deleted file mode 100644 index 540ba31e6..000000000 --- a/internal_tests/pytests/produtil/README +++ /dev/null @@ -1,23 +0,0 @@ -The test_produtil.py provides some simple tests for the produtil module. - -To run the test: - -1) cd to the directory METplus/internal_tests/pytest/produtil - -2) open the test_produtil.py file and replace the '/path/to' with the full path to the directory where your produtil_test.conf file -resides (this will be in METplus/internal_tests/pytest/produtil). - -NOTE: This is necessary, as we are NOT using run_metplus.py to begin the process of reading in the config -file, test_produtil.conf - - -2) Then, from the command line, enter the following command: - - pytest -c ./produtil_test.conf - -There are currently 9 tests and they should pass. - - - - - diff --git a/internal_tests/pytests/produtil/produtil_test.conf b/internal_tests/pytests/produtil/produtil_test.conf deleted file mode 100644 index 8d3b0b84b..000000000 --- a/internal_tests/pytests/produtil/produtil_test.conf +++ /dev/null @@ -1,31 +0,0 @@ - -# Test configuration for METplus produtil -[config] -STRING_VALUE = someStringValue!#@$% -INT_VALUE = 2908887 -RAW_VALUE = GRIB_lvl_type = 100 -BOOL_VALUE = True -NEW_LINES = very long line requiring newline character to be tested 12345 - 67890 end of the line. -UNASSIGNED_VALUE = -JOB_LIST = -job filter -dump_row {PROJ_DIR}/dump_file.out -job summary by AMAX_WIND -job summary 'ABS(AMAX_WIND-BMAX_WIND)' -out {OUTPUT_BASE}/max_wind_delta.tcst -JOBS = -job summary -by AMODEL,LEAD -column AMSLP -column BMSLP -column 'ABS(AMSLP-BMSLP)' -out {OUTPUT_BASE}/tc_stat_summary.out - -[dir] -# set in the metplus_data.conf to /path/to, override here for testing -PROJ_DIR = /tmp/produtil_testing - -# set in the metplus_system.conf to /path/to, override here for testing, set to -# appropriate version of MET -MET_INSTALL_DIR = /usr/local/met-8.1 -METPLUS_BASE = /usr/local/met-8.1 -OUTPUT_BASE = /tmp/produtil_testing/out -TMP_DIR = /tmp/produtil_testing/tmp - -# Used for testing -DIR_VALUE = /tmp/some_dir -BASE_DIR = /tmp -SPECIFIC_DIR = {BASE_DIR}/specific_place - -[exe] -WGRIB2 = wgrib2 diff --git a/internal_tests/pytests/produtil/test_produtil.py b/internal_tests/pytests/produtil/test_produtil.py deleted file mode 100644 index c5e816e74..000000000 --- a/internal_tests/pytests/produtil/test_produtil.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import produtil.setup -import sys -import logging -import pytest -from shutil import which - -from metplus.util import met_util as util - -# -# These are tests (not necessarily unit tests) for the -# MET Point-Stat Wrapper, PointStatWrapper.py -# NOTE: This test requires pytest, which is NOT part of the standard Python -# library. -# These tests require one configuration file in addition to the three -# required METplus configuration files: point_stat_test.conf. This contains -# the information necessary for running all the tests. Each test can be -# customized to replace various settings if needed. -# - -# -# -----------Mandatory----------- -# configuration and fixture to support METplus configuration files beyond -# the metplus_data, metplus_system, and metplus_runtime conf files. -# - - -# Add a test configuration -def pytest_addoption(parser): - parser.addoption("-c", action="store", help=" -c ") - - -# @pytest.fixture -def cmdopt(request): - return request.config.getoption("-c") - - -# ------------------------ -def dummy(): - assert(True) - -def get_config_obj(metplus_config): - """! Create the configuration object that is used by all tests""" - file_list = ["/path/to/METplus/internal_tests/pytests/produtil"] - extra_configs = [] - extra_configs.append(os.path.join(os.path.dirname(__file__), 'produtil_test.conf')) - config = metplus_config(extra_configs) - - return config - - -def test_getstr_ok(metplus_config): - """! Test that the expected string is retrieved via produtil's getstr - method - """ - conf_obj = get_config_obj(metplus_config) - str_value = conf_obj.getstr('config', 'STRING_VALUE') - expected_str_value = "someStringValue!#@$%" - assert str_value == expected_str_value - - -def test_getint_ok(metplus_config): - """! Test that the expected int in the produtil_test.conf file has been - retrieved correctly. - """ - conf_obj = get_config_obj(metplus_config) - expected_int_value = int(2908887) - int_value = conf_obj.getint('config', 'INT_VALUE') - assert int_value == expected_int_value - - - -def test_getdir_ok(metplus_config): - """! Test that the directory in the produtil_test.conf file has been - correctly retrieved. - """ - conf_obj = get_config_obj(metplus_config) - expected_dir = "/tmp/some_dir" - dir_retrieved = conf_obj.getdir('DIR_VALUE') - assert dir_retrieved == expected_dir - - -def test_getdir_compound_ok(metplus_config): - """! Test that directories created from other directories, ie. - BASE_DIR = /base/dir - SPECIFIC_DIR = {BASE_DIR}/specific/dir - - correctly returns the directory path for SPECIFIC_DIR - """ - expected_specific_dir = "/tmp/specific_place" - conf_obj = get_config_obj(metplus_config) - specific_dir = conf_obj.getdir('SPECIFIC_DIR') - assert specific_dir == expected_specific_dir - - -def test_no_value_as_string(metplus_config): - """! Tests that a key with no value returns an empty string.""" - - conf_obj = get_config_obj(metplus_config) - expected_unassigned = '' - unassigned = conf_obj.getstr('config', 'UNASSIGNED_VALUE') - print("unassigned: ", unassigned) - print("expected: ", expected_unassigned) - assert unassigned == expected_unassigned - - -def test_no_value_as_list(metplus_config): - """! Tests that a key with no list of strings returns an empty list.""" - - conf_obj = get_config_obj(metplus_config) - expected_unassigned = [] - unassigned = util.getlist(conf_obj.getstr('config', 'UNASSIGNED_VALUE')) - assert unassigned == expected_unassigned - - -def test_new_lines_in_conf(metplus_config): - """! Test that any newlines in the configuration file are handled - properly - """ - - conf_obj = get_config_obj(metplus_config) - expected_string = \ - "very long line requiring newline character to be tested 12345\n67890 end of the line." - long_line = conf_obj.getstr('config', 'NEW_LINES') - assert long_line == expected_string - - -def test_get_exe_ok(metplus_config): - """! Test that executables are correctly retrieved.""" - conf_obj = get_config_obj(metplus_config) - expected_exe = which('wgrib2') - executable = conf_obj.getexe('WGRIB2') - assert executable == expected_exe - - -def test_get_bool(metplus_config): - """! Test that boolean values are correctly retrieved.""" - conf_obj = get_config_obj(metplus_config) - bool_val = conf_obj.getbool('config', 'BOOL_VALUE') - assert bool_val is True - diff --git a/internal_tests/pytests/produtil/work_in_progress_test_produtil_regression.py b/internal_tests/pytests/produtil/work_in_progress_test_produtil_regression.py deleted file mode 100644 index 580e376cc..000000000 --- a/internal_tests/pytests/produtil/work_in_progress_test_produtil_regression.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import produtil -import sys -import logging -import pytest -import config_metplus -import config_launcher as launcher -import met_util as util - - -# -# These are tests (not necessarily unit tests) for the -# MET Point-Stat Wrapper, PointStatWrapper.py -# NOTE: This test requires pytest, which is NOT part of the standard Python -# library. -# These tests require one configuration file in addition to the three -# required METplus configuration files: point_stat_test.conf. This contains -# the information necessary for running all the tests. Each test can be -# customized to replace various settings if needed. -# - -# -# -----------Mandatory----------- -# configuration and fixture to support METplus configuration files beyond -# the metplus_data, metplus_system, and metplus_runtime conf files. -# - - -# Add a test configuration -# def pytest_addoption(parser): -# parser.addoption("-c", action="store", help=" -c ") -# -# -# # @pytest.fixture -# def cmdopt(request): -# return request.config.getoption("-c") - - -# ------------------------ - -def dummy(): - assert(True) - - -def get_config_obj(): - """! Create the configuration object that is used by all tests""" - file_list = ["METplus/internal_tests/pytests/produtil"] - config_obj = config_metplus.setup(file_list[0]) - - return config_obj - - -def test_getstr_ok(regtest): - """! Test that the expected string is retrieved via produtil's getstr - method - """ - conf_obj = get_config_obj() - str_value = conf_obj.getstr('config', 'STRING_VALUE') - expected_str_value = "someStringValue!#@$%" - # print(str_value, file=regtest) - regtest.write("done") -# -# -# def test_getint_ok(regtest): -# """! Test that the expected int in the produtil_test.conf file has been -# retrieved correctly. -# """ -# conf_obj = get_config_obj() -# expected_int_value = int(2908887) -# int_value = conf_obj.getint('config', 'INT_VALUE') -# # print(int_value, file=regtest) -# regtest.write("done") -# -# -# def test_getraw_ok(regtest): -# """! Test that the raw value in the produtil_test.conf file has been -# retrieved correctly. -# """ -# conf_obj = get_config_obj() -# expected_raw = 'GRIB_lvl_type = 100' -# raw_value = conf_obj.getraw('config', 'RAW_VALUE') -# # print(raw_value, file=regtest) -# # regtest.write("done") -# -# -# def test_getdir_ok(regtest): -# """! Test that the directory in the produtil_test.conf file has been -# correctly retrieved. -# """ -# conf_obj = get_config_obj() -# expected_dir = "/tmp/some_dir" -# dir_retrieved = conf_obj.getdir('DIR_VALUE') -# # print(dir_retrieved, file=regtest) -# -# -# def test_getdir_compound_ok(regtest): -# """! Test that directories created from other directories, ie. -# BASE_DIR = /base/dir -# SPECIFIC_DIR = {BASE_DIR}/specific/dir -# -# correctly returns the directory path for SPECIFIC_DIR -# """ -# expected_specific_dir = "/tmp/specific_place" -# conf_obj = get_config_obj() -# specific_dir = conf_obj.getdir('SPECIFIC_DIR') -# print(specific_dir, file=regtest) -# -# -# def test_no_value_as_string(regtest): -# """! Tests that a key with no value returns an empty string.""" -# -# conf_obj = get_config_obj() -# expected_unassigned = '' -# unassigned = conf_obj.getstr('config', 'UNASSIGNED_VALUE') -# # print(unassigned, file=regtest) -# -# -# def test_no_value_as_list(regtest): -# """! Tests that a key with no list of strings returns an empty list.""" -# -# conf_obj = get_config_obj() -# expected_unassigned = [] -# unassigned = util.getlist(conf_obj.getstr('config', 'UNASSIGNED_VALUE')) -# assert unassigned == expected_unassigned -# # print(unassigned, file=regtest) -# -# -# def test_new_lines_in_conf(regtest): -# """! Test that any newlines in the configuration file are handled -# properly -# """ -# -# conf_obj = get_config_obj() -# expected_string = \ -# "very long line requiring newline character to be tested 12345\n67890 end of the line." -# long_line = conf_obj.getstr('config', 'NEW_LINES') -# assert long_line == expected_string -# # print(long_line, file=regtest) -# -# -# def test_get_exe_ok(regtest): -# """! Test that executables are correctly retrieved.""" -# conf_obj = get_config_obj() -# expected_exe = '/usr/local/bin/wgrib2' -# executable = conf_obj.getexe('WGRIB2') -# assert executable == expected_exe -# # print(executable, file=regtest) -# -# -# def test_get_bool(regtest): -# """! Test that boolean values are correctly retrieved.""" -# conf_obj = get_config_obj() -# bool_val = conf_obj.getbool('config', 'BOOL_VALUE') -# assert bool_val is True -# # print(bool_val, file=regtest) -# diff --git a/internal_tests/pytests/pytest.ini b/internal_tests/pytests/pytest.ini new file mode 100644 index 000000000..8630509ec --- /dev/null +++ b/internal_tests/pytests/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +markers = + util: custom marker for testing metplus/util logic + wrapper_a: custom marker for testing metplus/wrapper logic - A group + wrapper_b: custom marker for testing metplus/wrapper logic - B group + wrapper_c: custom marker for testing metplus/wrapper logic - C group + wrapper: custom marker for testing metplus/wrapper logic - all others + long: custom marker for tests that take a long time to run + plotting: custom marker for tests that involve plotting diff --git a/internal_tests/pytests/config/config_1.conf b/internal_tests/pytests/util/config/config_1.conf similarity index 100% rename from internal_tests/pytests/config/config_1.conf rename to internal_tests/pytests/util/config/config_1.conf diff --git a/internal_tests/pytests/config/config_2.conf b/internal_tests/pytests/util/config/config_2.conf similarity index 100% rename from internal_tests/pytests/config/config_2.conf rename to internal_tests/pytests/util/config/config_2.conf diff --git a/internal_tests/pytests/config/config_3.conf b/internal_tests/pytests/util/config/config_3.conf similarity index 100% rename from internal_tests/pytests/config/config_3.conf rename to internal_tests/pytests/util/config/config_3.conf diff --git a/internal_tests/pytests/config/test_config.py b/internal_tests/pytests/util/config/test_config.py similarity index 89% rename from internal_tests/pytests/config/test_config.py rename to internal_tests/pytests/util/config/test_config.py index a18678af3..7c054ab3d 100644 --- a/internal_tests/pytests/config/test_config.py +++ b/internal_tests/pytests/util/config/test_config.py @@ -1,16 +1,14 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -import sys import pytest -import datetime + import os from configparser import NoOptionError from shutil import which -import produtil - from metplus.util import met_util as util + @pytest.mark.parametrize( 'input_value, result', [ (3600, 3600), @@ -28,6 +26,7 @@ (None, None), ] ) +@pytest.mark.util def test_getseconds(metplus_config, input_value, result): conf = metplus_config() if input_value is not None: @@ -35,10 +34,11 @@ def test_getseconds(metplus_config, input_value, result): try: seconds = conf.getseconds('config', 'TEST_SECONDS') - assert(seconds == result) + assert seconds == result except NoOptionError: if result is None: - assert(True) + assert True + # value = None -- config variable not set @pytest.mark.parametrize( @@ -55,6 +55,7 @@ def test_getseconds(metplus_config, input_value, result): (None, '1', '1'), ] ) +@pytest.mark.util def test_getstr(metplus_config, input_value, default, result): conf = metplus_config() if input_value is not None: @@ -62,10 +63,11 @@ def test_getstr(metplus_config, input_value, default, result): # catch NoOptionError exception and pass test if default is None try: - assert(result == conf.getstr('config', 'TEST_GETSTR', default)) + assert result == conf.getstr('config', 'TEST_GETSTR', default) except NoOptionError: if default is None: - assert(True) + assert True + # value = None -- config variable not set @pytest.mark.parametrize( @@ -78,6 +80,7 @@ def test_getstr(metplus_config, input_value, default, result): ] ) +@pytest.mark.util def test_getdir(metplus_config, input_value, default, result): conf = metplus_config() if input_value is not None: @@ -85,13 +88,14 @@ def test_getdir(metplus_config, input_value, default, result): # catch NoOptionError exception and pass test if default is None try: - assert(result == conf.getdir('TEST_GETSTR', default=default)) + assert result == conf.getdir('TEST_GETSTR', default=default) except NoOptionError: if result is 'NoOptionError': - assert(True) + assert True except ValueError: if result is 'ValueError': - assert(True) + assert True + # value = None -- config variable not set @pytest.mark.parametrize( @@ -104,6 +108,7 @@ def test_getdir(metplus_config, input_value, default, result): ('{valid?fmt=%Y%m%d}_{NOT_REAL_VAR}', None, '{valid?fmt=%Y%m%d}_{NOT_REAL_VAR}'), ] ) +@pytest.mark.util def test_getraw(metplus_config, input_value, default, result): conf = metplus_config() conf.set('config', 'TEST_EXTRA', 'extra') @@ -112,7 +117,7 @@ def test_getraw(metplus_config, input_value, default, result): if input_value is not None: conf.set('config', 'TEST_GETRAW', input_value) - assert(result == conf.getraw('config', 'TEST_GETRAW', default=default)) + assert result == conf.getraw('config', 'TEST_GETRAW', default=default) # value = None -- config variable not set @@ -137,6 +142,7 @@ def test_getraw(metplus_config, input_value, default, result): (None, None, None), ] ) +@pytest.mark.util def test_getbool(metplus_config, input_value, default, result): conf = metplus_config() if input_value is not None: @@ -144,10 +150,11 @@ def test_getbool(metplus_config, input_value, default, result): # catch NoOptionError exception and pass test if default is None try: - assert(result == conf.getbool('config', 'TEST_GETBOOL', default)) + assert result == conf.getbool('config', 'TEST_GETBOOL', default) except NoOptionError: if result is None: - assert(True) + assert True + # value = None -- config variable not set @pytest.mark.parametrize( @@ -158,12 +165,13 @@ def test_getbool(metplus_config, input_value, default, result): ('sh', which('sh')), ] ) +@pytest.mark.util def test_getexe(metplus_config, input_value, result): conf = metplus_config() if input_value is not None: conf.set('config', 'TEST_GETEXE', input_value) - assert(result == conf.getexe('TEST_GETEXE')) + assert result == conf.getexe('TEST_GETEXE') # value = None -- config variable not set @pytest.mark.parametrize( @@ -186,10 +194,11 @@ def test_getfloat(metplus_config, input_value, default, result): conf.set('config', 'TEST_GETFLOAT', input_value) try: - assert(result == conf.getfloat('config', 'TEST_GETFLOAT', default)) + assert result == conf.getfloat('config', 'TEST_GETFLOAT', default) except ValueError: if result is None: - assert(True) + assert True + # value = None -- config variable not set @pytest.mark.parametrize( @@ -209,16 +218,18 @@ def test_getfloat(metplus_config, input_value, default, result): ('', 2.2, util.MISSING_DATA_VALUE), ] ) +@pytest.mark.util def test_getint(metplus_config, input_value, default, result): conf = metplus_config() if input_value is not None: conf.set('config', 'TEST_GETINT', input_value) try: - assert(result == conf.getint('config', 'TEST_GETINT', default)) + assert result == conf.getint('config', 'TEST_GETINT', default) except ValueError: if result is None: - assert(True) + assert True + @pytest.mark.parametrize( 'config_key, expected_result', [ @@ -229,6 +240,7 @@ def test_getint(metplus_config, input_value, default, result): ('VAR_TO_TEST_A', 'A3'), ] ) +@pytest.mark.util def test_move_all_to_config_section(metplus_config, config_key, expected_result): config_files = ['config_1.conf', 'config_2.conf', @@ -237,7 +249,8 @@ def test_move_all_to_config_section(metplus_config, config_key, expected_result) test_dir = os.path.dirname(__file__) config_files = [os.path.join(test_dir, item) for item in config_files] config = metplus_config(config_files) - assert(config.getstr('config', config_key) == expected_result) + assert config.getstr('config', config_key) == expected_result + @pytest.mark.parametrize( 'overrides, config_key, expected_result', [ @@ -266,10 +279,12 @@ def test_move_all_to_config_section(metplus_config, config_key, expected_result) 'CMD_LINE_1', '2'), ] ) +@pytest.mark.util def test_move_all_to_config_section_cmd_line(metplus_config, overrides, config_key, expected_result): config = metplus_config(overrides) - assert(config.getstr('config', config_key, '') == expected_result) + assert config.getstr('config', config_key, '') == expected_result + @pytest.mark.parametrize( 'config_name, expected_result', [ @@ -314,6 +329,7 @@ def test_move_all_to_config_section_cmd_line(metplus_config, overrides, ), ] ) +@pytest.mark.util def test_getraw_nested_curly_braces(metplus_config, config_name, expected_result): @@ -323,4 +339,4 @@ def test_getraw_nested_curly_braces(metplus_config, config_files = [os.path.join(test_dir, item) for item in config_files] config = metplus_config(config_files) sec, name = config_name.split('.', 1) - assert(config.getraw(sec, name) == expected_result) + assert config.getraw(sec, name) == expected_result diff --git a/internal_tests/pytests/config_metplus/test_config_metplus.py b/internal_tests/pytests/util/config_metplus/test_config_metplus.py similarity index 95% rename from internal_tests/pytests/config_metplus/test_config_metplus.py rename to internal_tests/pytests/util/config_metplus/test_config_metplus.py index f0e669443..07a32655f 100644 --- a/internal_tests/pytests/config_metplus/test_config_metplus.py +++ b/internal_tests/pytests/util/config_metplus/test_config_metplus.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 import pytest + import pprint import os from datetime import datetime from metplus.util import config_metplus + +@pytest.mark.util def test_get_default_config_list(): test_data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, os.pardir, 'data', @@ -37,6 +41,7 @@ def test_get_default_config_list(): assert actual_new == expected_new assert actual_both == expected_both + @pytest.mark.parametrize( 'regex,index,id,expected_result', [ # 0: No ID @@ -64,6 +69,7 @@ def test_get_default_config_list(): '2': ['NAME', 'MEMBERS', 'REQUIRED', 'MIN_REQ']}), ] ) +@pytest.mark.util def test_find_indices_in_config_section(metplus_config, regex, index, id, expected_result): config = metplus_config() @@ -83,7 +89,6 @@ def test_find_indices_in_config_section(metplus_config, regex, index, config.set('config', 'TC_PAIRS_CONSENSUS2_REQUIRED', 'True') config.set('config', 'TC_PAIRS_CONSENSUS2_MIN_REQ', '2') - indices = config_metplus.find_indices_in_config_section(regex, config, index_index=index, id_index=id) @@ -94,6 +99,7 @@ def test_find_indices_in_config_section(metplus_config, regex, index, assert indices == expected_result + @pytest.mark.parametrize( 'conf_items, met_tool, expected_result', [ ({'CUSTOM_LOOP_LIST': "one, two, three"}, '', ['one', 'two', 'three']), @@ -110,12 +116,14 @@ def test_find_indices_in_config_section(metplus_config, regex, index, 'POINT2GRID_CUSTOM_LOOP_LIST': "four, five",}, 'point2grid', ['four', 'five']), ] ) +@pytest.mark.util def test_get_custom_string_list(metplus_config, conf_items, met_tool, expected_result): config = metplus_config() for conf_key, conf_value in conf_items.items(): config.set('config', conf_key, conf_value) - assert(config_metplus.get_custom_string_list(config, met_tool) == expected_result) + assert config_metplus.get_custom_string_list(config, met_tool) == expected_result + @pytest.mark.parametrize( 'config_var_name, expected_indices, set_met_tool', [ @@ -133,6 +141,7 @@ def test_get_custom_string_list(metplus_config, conf_items, met_tool, expected_r ('BOTH_VAR12_FIELD_NAME', ['12'], False), ] ) +@pytest.mark.util def test_find_var_indices_fcst(metplus_config, config_var_name, expected_indices, @@ -145,9 +154,10 @@ def test_find_var_indices_fcst(metplus_config, data_types=data_types, met_tool=met_tool) - assert(len(var_name_indices) == len(expected_indices)) + assert len(var_name_indices) == len(expected_indices) for actual_index in var_name_indices: - assert(actual_index in expected_indices) + assert actual_index in expected_indices + @pytest.mark.parametrize( 'data_type, met_tool, expected_out', [ @@ -172,10 +182,12 @@ def test_find_var_indices_fcst(metplus_config, ] ) +@pytest.mark.util def test_get_field_search_prefixes(data_type, met_tool, expected_out): assert(config_metplus.get_field_search_prefixes(data_type, met_tool) == expected_out) + @pytest.mark.parametrize( 'item_list, extension, is_valid', [ (['FCST'], 'NAME', False), @@ -215,9 +227,11 @@ def test_get_field_search_prefixes(data_type, met_tool, expected_out): ] ) +@pytest.mark.util def test_is_var_item_valid(metplus_config, item_list, extension, is_valid): conf = metplus_config() - assert(config_metplus.is_var_item_valid(item_list, '1', extension, conf)[0] == is_valid) + assert config_metplus.is_var_item_valid(item_list, '1', extension, conf)[0] == is_valid + @pytest.mark.parametrize( 'item_list, configs_to_set, is_valid', [ @@ -256,12 +270,14 @@ def test_is_var_item_valid(metplus_config, item_list, extension, is_valid): ] ) +@pytest.mark.util def test_is_var_item_valid_levels(metplus_config, item_list, configs_to_set, is_valid): conf = metplus_config() for key, value in configs_to_set.items(): conf.set('config', key, value) - assert(config_metplus.is_var_item_valid(item_list, '1', 'LEVELS', conf)[0] == is_valid) + assert config_metplus.is_var_item_valid(item_list, '1', 'LEVELS', conf)[0] == is_valid + # search prefixes are valid prefixes to append to field info variables # config_overrides are a dict of config vars and their values @@ -300,6 +316,7 @@ def test_is_var_item_valid_levels(metplus_config, item_list, configs_to_set, is_ ] ) +@pytest.mark.util def test_get_field_config_variables(metplus_config, search_prefixes, config_overrides, @@ -317,7 +334,8 @@ def test_get_field_config_variables(metplus_config, index, search_prefixes) - assert(field_configs.get(field_info_type) == expected_value) + assert field_configs.get(field_info_type) == expected_value + @pytest.mark.parametrize( 'config_keys, field_key, expected_value', [ @@ -365,6 +383,7 @@ def test_get_field_config_variables(metplus_config, ([], 'output_names', None), ] ) +@pytest.mark.util def test_get_field_config_variables_synonyms(metplus_config, config_keys, field_key, @@ -379,7 +398,8 @@ def test_get_field_config_variables_synonyms(metplus_config, index, [prefix]) - assert(field_configs.get(field_key) == expected_value) + assert field_configs.get(field_key) == expected_value + # field info only defined in the FCST_* variables @pytest.mark.parametrize( @@ -389,6 +409,7 @@ def test_get_field_config_variables_synonyms(metplus_config, ('OBS', False), ] ) +@pytest.mark.util def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): conf = metplus_config() conf.set('config', 'FCST_VAR1_NAME', "NAME1") @@ -398,7 +419,7 @@ def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): # this should not occur because OBS variables are missing if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) @@ -414,7 +435,8 @@ def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): var_list[2]['fcst_level'] == "LEVELS21" and \ var_list[3]['fcst_level'] == "LEVELS22") else: - assert(not var_list) + assert not var_list + # field info only defined in the OBS_* variables @pytest.mark.parametrize( @@ -424,6 +446,7 @@ def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): ('FCST', False), ] ) +@pytest.mark.util def test_parse_var_list_obs(metplus_config, data_type, list_created): conf = metplus_config() conf.set('config', 'OBS_VAR1_NAME', "NAME1") @@ -433,7 +456,7 @@ def test_parse_var_list_obs(metplus_config, data_type, list_created): # this should not occur because FCST variables are missing if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) @@ -449,7 +472,7 @@ def test_parse_var_list_obs(metplus_config, data_type, list_created): var_list[2]['obs_level'] == "LEVELS21" and \ var_list[3]['obs_level'] == "LEVELS22") else: - assert(not var_list) + assert not var_list # field info only defined in the BOTH_* variables @@ -460,6 +483,7 @@ def test_parse_var_list_obs(metplus_config, data_type, list_created): ('OBS', 'obs'), ] ) +@pytest.mark.util def test_parse_var_list_both(metplus_config, data_type, list_created): conf = metplus_config() conf.set('config', 'BOTH_VAR1_NAME', "NAME1") @@ -469,7 +493,7 @@ def test_parse_var_list_both(metplus_config, data_type, list_created): # this should not occur because BOTH variables are used if not config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) print(f'var_list:{var_list}') @@ -482,9 +506,11 @@ def test_parse_var_list_both(metplus_config, data_type, list_created): not var_list[1][f'{list_to_check}_level'] == "LEVELS12" or \ not var_list[2][f'{list_to_check}_level'] == "LEVELS21" or \ not var_list[3][f'{list_to_check}_level'] == "LEVELS22": - assert(False) + assert False + # field info defined in both FCST_* and OBS_* variables +@pytest.mark.util def test_parse_var_list_fcst_and_obs(metplus_config): conf = metplus_config() conf.set('config', 'FCST_VAR1_NAME', "FNAME1") @@ -498,7 +524,7 @@ def test_parse_var_list_fcst_and_obs(metplus_config): # this should not occur because FCST and OBS variables are found if not config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf) @@ -519,7 +545,9 @@ def test_parse_var_list_fcst_and_obs(metplus_config): var_list[3]['fcst_level'] == "FLEVELS22" and \ var_list[3]['obs_level'] == "OLEVELS22") + # VAR1 defined by FCST, VAR2 defined by OBS +@pytest.mark.util def test_parse_var_list_fcst_and_obs_alternate(metplus_config): conf = metplus_config() conf.set('config', 'FCST_VAR1_NAME', "FNAME1") @@ -530,6 +558,7 @@ def test_parse_var_list_fcst_and_obs_alternate(metplus_config): # configuration is invalid and parse var list should not give any results assert(not config_metplus.validate_configuration_variables(conf, force_check=True)[1] and not config_metplus.parse_var_list(conf)) + # VAR1 defined by OBS, VAR2 by FCST, VAR3 by both FCST AND OBS @pytest.mark.parametrize( 'data_type, list_len, name_levels', [ @@ -538,6 +567,7 @@ def test_parse_var_list_fcst_and_obs_alternate(metplus_config): ('OBS', 4, ('ONAME1:OLEVELS11','ONAME1:OLEVELS12','ONAME3:OLEVELS31','ONAME3:OLEVELS32')), ] ) +@pytest.mark.util def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_len, name_levels): conf = metplus_config() conf.set('config', 'OBS_VAR1_NAME', "ONAME1") @@ -551,15 +581,15 @@ def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_le # configuration is invalid and parse var list should not give any results if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) if len(var_list) != list_len: - assert(False) + assert False if data_type is None: - assert(len(var_list) == 0) + assert len(var_list) == 0 if name_levels is not None: dt_lower = data_type.lower() @@ -571,12 +601,13 @@ def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_le for expect, reality in zip(expected,var_list): if expect[f'{dt_lower}_name'] != reality[f'{dt_lower}_name']: - assert(False) + assert False if expect[f'{dt_lower}_level'] != reality[f'{dt_lower}_level']: - assert(False) + assert False + + assert True - assert(True) # option defined in obs only @pytest.mark.parametrize( @@ -586,6 +617,7 @@ def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_le ('OBS', 0), ] ) +@pytest.mark.util def test_parse_var_list_fcst_only_options(metplus_config, data_type, list_len): conf = metplus_config() conf.set('config', 'FCST_VAR1_NAME', "NAME1") @@ -595,11 +627,12 @@ def test_parse_var_list_fcst_only_options(metplus_config, data_type, list_len): # this should not occur because OBS variables are missing if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) + assert False var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) - assert(len(var_list) == list_len) + assert len(var_list) == list_len + @pytest.mark.parametrize( 'met_tool, indices', [ @@ -608,6 +641,7 @@ def test_parse_var_list_fcst_only_options(metplus_config, data_type, list_len): ('ENSEMBLE_STAT', {}), ] ) +@pytest.mark.util def test_find_var_indices_wrapper_specific(metplus_config, met_tool, indices): conf = metplus_config() data_type = 'FCST' @@ -617,11 +651,13 @@ def test_find_var_indices_wrapper_specific(metplus_config, met_tool, indices): var_name_indices = config_metplus.find_var_name_indices(conf, data_types=[data_type], met_tool=met_tool) - assert(var_name_indices == indices) + assert var_name_indices == indices + # ensure that the field configuration used for # met_tool_wrapper/EnsembleStat/EnsembleStat.conf # works as expected +@pytest.mark.util def test_parse_var_list_ensemble(metplus_config): config = metplus_config() config.set('config', 'ENS_VAR1_NAME', 'APCP') @@ -704,13 +740,15 @@ def test_parse_var_list_ensemble(metplus_config): assert(len(ensemble_var_list) == len(expected_ens_list)) for actual_ens, expected_ens in zip(ensemble_var_list, expected_ens_list): for key, value in expected_ens.items(): - assert(actual_ens.get(key) == value) + assert actual_ens.get(key) == value assert(len(var_list) == len(expected_var_list)) for actual_var, expected_var in zip(var_list, expected_var_list): for key, value in expected_var.items(): - assert(actual_var.get(key) == value) + assert actual_var.get(key) == value + +@pytest.mark.util def test_parse_var_list_series_by(metplus_config): config = metplus_config() config.set('config', 'BOTH_EXTRACT_TILES_VAR1_NAME', 'RH') @@ -768,16 +806,18 @@ def test_parse_var_list_series_by(metplus_config): print(f'SeriesAnalysis var list:') pp.pprint(actual_sa_list) - assert(len(actual_et_list) == len(expected_et_list)) + assert len(actual_et_list) == len(expected_et_list) for actual_et, expected_et in zip(actual_et_list, expected_et_list): for key, value in expected_et.items(): - assert(actual_et.get(key) == value) + assert actual_et.get(key) == value assert(len(actual_sa_list) == len(expected_sa_list)) for actual_sa, expected_sa in zip(actual_sa_list, expected_sa_list): for key, value in expected_sa.items(): - assert(actual_sa.get(key) == value) + assert actual_sa.get(key) == value + +@pytest.mark.util def test_parse_var_list_priority_fcst(metplus_config): priority_list = ['FCST_GRID_STAT_VAR1_NAME', 'FCST_GRID_STAT_VAR1_INPUT_FIELD_NAME', @@ -806,13 +846,15 @@ def test_parse_var_list_priority_fcst(metplus_config): data_type='FCST', met_tool='grid_stat') - assert(len(var_list) == 1) - assert(var_list[0].get('fcst_name') == priority_list[0].lower()) + assert len(var_list) == 1 + assert var_list[0].get('fcst_name') == priority_list[0].lower() priority_list.pop(0) + # test that if wrapper specific field info is specified, it only gets # values from that list. All generic values should be read if no # wrapper specific field info variables are specified +@pytest.mark.util def test_parse_var_list_wrapper_specific(metplus_config): conf = metplus_config() conf.set('config', 'FCST_VAR1_NAME', "ENAME1") @@ -846,6 +888,7 @@ def test_parse_var_list_wrapper_specific(metplus_config): g_var_list[0]['fcst_level'] == "GLEVELS11" and g_var_list[1]['fcst_level'] == "GLEVELS12") + @pytest.mark.parametrize( 'config_overrides, expected_results', [ # 2 levels @@ -896,6 +939,7 @@ def test_parse_var_list_wrapper_specific(metplus_config): ]), ] ) +@pytest.mark.util def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, expected_results): config = metplus_config() @@ -906,19 +950,19 @@ def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, var_list = config_metplus.parse_var_list(config, time_info=time_info, data_type=None) - assert(len(var_list) == len(expected_results)) + assert len(var_list) == len(expected_results) for var_item, expected_result in zip(var_list, expected_results): - assert(var_item['fcst_name'] == expected_result) + assert var_item['fcst_name'] == expected_result # run again with data type specified var_list = config_metplus.parse_var_list(config, time_info=time_info, data_type='FCST') - assert(len(var_list) == len(expected_results)) + assert len(var_list) == len(expected_results) for var_item, expected_result in zip(var_list, expected_results): - assert(var_item['fcst_name'] == expected_result) + assert var_item['fcst_name'] == expected_result @pytest.mark.parametrize( @@ -955,12 +999,14 @@ def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, ('StatAnalysis, MakePlots', ['StatAnalysis']), ] ) +@pytest.mark.util def test_get_process_list(metplus_config, input_list, expected_list): conf = metplus_config() conf.set('config', 'PROCESS_LIST', input_list) process_list = config_metplus.get_process_list(conf) output_list = [item[0] for item in process_list] - assert(output_list == expected_list) + assert output_list == expected_list + @pytest.mark.parametrize( 'input_list, expected_list', [ @@ -987,12 +1033,15 @@ def test_get_process_list(metplus_config, input_list, expected_list): ('TCStat', 'two')]), ] ) +@pytest.mark.util def test_get_process_list_instances(metplus_config, input_list, expected_list): conf = metplus_config() conf.set('config', 'PROCESS_LIST', input_list) output_list = config_metplus.get_process_list(conf) - assert(output_list == expected_list) + assert output_list == expected_list + +@pytest.mark.util def test_getraw_sub_and_nosub(metplus_config): raw_string = '{MODEL}_{CURRENT_FCST_NAME}' sub_actual = 'FCST_NAME' @@ -1007,6 +1056,8 @@ def test_getraw_sub_and_nosub(metplus_config): sub_value = config.getraw('config', 'OUTPUT_PREFIX', sub_vars=True) assert sub_value == sub_actual + +@pytest.mark.util def test_getraw_instance_with_unset_var(metplus_config): """! Replicates bug where CURRENT_FCST_NAME is substituted with an empty string when copied from an instance section diff --git a/internal_tests/pytests/logging/test_logging.py b/internal_tests/pytests/util/logging/test_logging.py similarity index 79% rename from internal_tests/pytests/logging/test_logging.py rename to internal_tests/pytests/util/logging/test_logging.py index 7e31851c3..68eca3262 100644 --- a/internal_tests/pytests/logging/test_logging.py +++ b/internal_tests/pytests/util/logging/test_logging.py @@ -1,26 +1,13 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +import pytest import logging import re import os -import pytest - -# -# -----------Mandatory----------- -# configuration and fixture to support METplus configuration files beyond -# the metplus_data, metplus_system, and metplus_runtime conf files. -# - - -# Add a test configuration -def pytest_addoption(parser): - parser.addoption("-c", action="store", help=" -c ") - -# @pytest.fixture -def cmdopt(request): - return request.config.getoption("-c") +@pytest.mark.util def test_log_level(metplus_config): # Verify that the log level is set to what we indicated in the config file. config = metplus_config() @@ -30,6 +17,7 @@ def test_log_level(metplus_config): assert fixture_logger.isEnabledFor(level) +@pytest.mark.util def test_log_level_key(metplus_config): # Verify that the LOG_LEVEL key is in the config file config_instance = metplus_config() @@ -38,6 +26,7 @@ def test_log_level_key(metplus_config): assert config_instance.has_option(section, option) +@pytest.mark.util def test_logdir_exists(metplus_config): # Verify that the expected log dir exists. config = metplus_config() @@ -47,6 +36,7 @@ def test_logdir_exists(metplus_config): assert os.path.exists(log_dir) +@pytest.mark.util def test_logfile_exists(metplus_config): # Verify that a logfile with format metplus.log exists # We are assuming that there can be numerous files in the log directory. @@ -67,9 +57,3 @@ def test_logfile_exists(metplus_config): else: # There is no log directory assert False - - - - - - diff --git a/internal_tests/pytests/met_config/test_met_config.py b/internal_tests/pytests/util/met_config/test_met_config.py similarity index 96% rename from internal_tests/pytests/met_config/test_met_config.py rename to internal_tests/pytests/util/met_config/test_met_config.py index 0f990e678..0f3adb658 100644 --- a/internal_tests/pytests/met_config/test_met_config.py +++ b/internal_tests/pytests/util/met_config/test_met_config.py @@ -6,6 +6,7 @@ from metplus.util.met_config import _read_climo_file_name, _read_climo_field from metplus.util import CLIMO_TYPES + @pytest.mark.parametrize( 'config_overrides, expected_value', [ # 0 no relevant config set @@ -30,6 +31,7 @@ '{ name="TMP"; level="(*,*)"; }'), ] ) +@pytest.mark.util def test_read_climo_field(metplus_config, config_overrides, expected_value): app_name = 'app' for climo_type in ('MEAN', 'STDEV'): @@ -45,6 +47,7 @@ def test_read_climo_field(metplus_config, config_overrides, expected_value): _read_climo_field(climo_type, config, app_name) assert config.getraw('config', expected_var) == expected_value + @pytest.mark.parametrize( 'config_overrides, expected_value', [ # 0 no relevant config set @@ -127,6 +130,7 @@ def test_read_climo_field(metplus_config, config_overrides, expected_value): 'hour_interval = 12;}')), ] ) +@pytest.mark.util def test_handle_climo_dict(metplus_config, config_overrides, expected_value): app_name = 'app' for climo_type in ('MEAN', 'STDEV'): @@ -145,27 +149,30 @@ def test_handle_climo_dict(metplus_config, config_overrides, expected_value): expected_sub = expected_value.replace('', climo_type.lower()) assert output_dict[expected_var] == expected_sub + @pytest.mark.parametrize( 'name, data_type, mp_configs, extra_args', [ ('beg', 'int', 'BEG', None), ('end', 'int', ['END'], None), ] ) +@pytest.mark.util def test_met_config_info(name, data_type, mp_configs, extra_args): item = METConfig(name=name, data_type=data_type) item.metplus_configs = mp_configs item.extra_args = extra_args - assert(item.name == name) - assert(item.data_type == data_type) + assert item.name == name + assert item.data_type == data_type if isinstance(mp_configs, list): - assert(item.metplus_configs == mp_configs) + assert item.metplus_configs == mp_configs else: - assert(item.metplus_configs == [mp_configs]) + assert item.metplus_configs == [mp_configs] if not extra_args: - assert(item.extra_args == {}) + assert item.extra_args == {} + @pytest.mark.parametrize( 'data_type, expected_function', [ @@ -178,11 +185,12 @@ def test_met_config_info(name, data_type, mp_configs, extra_args): ('bad_name', None), ] ) +@pytest.mark.util def test_set_met_config_function(data_type, expected_function): try: function_found = set_met_config_function(data_type) function_name = function_found.__name__ if function_found else None - assert(function_name == expected_function) + assert function_name == expected_function except ValueError: assert expected_function is None @@ -196,9 +204,11 @@ def test_set_met_config_function(data_type, expected_function): ('G002', '"G002"'), ] ) +@pytest.mark.util def test_format_regrid_to_grid(input, output): assert format_regrid_to_grid(input) == output + @pytest.mark.parametrize( 'config_overrides, expected_value', [ # 0 no climo variables set @@ -232,6 +242,7 @@ def test_format_regrid_to_grid(input, output): 'PYTHON_XARRAY'), ] ) +@pytest.mark.util def test_read_climo_file_name(metplus_config, config_overrides, expected_value): # name of app used for testing to read/set config variables diff --git a/internal_tests/pytests/met_util/test_met_util.py b/internal_tests/pytests/util/met_util/test_met_util.py similarity index 91% rename from internal_tests/pytests/met_util/test_met_util.py rename to internal_tests/pytests/util/met_util/test_met_util.py index ccc7d6bcd..8241ea452 100644 --- a/internal_tests/pytests/met_util/test_met_util.py +++ b/internal_tests/pytests/util/met_util/test_met_util.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 -import sys +import pytest + import datetime import os from dateutil.relativedelta import relativedelta import pprint -import pytest from metplus.util import met_util as util from metplus.util import time_util from metplus.util.config_metplus import parse_var_list + @pytest.mark.parametrize( 'key, value', [ ({"gt2.3", "gt5.5"}, True), @@ -43,14 +44,15 @@ ([">SFP70", ">SFP80", ">SFP90", ">SFP95"], True), ] ) +@pytest.mark.util def test_threshold(key, value): - assert(util.validate_thresholds(key) == value) + assert util.validate_thresholds(key) == value + # parses a threshold and returns a list of tuples of # comparison and number, i.e.: # 'gt4' => [('gt', 4)] # gt4&<5 => [('gt', 4), ('lt', 5)] - @pytest.mark.parametrize( 'key, value', [ ('gt4', [('gt', 4)]), @@ -81,8 +83,10 @@ def test_threshold(key, value): ("1', 'gt1'), @@ -223,13 +171,16 @@ def test_set_lists_as_loop_or_group(metplus_config): 'lt805,lt1609,lt4828,lt8045,ge8045,lt16090'), ] ) +@pytest.mark.plotting def test_format_thresh(metplus_config, expression, expected_result): - # Idependently test the creation of + # Independently test the creation of # string values for defining thresholds st = stat_analysis_wrapper(metplus_config) - assert(st.format_thresh(expression) == expected_result) + assert st.format_thresh(expression) == expected_result + +@pytest.mark.plotting def test_build_stringsub_dict(metplus_config): # Independently test the building of # the dictionary used in the stringtemplate @@ -431,7 +382,9 @@ def test_build_stringsub_dict(metplus_config): datetime.datetime(1900, 1, 1, 0, 0, 0)) assert(test_stringsub_dict['obs_init_hour_end'] == datetime.datetime(1900, 1, 1, 23, 59 ,59)) - + + +@pytest.mark.plotting def test_get_output_filename(metplus_config): # Independently test the building of # the output file name @@ -488,7 +441,7 @@ def test_get_output_filename(metplus_config): lists_to_loop, lists_to_group, config_dict) - assert(expected_output_filename == test_output_filename) + assert expected_output_filename == test_output_filename # Test 2 expected_output_filename = ( 'MODEL_TEST_MODEL_TEST_ANL_' @@ -508,7 +461,7 @@ def test_get_output_filename(metplus_config): lists_to_loop, lists_to_group, config_dict) - assert(expected_output_filename == test_output_filename) + assert expected_output_filename == test_output_filename # Test 3 expected_output_filename = ( 'MODEL_TEST_MODEL_TEST_ANL' @@ -528,7 +481,7 @@ def test_get_output_filename(metplus_config): lists_to_loop, lists_to_group, config_dict) - assert(expected_output_filename == test_output_filename) + assert expected_output_filename == test_output_filename # Test 4 expected_output_filename = ( 'MODEL_TEST_MODEL_TEST_ANL' @@ -546,8 +499,10 @@ def test_get_output_filename(metplus_config): lists_to_loop, lists_to_group, config_dict) - assert(expected_output_filename == test_output_filename) + assert expected_output_filename == test_output_filename + +@pytest.mark.plotting def test_get_lookin_dir(metplus_config): # Independently test the building of # the lookin directory @@ -593,36 +548,44 @@ def test_get_lookin_dir(metplus_config): 'OBS_THRESH_LIST', 'COV_THRESH_LIST', 'ALPHA_LIST', 'LINE_TYPE_LIST' ] lists_to_loop = [ 'FCST_VALID_HOUR_LIST', 'MODEL_LIST' ] - stat_analysis_pytest_dir = os.path.dirname(__file__) + pytest_data_dir = os.path.join(os.path.dirname(__file__), os.pardir, + os.pardir, os.pardir, 'data') # Test 1 - expected_lookin_dir = os.path.join(stat_analysis_pytest_dir, - '../../data/fake/20180201') - dir_path = os.path.join(stat_analysis_pytest_dir, - '../../data/fake/*') + expected_lookin_dir = os.path.join(pytest_data_dir, 'fake/20180201') + dir_path = os.path.join(pytest_data_dir, 'fake/*') test_lookin_dir = st.get_lookin_dir(dir_path, lists_to_loop, lists_to_group, config_dict) - assert(expected_lookin_dir == test_lookin_dir) + assert expected_lookin_dir == test_lookin_dir # Test 2 - expected_lookin_dir = os.path.join(stat_analysis_pytest_dir, - '../../data/fake/20180201') - dir_path = os.path.join(stat_analysis_pytest_dir, - '../../data/fake/{valid?fmt=%Y%m%d}') + expected_lookin_dir = os.path.join(pytest_data_dir, 'fake/20180201') + dir_path = os.path.join(pytest_data_dir, 'fake/{valid?fmt=%Y%m%d}') test_lookin_dir = st.get_lookin_dir(dir_path, lists_to_loop, lists_to_group, config_dict) - assert(expected_lookin_dir == test_lookin_dir) + assert expected_lookin_dir == test_lookin_dir # Test 3 - no matches for lookin dir wildcard expected_lookin_dir = '' - dir_path = os.path.join(stat_analysis_pytest_dir, - '../../data/fake/*nothingmatches*') + dir_path = os.path.join(pytest_data_dir, 'fake/*nothingmatches*') test_lookin_dir = st.get_lookin_dir(dir_path, lists_to_loop, lists_to_group, config_dict) - assert(expected_lookin_dir == test_lookin_dir) + assert expected_lookin_dir == test_lookin_dir + # Test 4 - 2 paths, one with wildcard + expected_lookin_dir = os.path.join(pytest_data_dir, 'fake/20180201') + expected_lookin_dir = f'{expected_lookin_dir} {expected_lookin_dir}' + dir_path = os.path.join(pytest_data_dir, 'fake/*') + dir_path = f'{dir_path}, {dir_path}' + + test_lookin_dir = st.get_lookin_dir(dir_path, lists_to_loop, + lists_to_group, config_dict) + assert expected_lookin_dir == test_lookin_dir + + +@pytest.mark.plotting def test_format_valid_init(metplus_config): # Independently test the formatting # of the valid and initialization date and hours @@ -641,18 +604,18 @@ def test_format_valid_init(metplus_config): config_dict['OBS_VALID_HOUR'] = '' config_dict['OBS_INIT_HOUR'] = '' config_dict = st.format_valid_init(config_dict) - assert(config_dict['FCST_VALID_BEG'] == '20190101_000000') - assert(config_dict['FCST_VALID_END'] == '20190105_000000') - assert(config_dict['FCST_VALID_HOUR'] == '"000000"') - assert(config_dict['FCST_INIT_BEG'] == '') - assert(config_dict['FCST_INIT_END'] == '') - assert(config_dict['FCST_INIT_HOUR'] == '"000000", "120000"') - assert(config_dict['OBS_VALID_BEG'] == '') - assert(config_dict['OBS_VALID_END'] == '') - assert(config_dict['OBS_VALID_HOUR'] == '') - assert(config_dict['OBS_INIT_BEG'] == '') - assert(config_dict['OBS_INIT_END'] == '') - assert(config_dict['OBS_INIT_HOUR'] == '') + assert config_dict['FCST_VALID_BEG'] == '20190101_000000' + assert config_dict['FCST_VALID_END'] == '20190105_000000' + assert config_dict['FCST_VALID_HOUR'] == '"000000"' + assert config_dict['FCST_INIT_BEG'] == '' + assert config_dict['FCST_INIT_END'] == '' + assert config_dict['FCST_INIT_HOUR'] == '"000000", "120000"' + assert config_dict['OBS_VALID_BEG'] == '' + assert config_dict['OBS_VALID_END'] == '' + assert config_dict['OBS_VALID_HOUR'] == '' + assert config_dict['OBS_INIT_BEG'] == '' + assert config_dict['OBS_INIT_END'] == '' + assert config_dict['OBS_INIT_HOUR'] == '' # Test 2 st.c_dict['DATE_BEG'] = '20190101' st.c_dict['DATE_END'] = '20190105' @@ -664,18 +627,18 @@ def test_format_valid_init(metplus_config): config_dict['OBS_VALID_HOUR'] = '' config_dict['OBS_INIT_HOUR'] = '' config_dict = st.format_valid_init(config_dict) - assert(config_dict['FCST_VALID_BEG'] == '20190101_000000') - assert(config_dict['FCST_VALID_END'] == '20190105_120000') - assert(config_dict['FCST_VALID_HOUR'] == '"000000", "120000"') - assert(config_dict['FCST_INIT_BEG'] == '') - assert(config_dict['FCST_INIT_END'] == '') - assert(config_dict['FCST_INIT_HOUR'] == '"000000", "120000"') - assert(config_dict['OBS_VALID_BEG'] == '') - assert(config_dict['OBS_VALID_END'] == '') - assert(config_dict['OBS_VALID_HOUR'] == '') - assert(config_dict['OBS_INIT_BEG'] == '') - assert(config_dict['OBS_INIT_END'] == '') - assert(config_dict['OBS_INIT_HOUR'] == '') + assert config_dict['FCST_VALID_BEG'] == '20190101_000000' + assert config_dict['FCST_VALID_END'] == '20190105_120000' + assert config_dict['FCST_VALID_HOUR'] == '"000000", "120000"' + assert config_dict['FCST_INIT_BEG'] == '' + assert config_dict['FCST_INIT_END'] == '' + assert config_dict['FCST_INIT_HOUR'] == '"000000", "120000"' + assert config_dict['OBS_VALID_BEG'] == '' + assert config_dict['OBS_VALID_END'] == '' + assert config_dict['OBS_VALID_HOUR'] == '' + assert config_dict['OBS_INIT_BEG'] == '' + assert config_dict['OBS_INIT_END'] == '' + assert config_dict['OBS_INIT_HOUR'] == '' # Test 3 st.c_dict['DATE_BEG'] = '20190101' st.c_dict['DATE_END'] = '20190101' @@ -687,18 +650,18 @@ def test_format_valid_init(metplus_config): config_dict['OBS_VALID_HOUR'] = '000000' config_dict['OBS_INIT_HOUR'] = '"000000", "120000"' config_dict = st.format_valid_init(config_dict) - assert(config_dict['FCST_VALID_BEG'] == '') - assert(config_dict['FCST_VALID_END'] == '') - assert(config_dict['FCST_VALID_HOUR'] == '') - assert(config_dict['FCST_INIT_BEG'] == '') - assert(config_dict['FCST_INIT_END'] == '') - assert(config_dict['FCST_INIT_HOUR'] == '') - assert(config_dict['OBS_VALID_BEG'] == '20190101_000000') - assert(config_dict['OBS_VALID_END'] == '20190101_000000') - assert(config_dict['OBS_VALID_HOUR'] == '"000000"') - assert(config_dict['OBS_INIT_BEG'] == '') - assert(config_dict['OBS_INIT_END'] == '') - assert(config_dict['OBS_INIT_HOUR'] == '"000000", "120000"') + assert config_dict['FCST_VALID_BEG'] == '' + assert config_dict['FCST_VALID_END'] == '' + assert config_dict['FCST_VALID_HOUR'] == '' + assert config_dict['FCST_INIT_BEG'] == '' + assert config_dict['FCST_INIT_END'] == '' + assert config_dict['FCST_INIT_HOUR'] == '' + assert config_dict['OBS_VALID_BEG'] == '20190101_000000' + assert config_dict['OBS_VALID_END'] == '20190101_000000' + assert config_dict['OBS_VALID_HOUR'] == '"000000"' + assert config_dict['OBS_INIT_BEG'] == '' + assert config_dict['OBS_INIT_END'] == '' + assert config_dict['OBS_INIT_HOUR'] == '"000000", "120000"' # Test 3 st.c_dict['DATE_BEG'] = '20190101' st.c_dict['DATE_END'] = '20190101' @@ -710,19 +673,21 @@ def test_format_valid_init(metplus_config): config_dict['OBS_VALID_HOUR'] = '000000' config_dict['OBS_INIT_HOUR'] = '"000000", "120000"' config_dict = st.format_valid_init(config_dict) - assert(config_dict['FCST_VALID_BEG'] == '') - assert(config_dict['FCST_VALID_END'] == '') - assert(config_dict['FCST_VALID_HOUR'] == '') - assert(config_dict['FCST_INIT_BEG'] == '') - assert(config_dict['FCST_INIT_END'] == '') - assert(config_dict['FCST_INIT_HOUR'] == '') - assert(config_dict['OBS_VALID_BEG'] == '') - assert(config_dict['OBS_VALID_END'] == '') - assert(config_dict['OBS_VALID_HOUR'] == '"000000"') - assert(config_dict['OBS_INIT_BEG'] == '20190101_000000') - assert(config_dict['OBS_INIT_END'] == '20190101_120000') - assert(config_dict['OBS_INIT_HOUR'] == '"000000", "120000"') - + assert config_dict['FCST_VALID_BEG'] == '' + assert config_dict['FCST_VALID_END'] == '' + assert config_dict['FCST_VALID_HOUR'] == '' + assert config_dict['FCST_INIT_BEG'] == '' + assert config_dict['FCST_INIT_END'] == '' + assert config_dict['FCST_INIT_HOUR'] == '' + assert config_dict['OBS_VALID_BEG'] == '' + assert config_dict['OBS_VALID_END'] == '' + assert config_dict['OBS_VALID_HOUR'] == '"000000"' + assert config_dict['OBS_INIT_BEG'] == '20190101_000000' + assert config_dict['OBS_INIT_END'] == '20190101_120000' + assert config_dict['OBS_INIT_HOUR'] == '"000000", "120000"' + + +@pytest.mark.plotting def test_parse_model_info(metplus_config): # Independently test the creation of # the model information dictionary @@ -747,19 +712,16 @@ def test_parse_model_info(metplus_config): expected_out_stat_filename_type = 'user' test_model_info_list = st.parse_model_info() - assert(test_model_info_list[0]['name'] == expected_name) - assert(test_model_info_list[0]['reference_name'] == - expected_reference_name) - assert(test_model_info_list[0]['obtype'] == expected_obtype) - assert(test_model_info_list[0]['dump_row_filename_template'] == - expected_dump_row_filename_template) - assert(test_model_info_list[0]['dump_row_filename_type'] == - expected_dump_row_filename_type) - assert(test_model_info_list[0]['out_stat_filename_template'] == - expected_out_stat_filename_template) - assert(test_model_info_list[0]['out_stat_filename_type'] == - expected_out_stat_filename_type) + assert test_model_info_list[0]['name'] == expected_name + assert test_model_info_list[0]['reference_name'] == expected_reference_name + assert test_model_info_list[0]['obtype'] == expected_obtype + assert test_model_info_list[0]['dump_row_filename_template'] == expected_dump_row_filename_template + assert test_model_info_list[0]['dump_row_filename_type'] == expected_dump_row_filename_type + assert test_model_info_list[0]['out_stat_filename_template'] == expected_out_stat_filename_template + assert test_model_info_list[0]['out_stat_filename_type'] == expected_out_stat_filename_type + +@pytest.mark.plotting def test_run_stat_analysis(metplus_config): # Test running of stat_analysis st = stat_analysis_wrapper(metplus_config) @@ -772,9 +734,9 @@ def test_run_stat_analysis(metplus_config): st.c_dict['DATE_END'] = '20190101' st.c_dict['DATE_TYPE'] = 'VALID' st.run_stat_analysis() - assert(os.path.exists(expected_filename)) - assert(os.path.getsize(expected_filename) - == os.path.getsize(comparison_filename)) + assert os.path.exists(expected_filename) + assert os.path.getsize(expected_filename) == os.path.getsize(comparison_filename) + @pytest.mark.parametrize( 'data_type, config_list, expected_list', [ @@ -788,14 +750,17 @@ def test_run_stat_analysis(metplus_config): ('OBS', '\"(0,*,*)\", \"(1,*,*)\"', ["0,*,*", "1,*,*"]), ] ) +@pytest.mark.plotting def test_get_level_list(metplus_config, data_type, config_list, expected_list): config = metplus_config() config.set('config', f'{data_type}_LEVEL_LIST', config_list) saw = StatAnalysisWrapper(config) - assert(saw.get_level_list(data_type) == expected_list) + assert saw.get_level_list(data_type) == expected_list + +@pytest.mark.plotting def test_get_config_file(metplus_config): fake_config_name = '/my/config/file' config = metplus_config() diff --git a/internal_tests/pytests/stat_analysis/test_stat_analysis_plotting.py b/internal_tests/pytests/wrappers/stat_analysis/test_stat_analysis_plotting.py similarity index 77% rename from internal_tests/pytests/stat_analysis/test_stat_analysis_plotting.py rename to internal_tests/pytests/wrappers/stat_analysis/test_stat_analysis_plotting.py index 01ed2be65..e869b5d58 100644 --- a/internal_tests/pytests/stat_analysis/test_stat_analysis_plotting.py +++ b/internal_tests/pytests/wrappers/stat_analysis/test_stat_analysis_plotting.py @@ -1,48 +1,17 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -import os -import datetime -import sys -import logging import pytest -import datetime -import glob - -import produtil.setup -from metplus.wrappers.stat_analysis_wrapper import StatAnalysisWrapper -from metplus.util import met_util as util +import os -# -# These are tests (not necessarily unit tests) for the -# MET stat_analysis wrapper, stat_analysis_wrapper.py -# NOTE: This test requires pytest, which is NOT part of the standard Python -# library. -# These tests require one configuration file in addition to the three -# required METplus configuration files: test_stat_analysis.conf. This contains -# the information necessary for running all the tests. Each test can be -# customized to replace various settings if needed. -# +import glob -# -# -----------Mandatory----------- -# configuration and fixture to support METplus configuration files beyond -# the metplus_data, metplus_system, and metplus_runtime conf files. -# +from metplus.wrappers.stat_analysis_wrapper import StatAnalysisWrapper +from metplus.util import handle_tmp_dir +METPLUS_BASE = os.getcwd().split('/internal_tests')[0] -# Add a test configuration -def pytest_addoption(parser): - parser.addoption("-c", action="store", help=" -c ") -# @pytest.fixture -def cmdopt(request): - return request.config.getoption("-c") - -# -# ------------Pytest fixtures that can be used for all tests --------------- -# -#@pytest.fixture def stat_analysis_wrapper(metplus_config): """! Returns a default StatAnalysisWrapper with /path/to entries in the metplus_system.conf and metplus_runtime.conf configuration @@ -54,39 +23,11 @@ def stat_analysis_wrapper(metplus_config): extra_configs = [] extra_configs.append(os.path.join(os.path.dirname(__file__), 'test_plotting.conf')) config = metplus_config(extra_configs) - util.handle_tmp_dir(config) + handle_tmp_dir(config) return StatAnalysisWrapper(config) -# ------------------TESTS GO BELOW --------------------------- -# - -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# To test numerous files for filesize, use parametrization: -# @pytest.mark.parametrize( -# 'key, value', [ -# ('/usr/local/met-6.1/bin/point_stat', 382180), -# ('/usr/local/met-6.1/bin/stat_analysis', 3438944), -# ('/usr/local/met-6.1/bin/pb2nc', 3009056) -# -# ] -# ) -# def test_file_sizes(key, value): -# st = stat_analysis_wrapper() -# # Retrieve the value of the class attribute that corresponds -# # to the key in the parametrization -# files_in_dir = [] -# for dirpath, dirnames, files in os.walk("/usr/local/met-6.1/bin"): -# for name in files: -# files_in_dir.append(os.path.join(dirpath, name)) -# if actual_key in files_in_dir: -# # The actual_key is one of the files of interest we retrieved from -# # the output directory. Verify that it's file size is what we -# # expected. -# assert actual_key == key -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -METPLUS_BASE = os.getcwd().split('/internal_tests')[0] - +@pytest.mark.plotting def test_set_lists_as_loop_or_group(metplus_config): # Independently test that the lists that are set # in the config file are being set @@ -148,6 +89,7 @@ def test_set_lists_as_loop_or_group(metplus_config): for elem in test_lists_to_loop_items)) +@pytest.mark.plotting def test_get_output_filename(metplus_config): # Independently test the building of # the output file name @@ -218,66 +160,10 @@ def test_get_output_filename(metplus_config): lists_to_loop, lists_to_group, config_dict) - assert (expected_output_filename == test_output_filename) - + assert expected_output_filename == test_output_filename -def test_parse_model_info(metplus_config): - pytest.skip("This function will be removed from MakePlots") - # Independently test the creation of - # the model information dictionary - # and the reading from the config file - # are as expected - st = stat_analysis_wrapper(metplus_config) - # Test 1 - expected_name1 = 'MODEL_TEST1' - expected_reference_name1 = 'MODEL_TEST1' - expected_obtype1 = 'MODEL_TEST1_ANL' - expected_dump_row_filename_template1 = ( - '{model?fmt=%s}_{obtype?fmt=%s}_valid{valid_beg?fmt=%Y%m%d}' - 'to{valid_end?fmt=%Y%m%d}_valid{valid_hour_beg?fmt=%H%M}to' - '{valid_hour_end?fmt=%H%M}Z_init{init_hour_beg?fmt=%H%M}to' - '{init_hour_end?fmt=%H%M}Z_fcst_lead{fcst_lead?fmt=%s}_' - 'fcst{fcst_var?fmt=%s}{fcst_level?fmt=%s}{fcst_thresh?fmt=%s}' - '{interp_mthd?fmt=%s}_obs{obs_var?fmt=%s}{obs_level?fmt=%s}' - '{obs_thresh?fmt=%s}{interp_mthd?fmt=%s}_vxmask{vx_mask?fmt=%s}' - '_dump_row.stat' - ) - expected_dump_row_filename_type1 = 'user' - expected_out_stat_filename_template1 = 'NA' - expected_out_stat_filename_type1 = 'NA' - expected_name2 = 'TEST2_MODEL' - expected_reference_name2 = 'TEST2_MODEL' - expected_obtype2 = 'ANLYS2' - expected_dump_row_filename_template2 = expected_dump_row_filename_template1 - expected_dump_row_filename_type2 = 'user' - expected_out_stat_filename_template2 = 'NA' - expected_out_stat_filename_type2 = 'NA' - test_model_info_list = st.parse_model_info() - assert (test_model_info_list[0]['name'] == expected_name1) - assert (test_model_info_list[0]['reference_name'] == - expected_reference_name1) - assert (test_model_info_list[0]['obtype'] == expected_obtype1) - assert (test_model_info_list[0]['dump_row_filename_template'] == - expected_dump_row_filename_template1) - assert (test_model_info_list[0]['dump_row_filename_type'] == - expected_dump_row_filename_type1) - assert (test_model_info_list[0]['out_stat_filename_template'] == - expected_out_stat_filename_template1) - assert (test_model_info_list[0]['out_stat_filename_type'] == - expected_out_stat_filename_type1) - assert (test_model_info_list[1]['name'] == expected_name2) - assert (test_model_info_list[1]['reference_name'] == - expected_reference_name2) - assert (test_model_info_list[1]['obtype'] == expected_obtype2) - assert (test_model_info_list[1]['dump_row_filename_template'] == - expected_dump_row_filename_template2) - assert (test_model_info_list[1]['dump_row_filename_type'] == - expected_dump_row_filename_type2) - assert (test_model_info_list[1]['out_stat_filename_template'] == - expected_out_stat_filename_template2) - assert (test_model_info_list[1]['out_stat_filename_type'] == - expected_out_stat_filename_type2) +@pytest.mark.plotting def test_filter_for_plotting(metplus_config): # Test running of stat_analysis st = stat_analysis_wrapper(metplus_config) @@ -510,6 +396,6 @@ def test_filter_for_plotting(metplus_config): os.listdir(st.config.getdir('OUTPUT_BASE') +'/plotting/stat_analysis') ) - assert(ntest_files == 32) + assert ntest_files == 32 for expected_filename in expected_filename_list: - assert(os.path.exists(expected_filename)) + assert os.path.exists(expected_filename) diff --git a/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py b/internal_tests/pytests/wrappers/tc_gen/test_tc_gen_wrapper.py similarity index 97% rename from internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py rename to internal_tests/pytests/wrappers/tc_gen/test_tc_gen_wrapper.py index 575fd2bf6..d424abbea 100644 --- a/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py +++ b/internal_tests/pytests/wrappers/tc_gen/test_tc_gen_wrapper.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -import os -import sys import pytest -import datetime + +import os from metplus.wrappers.tc_gen_wrapper import TCGenWrapper + @pytest.mark.parametrize( 'config_overrides, env_var_values', [ @@ -285,6 +285,7 @@ ] ) +@pytest.mark.wrapper_a def test_tc_gen(metplus_config, config_overrides, env_var_values): # expected number of 2016 files (including file_list line) expected_genesis_count = 7 @@ -382,11 +383,11 @@ def test_tc_gen(metplus_config, config_overrides, env_var_values): all_cmds = wrapper.run_all_times() print(f"ALL COMMANDS: {all_cmds}") - assert(len(all_cmds) == len(expected_cmds)) + assert len(all_cmds) == len(expected_cmds) for (cmd, env_vars), expected_cmd in zip(all_cmds, expected_cmds): # ensure commands are generated as expected - assert(cmd == expected_cmd) + assert cmd == expected_cmd # check that environment variables were set properly # including deprecated env vars (not in wrapper env var keys) @@ -396,27 +397,29 @@ def test_tc_gen(metplus_config, config_overrides, env_var_values): for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) - assert(match is not None) + assert match is not None value = match.split('=', 1)[1] - assert(env_var_values.get(env_var_key, '') == value) + assert env_var_values.get(env_var_key, '') == value # verify file count of genesis, edeck, shape, and track file list files with open(genesis_path, 'r') as file_handle: lines = file_handle.read().splitlines() - assert(len(lines) == expected_genesis_count) + assert len(lines) == expected_genesis_count with open(edeck_path, 'r') as file_handle: lines = file_handle.read().splitlines() - assert(len(lines) == expected_edeck_count) + assert len(lines) == expected_edeck_count with open(shape_path, 'r') as file_handle: lines = file_handle.read().splitlines() - assert(len(lines) == expected_shape_count) + assert len(lines) == expected_shape_count with open(track_path, 'r') as file_handle: lines = file_handle.read().splitlines() - assert(len(lines) == expected_track_count) + assert len(lines) == expected_track_count + +@pytest.mark.wrapper_a def test_get_config_file(metplus_config): fake_config_name = '/my/config/file' diff --git a/internal_tests/pytests/tc_pairs/tc_pairs_wrapper_test.conf b/internal_tests/pytests/wrappers/tc_pairs/tc_pairs_wrapper_test.conf similarity index 100% rename from internal_tests/pytests/tc_pairs/tc_pairs_wrapper_test.conf rename to internal_tests/pytests/wrappers/tc_pairs/tc_pairs_wrapper_test.conf diff --git a/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py b/internal_tests/pytests/wrappers/tc_pairs/test_tc_pairs_wrapper.py similarity index 96% rename from internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py rename to internal_tests/pytests/wrappers/tc_pairs/test_tc_pairs_wrapper.py index a0845be22..265564a29 100644 --- a/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py +++ b/internal_tests/pytests/wrappers/tc_pairs/test_tc_pairs_wrapper.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +import pytest + import os from datetime import datetime -import pytest from metplus.wrappers.tc_pairs_wrapper import TCPairsWrapper @@ -15,6 +16,7 @@ time_fmt = '%Y%m%d%H' run_times = ['2014121318'] + def set_minimum_config_settings(config, loop_by='INIT'): # set config variables to prevent command from running and bypass check # if input files actually exist @@ -41,6 +43,7 @@ def set_minimum_config_settings(config, loop_by='INIT'): # can set adeck or edeck variables config.set('config', 'TC_PAIRS_ADECK_TEMPLATE', adeck_template) + @pytest.mark.parametrize( 'config_overrides, isOK', [ ({}, True), @@ -80,6 +83,7 @@ def test_read_storm_info(metplus_config, config_overrides, isOK): ('2020100700_F000_261N_1101W_FOF', 'wildcard', 'wildcard'), ] ) +@pytest.mark.wrapper def test_parse_storm_id(metplus_config, storm_id, basin, cyclone): """! Check that storm ID is parsed properly to get basin and cyclone. Check that it returns wildcard expressions basin and cyclone cannot be @@ -107,6 +111,7 @@ def test_parse_storm_id(metplus_config, storm_id, basin, cyclone): assert actual_basin == expected_basin assert actual_cyclone == expected_cyclone + @pytest.mark.parametrize( 'basin,cyclone,expected_files,expected_wildcard', [ ('al', '0104', ['get_bdeck_balq2014123118.gfso.0104'], False), @@ -123,6 +128,7 @@ def test_parse_storm_id(metplus_config, storm_id, basin, cyclone): 'get_bdeck_bmlq2014123118.gfso.0105'], True), ] ) +@pytest.mark.wrapper def test_get_bdeck(metplus_config, basin, cyclone, expected_files, expected_wildcard): """! Checks that the correct list of empty test files are found and the @@ -150,11 +156,12 @@ def test_get_bdeck(metplus_config, basin, cyclone, expected_files, wrapper = TCPairsWrapper(config) actual_files, actual_wildcard = wrapper._get_bdeck(basin, cyclone, time_info) - assert(actual_wildcard == expected_wildcard) - assert(len(actual_files) == len(expected_files)) + assert actual_wildcard == expected_wildcard + assert len(actual_files) == len(expected_files) for actual_file, expected_file in zip(sorted(actual_files), sorted(expected_files)): - assert(os.path.basename(actual_file) == expected_file) + assert os.path.basename(actual_file) == expected_file + @pytest.mark.parametrize( 'template, filename,other_basin,other_cyclone', [ @@ -178,6 +185,7 @@ def test_get_bdeck(metplus_config, basin, cyclone, expected_files, '20141009bml.dat', 'ml', None), ] ) +@pytest.mark.wrapper def test_get_basin_cyclone_from_bdeck(metplus_config, template, filename, other_cyclone, other_basin): fake_dir = '/fake/dir' @@ -210,6 +218,7 @@ def test_get_basin_cyclone_from_bdeck(metplus_config, template, filename, assert actual_basin == expected_basin assert actual_cyclone == expected_cyclone + @pytest.mark.parametrize( 'config_overrides, storm_type, values_to_check', [ # 0: storm_id @@ -231,6 +240,7 @@ def test_get_basin_cyclone_from_bdeck(metplus_config, template, filename, 'cyclone', ['09', '10', '09', '10']), ] ) +@pytest.mark.wrapper def test_tc_pairs_storm_id_lists(metplus_config, config_overrides, storm_type, values_to_check): config = metplus_config() @@ -272,20 +282,21 @@ def test_tc_pairs_storm_id_lists(metplus_config, config_overrides, print(f"CMD{idx}: {cmd}") print(f"ENV{idx}: {env_list}") - assert(len(all_cmds) == len(values_to_check)) + assert len(all_cmds) == len(values_to_check) for (cmd, env_vars), value_to_check in zip(all_cmds, values_to_check): env_var_key = f'METPLUS_{storm_type.upper()}' match = next((item for item in env_vars if item.startswith(env_var_key)), None) - assert (match is not None) + assert match is not None print(f"Checking env var: {env_var_key}") actual_value = match.split('=', 1)[1] expected_value = f'{storm_type} = ["{value_to_check}"];' assert actual_value == expected_value + @pytest.mark.parametrize( 'config_overrides, env_var_values', [ # 0: no config overrides that set env vars @@ -370,6 +381,7 @@ def test_tc_pairs_storm_id_lists(metplus_config, config_overrides, ] ) +@pytest.mark.wrapper def test_tc_pairs_loop_order_processes(metplus_config, config_overrides, env_var_values): # run using init and valid time variables @@ -425,26 +437,27 @@ def test_tc_pairs_loop_order_processes(metplus_config, config_overrides, all_cmds = wrapper.run_all_times() print(f"ALL COMMANDS: {all_cmds}") - assert(len(all_cmds) == len(expected_cmds)) + assert len(all_cmds) == len(expected_cmds) for (cmd, env_vars), expected_cmd in zip(all_cmds, expected_cmds): # ensure commands are generated as expected - assert(cmd == expected_cmd) + assert cmd == expected_cmd # check that environment variables were set properly for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: match = next((item for item in env_vars if item.startswith(env_var_key)), None) - assert(match is not None) + assert match is not None print(f'Checking env var: {env_var_key}') actual_value = match.split('=', 1)[1] - assert(env_var_values.get(env_var_key, '') == actual_value) + assert env_var_values.get(env_var_key, '') == actual_value if remove_beg: del env_var_values[f'METPLUS_{loop_by}_BEG'] if remove_end: del env_var_values[f'METPLUS_{loop_by}_END'] + @pytest.mark.parametrize( 'config_overrides, env_var_values', [ # 0: no config overrides that set env vars @@ -460,6 +473,7 @@ def test_tc_pairs_loop_order_processes(metplus_config, config_overrides, {'METPLUS_CYCLONE': 'cyclone = ["1005", "0104"];'}), ] ) +@pytest.mark.wrapper def test_tc_pairs_read_all_files(metplus_config, config_overrides, env_var_values): # run using init and valid time variables @@ -512,22 +526,24 @@ def test_tc_pairs_read_all_files(metplus_config, config_overrides, all_cmds = wrapper.run_all_times() print(f"ALL COMMANDS: {all_cmds}") - assert(len(all_cmds) == len(expected_cmds)) + assert len(all_cmds) == len(expected_cmds) for (cmd, env_vars), expected_cmd in zip(all_cmds, expected_cmds): # check that environment variables were set properly for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: match = next((item for item in env_vars if item.startswith(env_var_key)), None) - assert(match is not None) + assert match is not None print(f'Checking env var: {env_var_key}') actual_value = match.split('=', 1)[1] - assert(env_var_values.get(env_var_key, '') == actual_value) + assert env_var_values.get(env_var_key, '') == actual_value # unset begin and end for next loop del env_var_values[f'METPLUS_{loop_by}_BEG'] del env_var_values[f'METPLUS_{loop_by}_END'] + +@pytest.mark.wrapper def test_get_config_file(metplus_config): fake_config_name = '/my/config/file' diff --git a/internal_tests/pytests/tc_stat/tc_stat_conf.conf b/internal_tests/pytests/wrappers/tc_stat/tc_stat_conf.conf similarity index 100% rename from internal_tests/pytests/tc_stat/tc_stat_conf.conf rename to internal_tests/pytests/wrappers/tc_stat/tc_stat_conf.conf diff --git a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py b/internal_tests/pytests/wrappers/tc_stat/test_tc_stat_wrapper.py similarity index 98% rename from internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py rename to internal_tests/pytests/wrappers/tc_stat/test_tc_stat_wrapper.py index e84b90b09..fe66cff3f 100644 --- a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py +++ b/internal_tests/pytests/wrappers/tc_stat/test_tc_stat_wrapper.py @@ -1,21 +1,22 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +import pytest import os import sys -import pytest import datetime -import produtil - from metplus.wrappers.tc_stat_wrapper import TCStatWrapper from metplus.util import ti_calculate + def get_config(metplus_config): extra_configs = [] extra_configs.append(os.path.join(os.path.dirname(__file__), 'tc_stat_conf.conf')) return metplus_config(extra_configs) + def tc_stat_wrapper(metplus_config): """! Returns a default TCStatWrapper with /path/to entries in the metplus_system.conf and metplus_runtime.conf configuration @@ -27,6 +28,7 @@ def tc_stat_wrapper(metplus_config): config = get_config(metplus_config) return TCStatWrapper(config) + @pytest.mark.parametrize( 'overrides, c_dict', [ ({'TC_STAT_INIT_BEG': '20150301', @@ -106,7 +108,8 @@ def tc_stat_wrapper(metplus_config): 'INIT_STR_EXC_VAL': 'init_str_exc_val = ["HUWARN"];'}), ] - ) +) +@pytest.mark.wrapper def test_override_config_in_c_dict(metplus_config, overrides, c_dict): config = get_config(metplus_config) instance = 'tc_stat_overrides' @@ -119,6 +122,7 @@ def test_override_config_in_c_dict(metplus_config, overrides, c_dict): assert (wrapper.env_var_dict.get(f'METPLUS_{key}') == expected_value or wrapper.c_dict.get(key) == expected_value) + @pytest.mark.parametrize( 'jobs, init_dt, expected_output', [ # single fake job @@ -143,6 +147,7 @@ def test_override_config_in_c_dict(metplus_config, overrides, c_dict): ), ] ) +@pytest.mark.wrapper def test_handle_jobs(metplus_config, jobs, init_dt, expected_output): if init_dt: time_info = ti_calculate({'init': init_dt}) @@ -158,7 +163,7 @@ def test_handle_jobs(metplus_config, jobs, init_dt, expected_output): wrapper.c_dict['JOBS'].append(job.replace('', output_dir)) output = wrapper.handle_jobs(time_info) - assert(output == expected_output.replace('', output_dir)) + assert output == expected_output.replace('', output_dir) def cleanup_test_dirs(parent_dirs, output_dir): @@ -168,6 +173,7 @@ def cleanup_test_dirs(parent_dirs, output_dir): if os.path.exists(parent_dir_sub): os.removedirs(parent_dir_sub) + @pytest.mark.parametrize( 'jobs, init_dt, expected_output, parent_dirs', [ # single fake job, no parent dir @@ -216,6 +222,7 @@ def cleanup_test_dirs(parent_dirs, output_dir): ), ] ) +@pytest.mark.wrapper def test_handle_jobs_create_parent_dir(metplus_config, jobs, init_dt, expected_output, parent_dirs): # if init time is provided, calculate other time dict items @@ -254,6 +261,7 @@ def test_handle_jobs_create_parent_dir(metplus_config, jobs, init_dt, cleanup_test_dirs(parent_dirs, output_dir) +@pytest.mark.wrapper def test_get_config_file(metplus_config): fake_config_name = '/my/config/file' diff --git a/internal_tests/pytests/user_script/test_user_script.py b/internal_tests/pytests/wrappers/user_script/test_user_script.py similarity index 99% rename from internal_tests/pytests/user_script/test_user_script.py rename to internal_tests/pytests/wrappers/user_script/test_user_script.py index de69d7634..b45fe8810 100644 --- a/internal_tests/pytests/user_script/test_user_script.py +++ b/internal_tests/pytests/wrappers/user_script/test_user_script.py @@ -1,17 +1,13 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -import os -import sys -import re -import logging -from collections import namedtuple import pytest + +import re from datetime import datetime -import produtil from metplus.wrappers.user_script_wrapper import UserScriptWrapper -from metplus.util import time_util + def sub_clock_time(input_cmd, clock_time): """! Helper function to replace clock time from config in expected output @@ -54,6 +50,7 @@ def sub_clock_time(input_cmd, clock_time): return output_cmd + def set_run_type_info(config, run_type): """! Set time values for init or valid time in config object @@ -347,6 +344,7 @@ def set_run_type_info(config, run_type): ['echo a'] * 12 + ['echo b'] * 12), ] ) +@pytest.mark.wrapper def test_run_user_script_all_times(metplus_config, input_configs, run_types, expected_cmds): config = metplus_config() diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index 7bb1da0d3..58a525501 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -137,6 +137,8 @@ Category: s2s 10:: UserScript_obsERA_obsOnly_RMM:: model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf:: spacetime_env, metdatadb 11:: UserScript_fcstGFS_obsERA_WeatherRegime:: model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.conf:: weatherregime_env,cartopy,metplus 12:: UserScript_obsERA_obsOnly_Stratosphere:: model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf:: metplotpy_env,metdatadb +13::SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool:: model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf:: netcdf4_env +14::GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile:: model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf:: netcdf4_env Category: space_weather 0::GridStat_fcstGloTEC_obsGloTEC_vx7:: model_applications/space_weather/GridStat_fcstGloTEC_obsGloTEC_vx7.conf diff --git a/metplus/util/time_looping.py b/metplus/util/time_looping.py index 0632b1763..f2e771430 100644 --- a/metplus/util/time_looping.py +++ b/metplus/util/time_looping.py @@ -12,7 +12,7 @@ def time_generator(config): Yields the next run time dictionary or None if something went wrong """ # determine INIT or VALID prefix - prefix = get_time_prefix(config) + prefix = _get_time_prefix(config) if not prefix: yield None return @@ -82,6 +82,44 @@ def time_generator(config): current_dt += time_interval +def get_start_and_end_times(config): + prefix = _get_time_prefix(config) + if not prefix: + return None, None + + # get clock time of when the run started + clock_dt = datetime.strptime( + config.getstr('config', 'CLOCK_TIME'), + '%Y%m%d%H%M%S' + ) + + time_format = config.getraw('config', f'{prefix}_TIME_FMT', '') + if not time_format: + config.logger.error(f'Could not read {prefix}_TIME_FMT') + return None, None + + start_string = config.getraw('config', f'{prefix}_BEG') + end_string = config.getraw('config', f'{prefix}_END', start_string) + + start_dt = _get_current_dt(start_string, + time_format, + clock_dt, + config.logger) + + end_dt = _get_current_dt(end_string, + time_format, + clock_dt, + config.logger) + + if not _validate_time_values(start_dt, + end_dt, + get_relativedelta('60'), + prefix, + config.logger): + return None, None + + return start_dt, end_dt + def _validate_time_values(start_dt, end_dt, time_interval, prefix, logger): if not start_dt: logger.error(f"Could not read {prefix}_BEG") @@ -95,7 +133,7 @@ def _validate_time_values(start_dt, end_dt, time_interval, prefix, logger): if (start_dt + time_interval < start_dt + timedelta(seconds=60)): logger.error(f'{prefix}_INCREMENT must be greater than or ' - 'equal to 60 seconds') + 'equal to 60 seconds') return False if start_dt > end_dt: @@ -109,9 +147,10 @@ def _create_time_input_dict(prefix, current_dt, clock_dt): 'loop_by': prefix.lower(), prefix.lower(): current_dt, 'now': clock_dt, + 'today': clock_dt.strftime('%Y%m%d'), } -def get_time_prefix(config): +def _get_time_prefix(config): """! Read the METplusConfig object and determine the prefix for the time looping variables. diff --git a/metplus/util/time_util.py b/metplus/util/time_util.py index 92f651435..4eb180f30 100755 --- a/metplus/util/time_util.py +++ b/metplus/util/time_util.py @@ -138,7 +138,7 @@ def ti_get_seconds_from_relativedelta(lead, valid_time=None): return None # if valid time is specified, use it to determine seconds - if valid_time is not None: + if valid_time: return int((valid_time - (valid_time - lead)).total_seconds()) if lead.months != 0 or lead.years != 0: diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index 8a7ceee31..de313014c 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -22,6 +22,7 @@ from ..util import met_util as util from ..util import do_string_sub, find_indices_in_config_section from ..util import parse_var_list, remove_quotes +from ..util import get_start_and_end_times from . import CommandBuilder class StatAnalysisWrapper(CommandBuilder): @@ -177,13 +178,18 @@ def create_c_dict(self): 'LOOP_BY', '')) - for time_conf in ['VALID_BEG', 'VALID_END', 'INIT_BEG', 'INIT_END']: - c_dict[time_conf] = self.config.getstr('config', time_conf, '') + start_dt, end_dt = get_start_and_end_times(self.config) + if not start_dt: + self.log_error('Could not get start and end times. ' + 'VALID_BEG/END or INIT_BEG/END must be set.') + else: + c_dict['DATE_BEG'] = start_dt.strftime('%Y%m%d') + c_dict['DATE_END'] = end_dt.strftime('%Y%m%d') for job_conf in ['JOB_NAME', 'JOB_ARGS']: c_dict[job_conf] = self.config.getstr('config', - f'STAT_ANALYSIS_{job_conf}', - '') + f'STAT_ANALYSIS_{job_conf}', + '') # read in all lists except field lists, which will be read in afterwards and checked all_lists_to_read = self.expected_config_lists + self.list_categories @@ -1061,22 +1067,24 @@ def get_lookin_dir(self, dir_path, lists_to_loop, lists_to_group, config_dict): lookin_dir - string of the filled directory from dir_path """ - if '?fmt=' in dir_path: - stringsub_dict = self.build_stringsub_dict(lists_to_loop, - lists_to_group, - config_dict) - dir_path_filled = do_string_sub(dir_path, - **stringsub_dict) - else: - dir_path_filled = dir_path - if '*' in dir_path_filled: - self.logger.debug(f"Expanding wildcard path: {dir_path_filled}") - dir_path_filled_all = ' '.join(sorted(glob.glob(dir_path_filled))) - self.logger.warning(f"Wildcard expansion found no matches") - else: - dir_path_filled_all = dir_path_filled - lookin_dir = dir_path_filled_all - return lookin_dir + stringsub_dict = self.build_stringsub_dict(lists_to_loop, + lists_to_group, + config_dict) + dir_path_filled = do_string_sub(dir_path, + **stringsub_dict) + + all_paths = [] + for one_path in dir_path_filled.split(','): + if '*' in one_path: + self.logger.debug(f"Expanding wildcard path: {one_path}") + expand_path = glob.glob(one_path.strip()) + if not expand_path: + self.logger.warning(f"Wildcard expansion found no matches") + continue + all_paths.extend(sorted(expand_path)) + else: + all_paths.append(one_path.strip()) + return ' '.join(all_paths) def format_valid_init(self, config_dict): """! Format the valid and initialization dates and @@ -1811,7 +1819,7 @@ def run_stat_analysis_job(self, runtime_settings_dict_list): self.set_environment_variables() # set lookin dir - self.logger.debug(f"Setting -lookindir to {runtime_settings_dict['LOOKIN_DIR']}") + self.logger.debug(f"Setting -lookin dir to {runtime_settings_dict['LOOKIN_DIR']}") self.lookindir = runtime_settings_dict['LOOKIN_DIR'] self.job_args = runtime_settings_dict['JOB'] @@ -1844,24 +1852,11 @@ def create_output_directories(self, runtime_settings_dict): return run_job def run_all_times(self): - date_type = self.c_dict['DATE_TYPE'] - self.c_dict['DATE_BEG'] = self.c_dict[date_type+'_BEG'] - self.c_dict['DATE_END'] = self.c_dict[date_type+'_END'] self.run_stat_analysis() return self.all_commands def run_at_time(self, input_dict): - loop_by_init = util.is_loop_by_init(self.config) - if loop_by_init is None: - return - - if loop_by_init: - loop_by = 'INIT' - else: - loop_by = 'VALID' - - self.c_dict['DATE_TYPE'] = loop_by - + loop_by = self.c_dict['DATE_TYPE'] run_date = input_dict[loop_by.lower()].strftime('%Y%m%d') self.c_dict['DATE_BEG'] = run_date self.c_dict['DATE_END'] = run_date diff --git a/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf new file mode 100644 index 000000000..228280838 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile.conf @@ -0,0 +1,102 @@ +[config] + +PROCESS_LIST = GridStat + +### +# Time Info +### + +LOOP_BY = INIT +INIT_TIME_FMT = %Y%m%d%H +INIT_BEG=1982010100 +INIT_END=2010020100 +INIT_INCREMENT = 1Y + +LEAD_SEQ = + +LOOP_ORDER = processes + +### +# File I/O +### + + +FCST_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +OBS_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +GRID_STAT_CLIMO_MEAN_INPUT_DIR = +GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE = + +GRID_STAT_CLIMO_STDEV_INPUT_DIR = +GRID_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + +GRID_STAT_OUTPUT_DIR = {OUTPUT_BASE}/HSS_out_Mplus +GRID_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m} + + +### +# Field Info +### + +MODEL = CFSv2 +OBTYPE = OBS + +FCST_VAR1_NAME = {CONFIG_DIR}/forecast_read-in_CFSv2_categoricalthresholds.py {INPUT_BASE}/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/CFSv2.tmp2m.{init?fmt=%Y%m}.fcst.nc:tmp2m:{init?fmt=%Y%m%d%H}:0:0 +FCST_VAR1_LEVELS = +FCST_VAR1_THRESH = lt1.5, lt2.5 + +OBS_VAR1_NAME = {CONFIG_DIR}/forecast_read-in_CFSv2_categoricalthresholds_obs.py {INPUT_BASE}/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/CFSv2.tmp2m.{init?fmt=%Y%m}.fcst.nc:tmp2m:{init?fmt=%Y%m%d%H}:0:0 +OBS_VAR1_LEVELS = +OBS_VAR1_THRESH = lt1.5, lt2.5 + +CONFIG_DIR = {PARM_BASE}/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile + +### +# GridStat +### + + + +GRID_STAT_CONFIG_FILE = {PARM_BASE}/met_config/GridStatConfig_wrapped + + +GRID_STAT_REGRID_TO_GRID = FCST + + +GRID_STAT_DESC = NA + +FCST_GRID_STAT_FILE_WINDOW_BEGIN = 0 +FCST_GRID_STAT_FILE_WINDOW_END = 0 +OBS_GRID_STAT_FILE_WINDOW_BEGIN = 0 +OBS_GRID_STAT_FILE_WINDOW_END = 0 + +GRID_STAT_NEIGHBORHOOD_WIDTH = 1 +GRID_STAT_NEIGHBORHOOD_SHAPE = SQUARE + +GRID_STAT_NEIGHBORHOOD_COV_THRESH = >=0.5 + +GRID_STAT_ONCE_PER_FIELD = False + +FCST_IS_PROB = false + +FCST_GRID_STAT_PROB_THRESH = ==0.1 + +OBS_IS_PROB = false + +OBS_GRID_STAT_PROB_THRESH = ==0.1 + +GRID_STAT_OUTPUT_PREFIX = + + + +GRID_STAT_OUTPUT_FLAG_MCTC = BOTH +GRID_STAT_OUTPUT_FLAG_MCTS = BOTH + +GRID_STAT_NC_PAIRS_FLAG_LATLON = TRUE +GRID_STAT_NC_PAIRS_FLAG_RAW = TRUE +GRID_STAT_NC_PAIRS_FLAG_DIFF = TRUE + + +GRID_STAT_HSS_EC_VALUE = + diff --git a/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds.py b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds.py new file mode 100644 index 000000000..cee496070 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds.py @@ -0,0 +1,183 @@ + +import sys +import re +import numpy as np +import datetime as dt +from dateutil.relativedelta import * # New import +from netCDF4 import Dataset, chartostring +from preprocessFun_Modified import preprocess, dominant_tercile_fcst, dominant_tercile_obs, get_init_year # New import + +#grab input from user +#should be (1)input file using full path (2) variable name (3) valid time for the forecast in %Y%m%d%H%M format and (4) ensemble member number, all separated by ':' characters +#program can only accept that 1 input, while still maintaining user flexability to change multiple +#variables, including valid time, ens member, etc. +# Addition by Johnna - model, lead, init taken from file name +# Addition by Johnna - climatology and std dev caclulated for given lead, model, init +# Addition by Johnna - added command line pull for lead + +print('Type in input_file using full path:variable name:valid time in %Y%m%d%H%M:ensemble member number:lead') +# Example +# /cpc/nmme/CFSv2/hcst_new/010100/CFSv2.tmp2m.198201.fcst.nc:tmp2m:198201010101:0:0 + +input_file, var_name, init_time, ens_mem, lead = sys.argv[1].split(':') +ens_mem = int(ens_mem) +lead = int(lead) # Added by Johnna +init_time = dt.datetime.strptime(init_time,"%Y%m%d%H%M") + +# --- +# Added by Johnna +input_file_split = input_file.split('/') +#EDIT BY JOHN O +init_temp = "010100" +#init_temp = input_file_split[5] + +#fil_name = input_file_split[6] +fil_name = input_file +year_temp = fil_name.split('.') +#year = year_temp[2] +year = year_temp[-3] +print('YYYYMM: ' + str(year)) + +# Setup Climatology +#EDIT BY JOHN O +#model = input_file_split[3] +model = "CFSv2" +init = init_temp.replace('0100','') +lead = lead +clim_per = '1982_2010' +member = ens_mem +variable = var_name + +# Get path based on model (only works for this file name) +input_file_split2 = input_file.split(model + '.') +path = input_file_split2[0] + +# Calculate climatologies and standard deviations/etc. This calculates every time the loop runs (i.e. for each file read into MET) +# There might be a better positioning for this function such that it doesn't recalc each loop, but I pulled from the command line input +# above to create the function, so right now it has dependance on the file names/user input/etc. +# Fine for now since data are smallish, but if theres something higher res might slow things down. +print('Model: ' + model + ' Init: ' + str(init) + ' lead: ' + str(lead) + ' Member: ' + str(ens_mem) + ' Variable: ' + variable) + +# We only need the fcst_cat_thresh +# Commenting out the original preprocess function +#clim, stddev, anom, std_anom = preprocess(path, model, init, variable, lead, clim_per, member) + +# New preprocessing function to get the cat thresholds +# In order 0, 1, 2 where 0 is LT, 1 is MT, 2 is UT +# fcst_cat_thresh has ALL times (28), and is calculated for ALL 24 members +# So the array fcst_cat_thresh is time | 29, lat | 181, lon | 360) +# I wrote a clumsy function to split this into years based on the filename read in (get_init_year) +# But it works! Basically just a bunch of if statements like if the filename is 198201 then its index 0 of the array and so on to the last index +# I also swap the fcst lats to be -90 to 90 instead of 90 to -90 and match up the longitudes (theres a cyclic point in the fcst so its actually 361 pts) +fcst_cat_thresh = dominant_tercile_fcst(path, model, init, variable, clim_per, lead) +idx = get_init_year(year) +fcst_cat_thresh_1year = fcst_cat_thresh[idx,::-1,0:360] + +# Going to do the obs in the same wrapper. I realized this would be easier so I hope this is okay... I think its just a flag...? +# Using same clunky function to get the right year out of the big array +#EDIT BY JOHN O +obs_cat_thresh = dominant_tercile_obs(path) +obs_cat_thresh_1year = obs_cat_thresh[idx,:,0:360] + +# Redefine var_name to fcst (necessary for below) +var_name = 'fcst' +# --- + +try: + print('The file you are working on is: ' + input_file) + # all of this is actually pointless eexcept to get the dimensions and times, all of the calculations are done in the functions + #set pointers to file and group name in file + + f = Dataset(input_file) + v = f[var_name][member,lead,:,:] + + #grab data from file + lat = np.float64(f.variables['lat'][::-1]) + lon = np.float64(f.variables['lon'][:]) + + # Grab and format time, this is taken from the file name, which might not be the best way of doing it + # Can also potentially pull from the netCDF; but need an extra package in netCDF4 to do that, and its a little weird + # given units of months since. This was a bit easier. + # Do need to add relativedelta package but thats fairly common (its from dateutil) + val_time = init_time + relativedelta(months=lead) + print('Valid Time: ' + str(val_time)) + + + # Coming from the function + # uncomment out the obs one if you want to use the obs? + v = fcst_cat_thresh_1year + #v = obs_cat_thresh_1year + print('Shape of variable to read into MET: ' + str(v.shape)) + + # -------------------------- + # Commented out by Johnna, defined above in user input + # Print statement erroring so commented out + #grab intialization time from file name and hold + #also compute the lead time + #i_time_ind = input_file.split("_").index("aod.nc")-1 + #i_time = input_file.split("_")[i_time_ind] + #i_time_obj = dt.datetime.strptime(i_time,"%Y%m%d%H") + #lead, rem = divmod((val_time - i_time_obj).total_seconds(), 3600) + + #print("Ensemble Member evaluation for: "+f.members.split(',')[ens_mem]) + + #checks if the the valid time for the forecast from user is present in file. + #Exits if the time is not present with a message + #if not val_time.timestamp() in f['time'][:]: + # print("valid time of "+str(val_time)+" is not present. Check file initialization time, passed valid time.") + # f.close() + # sys.exit(1) + + #grab index in the time array for the valid time provided by user (val_time) + #val_time_ind = np.where(f['time'][:] == val_time.timestamp())[0][0] + #var = np.float64(v[val_time_ind:val_time_ind+1,ens_mem:ens_mem+1,::-1,:]) + # -------------------------- + + #squeeze out all 1d arrays, add fill value, convert to float64 + var = np.float64(v) + var[var < -800] = -9999 + + met_data = np.squeeze(var).copy() + #JOHN O ADDED TO TEST IF FLIPPING IS OCCURING + met_data = met_data[::-1,:] + met_data = np.nan_to_num(met_data, nan=-1) + print('Done, no exceptions') + +except NameError: + print("Can't find input file") + sys.exit(1) + +########## +#create a metadata dictionary + +attrs = { + + 'valid': str(val_time.strftime("%Y%m%d"))+'_'+str(val_time.strftime("%H%M%S")), + 'init': str(init_time.strftime("%Y%m%d"))+'_'+str(init_time.strftime("%H%M%S")), + 'name': var_name, + 'long_name': input_file, + 'lead': str(int(lead)), + 'accum': '00', + 'level': 'sfc', + 'units': 'Degrees K', + + 'grid': { + 'name': 'Global 1 degree', + 'type': 'LatLon', + 'lat_ll': -90.0, + 'lon_ll': 0.0, + 'delta_lat': 1.0, + 'delta_lon': 1.0, + + 'Nlon': f.dimensions['lon'].size, + 'Nlat': f.dimensions['lat'].size, + } + } + +#print some output to show script ran successfully +print("Input file: " + repr(input_file)) +print("Variable name: " + repr(var_name)) +print("valid time: " + repr(val_time.strftime("%Y%m%d%H%M"))) +print("Attributes:\t" + repr(attrs)) +f.close() + diff --git a/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds_obs.py b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds_obs.py new file mode 100644 index 000000000..931115f68 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/forecast_read-in_CFSv2_categoricalthresholds_obs.py @@ -0,0 +1,186 @@ + +import sys +import re +import numpy as np +import datetime as dt +from dateutil.relativedelta import * # New import +from netCDF4 import Dataset, chartostring +from preprocessFun_Modified import preprocess, dominant_tercile_fcst, dominant_tercile_obs, get_init_year # New import + +#grab input from user +#should be (1)input file using full path (2) variable name (3) valid time for the forecast in %Y%m%d%H%M format and (4) ensemble member number, all separated by ':' characters +#program can only accept that 1 input, while still maintaining user flexability to change multiple +#variables, including valid time, ens member, etc. +# Addition by Johnna - model, lead, init taken from file name +# Addition by Johnna - climatology and std dev caclulated for given lead, model, init +# Addition by Johnna - added command line pull for lead + +print('Type in input_file using full path:variable name:valid time in %Y%m%d%H%M:ensemble member number:lead') +# Example +# /cpc/nmme/CFSv2/hcst_new/010100/CFSv2.tmp2m.198201.fcst.nc:tmp2m:198201010101:0:0 + +input_file, var_name, init_time, ens_mem, lead = sys.argv[1].split(':') +ens_mem = int(ens_mem) +lead = int(lead) # Added by Johnna +init_time = dt.datetime.strptime(init_time,"%Y%m%d%H%M") + +# --- +# Added by Johnna +input_file_split = input_file.split('/') +#EDIT BY JOHN O +init_temp = "010100" +#init_temp = input_file_split[5] + +#fil_name = input_file_split[6] +fil_name = input_file +year_temp = fil_name.split('.') +#year = year_temp[2] +year = year_temp[-3] +print('YYYYMM: ' + str(year)) + +# Setup Climatology +#EDIT BY JOHN O +#model = input_file_split[3] +model = "CFSv2" +init = init_temp.replace('0100','') +lead = lead +clim_per = '1982_2010' +member = ens_mem +variable = var_name + +# Get path based on model (only works for this file name) +input_file_split2 = input_file.split(model + '.') +path = input_file_split2[0] + +# Calculate climatologies and standard deviations/etc. This calculates every time the loop runs (i.e. for each file read into MET) +# There might be a better positioning for this function such that it doesn't recalc each loop, but I pulled from the command line input +# above to create the function, so right now it has dependance on the file names/user input/etc. +# Fine for now since data are smallish, but if theres something higher res might slow things down. +print('Model: ' + model + ' Init: ' + str(init) + ' lead: ' + str(lead) + ' Member: ' + str(ens_mem) + ' Variable: ' + variable) + +# We only need the fcst_cat_thresh +# Commenting out the original preprocess function +#clim, stddev, anom, std_anom = preprocess(path, model, init, variable, lead, clim_per, member) + +# New preprocessing function to get the cat thresholds +# In order 0, 1, 2 where 0 is LT, 1 is MT, 2 is UT +# fcst_cat_thresh has ALL times (28), and is calculated for ALL 24 members +# So the array fcst_cat_thresh is time | 29, lat | 181, lon | 360) +# I wrote a clumsy function to split this into years based on the filename read in (get_init_year) +# But it works! Basically just a bunch of if statements like if the filename is 198201 then its index 0 of the array and so on to the last index +# I also swap the fcst lats to be -90 to 90 instead of 90 to -90 and match up the longitudes (theres a cyclic point in the fcst so its actually 361 pts) +fcst_cat_thresh = dominant_tercile_fcst(path, model, init, variable, clim_per, lead) +idx = get_init_year(year) +fcst_cat_thresh_1year = fcst_cat_thresh[idx,::-1,0:360] + +# Going to do the obs in the same wrapper. I realized this would be easier so I hope this is okay... I think its just a flag...? +# Using same clunky function to get the right year out of the big array +#EDIT BY JOHN O +obs_cat_thresh = dominant_tercile_obs(path) +obs_cat_thresh_1year = obs_cat_thresh[idx,:,0:360] + +# Redefine var_name to fcst (necessary for below) +var_name = 'fcst' +# --- + +try: + print('The file you are working on is: ' + input_file) + # all of this is actually pointless eexcept to get the dimensions and times, all of the calculations are done in the functions + #set pointers to file and group name in file + + f = Dataset(input_file) + v = f[var_name][member,lead,:,:] + + #grab data from file + lat = np.float64(f.variables['lat'][::-1]) + lon = np.float64(f.variables['lon'][:]) + + # Grab and format time, this is taken from the file name, which might not be the best way of doing it + # Can also potentially pull from the netCDF; but need an extra package in netCDF4 to do that, and its a little weird + # given units of months since. This was a bit easier. + # Do need to add relativedelta package but thats fairly common (its from dateutil) + val_time = init_time + relativedelta(months=lead) + print('Valid Time: ' + str(val_time)) + + + # Coming from the function + # uncomment out the obs one if you want to use the obs? + #v = fcst_cat_thresh_1year + v = obs_cat_thresh_1year + print('Shape of variable to read into MET: ' + str(v.shape)) + + # -------------------------- + # Commented out by Johnna, defined above in user input + # Print statement erroring so commented out + #grab intialization time from file name and hold + #also compute the lead time + #i_time_ind = input_file.split("_").index("aod.nc")-1 + #i_time = input_file.split("_")[i_time_ind] + #i_time_obj = dt.datetime.strptime(i_time,"%Y%m%d%H") + #lead, rem = divmod((val_time - i_time_obj).total_seconds(), 3600) + + #print("Ensemble Member evaluation for: "+f.members.split(',')[ens_mem]) + + #checks if the the valid time for the forecast from user is present in file. + #Exits if the time is not present with a message + #if not val_time.timestamp() in f['time'][:]: + # print("valid time of "+str(val_time)+" is not present. Check file initialization time, passed valid time.") + # f.close() + # sys.exit(1) + + #grab index in the time array for the valid time provided by user (val_time) + #val_time_ind = np.where(f['time'][:] == val_time.timestamp())[0][0] + #var = np.float64(v[val_time_ind:val_time_ind+1,ens_mem:ens_mem+1,::-1,:]) + # -------------------------- + + #squeeze out all 1d arrays, add fill value, convert to float64 + var = np.float64(v) + var[var < -800] = -9999 + + met_data = np.squeeze(var).copy() + #JOHN ADDED TO REMOVE EXTRA LON + #met_data = met_data[:,:-1] + #JOHN O ADDED TO FLIP OBS GRID + met_data = met_data[::-1,:] + met_data = np.nan_to_num(met_data, nan=-1) + print('Done, no exceptions') + print('New shape of variable to read into MET: ' + str(met_data.shape)) + +except NameError: + print("Can't find input file") + sys.exit(1) + +########## +#create a metadata dictionary + +attrs = { + + 'valid': str(val_time.strftime("%Y%m%d"))+'_'+str(val_time.strftime("%H%M%S")), + 'init': str(init_time.strftime("%Y%m%d"))+'_'+str(init_time.strftime("%H%M%S")), + 'name': var_name, + 'long_name': input_file, + 'lead': str(int(lead)), + 'accum': '00', + 'level': 'sfc', + 'units': 'Degrees K', + + 'grid': { + 'name': 'Global 1 degree', + 'type': 'LatLon', + 'lat_ll': -90.0, + 'lon_ll': 0.0, + 'delta_lat': 1.0, + 'delta_lon': 1.0, + + 'Nlon': 360, + 'Nlat': f.dimensions['lat'].size, + } + } + +#print some output to show script ran successfully +print("Input file: " + repr(input_file)) +print("Variable name: " + repr(var_name)) +print("valid time: " + repr(val_time.strftime("%Y%m%d%H%M"))) +print("Attributes:\t" + repr(attrs)) +f.close() + diff --git a/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/preprocessFun_Modified.py b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/preprocessFun_Modified.py new file mode 100644 index 000000000..f4a638331 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/GridStat_fcstCFSv2_obsGHCNCAMS_MultiTercile/preprocessFun_Modified.py @@ -0,0 +1,348 @@ + +# Functions to pre-process data + +def preprocess(path, model, init, variable, lead, clim_per, member): + import numpy as np + from netCDF4 import Dataset + + if clim_per == '1982_2010': + years = np.arange(1982,2011,1) + elif clim_per == '1991_2020': + years = np.arange(1991,2020,1) + else: + print('Check your climatology period') + + # Get the directory + dir = path + + # Make an empty array to store the climatology and SD (just store for 1 lead, 1 member) + full_fcst_array = np.zeros((len(years), 181, 360)) + anom = np.zeros((len(years), 181, 360)) + std_anom = np.zeros((len(years), 181, 360)) + clim = np.zeros((181, 360)) + stddev = np.zeros((181, 360)) + + for y in range(len(years)): + year = years[y] + # Can comment out if this bothers you + path = str(dir + model + '.' + variable + '.' + str(year) + str(init) + '.fcst.nc') + #print('Opening ' + path) + + dataset = Dataset(path) + # Shape of array before subset (24, 10, 181, 360) + fcst = dataset.variables['fcst'][member,lead,:,:] + #print(fcst.shape) + full_fcst_array[y,:,:] = fcst + + # Can comment out if this bothers you + #print('Shape of fcst array with all times: ' + str(full_fcst_array.shape)) + + # Define climatology for the lead and member of interest + clim = np.nanmean(full_fcst_array,axis=0) + + # Define standard deviation for the lead and member of interest + stddev = np.nanstd(full_fcst_array,axis=0) + + # Define anomalies and standardized anomalies (perhaps unnecessary) + for y in range(len(years)): + anom[y,:,:] = full_fcst_array[y,:,:] - clim + std_anom[y,:,:] = anom[y,:,:]/stddev + + return clim, stddev, anom, std_anom +# -------------------------------------------------------------------------------------------------- + +# -------------------------------------------------------------------------------------------------- +def dominant_tercile_fcst(path, model, init, variable, clim_per, lead): + + import numpy as np + member = 0 + members = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] + + nEns = len(members) + + # Make climos, std anoms, etc. from function for 1 member (to get shapes for empty variables) + clim, stddev, anom, std_anom = preprocess(path, model, init, variable, lead, clim_per, member) + + # Empty variables + std_anom_all = np.zeros((std_anom.shape[0], std_anom.shape[1], std_anom.shape[2], nEns)) + + for i in range(len(members)): + member = members[i] + clim, stddev, anom, std_anom = preprocess(path, model, init, variable, lead, clim_per, member) + std_anom_all[:, :, :, i] = std_anom + + ut_ones = np.where(std_anom_all[:,:,:,:] > 0.43,1,0) + lt_ones = np.where(std_anom_all[:,:,:,:] < -0.43,1,0) + mt_ones = np.where( (std_anom_all[:,:,:,:] > -0.43) & (std_anom_all[:,:,:,:] < 0.43),1,0) + + ut_prob = np.nansum(ut_ones,3)/len(members) + lt_prob = np.nansum(lt_ones,3)/len(members) + mt_prob = np.nansum(mt_ones,3)/len(members) + + # Put all in 1 array + all_probs = np.zeros((3, ut_prob.shape[0], ut_prob.shape[1], ut_prob.shape[2])) + + all_probs[0,:,:,:] = lt_prob + all_probs[1,:,:,:] = mt_prob + all_probs[2,:,:,:] = ut_prob + + + # Johnna's test statements: + #print('Model stuff') + #print(std_anom_all[0,50,100,:]) + + #print('lt') + #print(all_probs[0,:,50,100]) + + #print('mt') + #print(all_probs[1,:,50,100]) + + #print('ut') + #print(all_probs[2,:,50,100]) + + dominant_tercile = np.argmax(all_probs, axis=0) + + #print('dominant tercile') + #print(dominant_tercile[:,50,100]) + + temp = np.where(dominant_tercile == 2, 3., dominant_tercile) + temp = np.where(dominant_tercile == 1, 2., temp) + temp = np.where(dominant_tercile == 0, 1., temp) + + #print('temp') + #print(temp[:,50,100]) + + #cat_thresh_fcst = dominant_tercile + #cat_thresh_fcst = temp + + # Swap lats to match obs (will swap back later) + temp = temp[:,::-1,0:360] + # Mask according to obs: + # Note, will need to change path name here: + data_mask = mask(path) + + temp_masked = np.zeros((temp.shape[0], temp.shape[1], temp.shape[2])) + for i in range(0,temp.shape[0]): + #MODIFIED BY JOHN O, CHANGE NANS TO -9999s + temp_masked[i,:,:] = np.where(data_mask[:,0:360] == -9.99000000e+08, -9999, temp[i,:,:]) + + #print('temp masked') + #print(temp_masked[:,50,100]) + + cat_thresh_fcst = temp_masked[:,::-1,:] # MET function swaps lats to normal, putting this back into abnormal + + return cat_thresh_fcst +# -------------------------------------------------------------------------------------------------- + + + + +# -------------------------------------------------------------------------------------------------- +def dominant_tercile_obs(path_obs): + from netCDF4 import Dataset + import numpy as np + + # Read in obs data + obs_data = Dataset(path_obs + "ghcn_cams.1x1.1982-2020.mon.nc") + obs_clim_data = Dataset(path_obs + "ghcn_cams.1x1.1982-2010.mon.clim.nc") + obs_stddev_data = Dataset(path_obs + "ghcn_cams.1x1.1982-2010.mon.stddev.nc") + + + print('Note this function for obs is ONLY meant to be used for January monthly verification with a 1982-2010 base period') + obs_full = obs_data.variables['tmp2m'][:,:,:] + obs_clim = obs_clim_data.variables['clim'][0,:,:] + obs_stddev = obs_stddev_data.variables['stddev'][0,:,:] + + # Grab only Januaries + obs_jan_full = obs_full[::12,:,:] + + # For 1982-2010 + obs_jan = obs_jan_full[0:29,:,:] + + # Make std anoms + obs_std_anom = np.zeros((obs_jan.shape[0], obs_jan.shape[1], obs_jan.shape[2])) + + for t in range(0,obs_jan.shape[0]): + obs_std_anom[t,:,:] = (obs_jan[t,:,:] - obs_clim) / obs_stddev + + ut_obs_ones = np.where(obs_std_anom[:,:,:] > 0.43,3,0) + lt_obs_ones = np.where(obs_std_anom[:,:,:] < -0.43,1,0) + mt_obs_ones = np.where( (obs_std_anom[:,:,:] > -0.43) & (obs_std_anom[:,:,:] < 0.43),2,0) + + # Put all in 1 array + all_probs = np.zeros((3, ut_obs_ones.shape[0], ut_obs_ones.shape[1], ut_obs_ones.shape[2])) + + all_probs[0,:,:,:] = lt_obs_ones + all_probs[1,:,:,:] = mt_obs_ones + all_probs[2,:,:,:] = ut_obs_ones + + + # Johnna testing: + #print('obs stuff') + #print(obs_std_anom[:,100,50]) + + #print('lt') + #print(all_probs[0,:,100,50]) + + #print('mt') + #print(all_probs[1,:,100,50]) + + #print('ut') + #print(all_probs[2,:,100,50]) + + + # Mask according to obs: + # Note, will need to change path name here: + data_mask = mask(path_obs) + + temp1 = np.nansum(all_probs,axis=0) + temp = temp1[:,:,0:360] + #print(cat_thresh_obs[:, 100, 50]) + #print(np.nanmax(cat_thresh_obs)) + #print(np.nanmin(cat_thresh_obs)) + + temp_masked = np.zeros((temp.shape[0], temp.shape[1], temp.shape[2])) + for i in range(0,temp.shape[0]): + #MODIFIED BY JOHN O, CHANGE NANS TO -9999s + temp_masked[i,:,:] = np.where(data_mask[:,0:360] == -9.99000000e+08, -9999, temp[i,:,:]) + + cat_thresh_obs = temp_masked + + return cat_thresh_obs +# -------------------------------------------------------------------------------------------------- + + + + + + +# -------------------------------------------------------------------------------------------------- +def plot_bs(varObs, plotType): + import numpy as np + import matplotlib as mpl + mpl.use('Agg') + import matplotlib.pyplot as plt + from mpl_toolkits.basemap import Basemap + from matplotlib.colors import LinearSegmentedColormap, ListedColormap, BoundaryNorm + from matplotlib import ticker + + lats = np.arange(-90, 90, 1) + lons = np.arange(0, 360, 1) + lon, lat = np.meshgrid(lons, lats) + #clevs = [240, 250, 260, 270, 280, 290, 300] + #clevs = [-4.0, -3.0, -2.0, -1.0, -0.5, -0.25, 0.25, 0.5, 1.0, 2.0, 3.0, 4.0] + #clevs = [0.125, 0.25, 0.375, 0.5, 0.625, 0.755, 0.875] + #clevs = [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9] + + if plotType == 'BS': + clevs = np.arange(0.025, 0.425, 0.025) + #print(clevs) + #clevs = [0.0, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.275, 0.3, 0.325, 0.35, 0.375, 0.4] + elif plotType == 'BS_clim': + clevs = np.arange(0.025, 0.425, 0.025) + elif plotType == 'bs_py_min_met': + clevs = np.arange(-0.1, 0.1, 0.01) + elif plotType == 'bsc_py_min_met': + clevs = np.arange(-0.1, 0.1, 0.01) + else: + clevs = np.arange(-0.4, 0.4, 0.025) + if varObs == 'tmp2m': + myBlues = ['#5a00d2', '#3e9dff', '#30c6f3', '#00ffff', '#c7eab9'] + myReds = ['#f9e3b4', '#eeff41', '#ffc31b', '#e69138', '#c43307'] + else: + myBlues = ['#995005', '#da751f', '#e0a420', '#f9cb9c', '#f9e3b4'] + myReds = ['#5affd1', '#b3eaac', '#75e478', '#4db159', '#2e8065'] + myColors = myBlues+['white']+myReds + cmap = ListedColormap(myColors, 'mycmap', N = len(clevs)-1) + norm = BoundaryNorm(clevs, cmap.N) + m = Basemap(projection='mill', resolution='l', llcrnrlon=0, llcrnrlat= -90, + urcrnrlon=360, urcrnrlat = 90) + fig = plt.figure(figsize=(10,10)) + + return lats, lons, clevs, myBlues, myReds, myColors, cmap, norm, m, fig +# -------------------------------------------------------------------------------------------------- + + + +# -------------------------------------------------------------------------------------------------- +def get_init_year(year): + + # There is absolutely a better way to do this... + + if year == '198201': + idx = 0 + if year == '198301': + idx = 1 + if year == '198401': + idx = 2 + if year == '198501': + idx = 3 + if year == '198601': + idx = 4 + if year == '198701': + idx = 5 + if year == '198801': + idx = 6 + if year == '198901': + idx = 7 + if year == '199001': + idx = 8 + if year == '199101': + idx = 9 + if year == '199201': + idx = 10 + if year == '199301': + idx = 11 + if year == '199401': + idx = 12 + if year == '199501': + idx = 13 + if year == '199601': + idx = 14 + if year == '199701': + idx = 15 + if year == '199801': + idx = 16 + if year == '199901': + idx = 17 + if year == '200001': + idx = 18 + if year == '200101': + idx = 19 + if year == '200201': + idx = 20 + if year == '200301': + idx = 21 + if year == '200401': + idx = 22 + if year == '200501': + idx = 23 + if year == '200601': + idx = 24 + if year == '200701': + idx = 25 + if year == '200801': + idx = 26 + if year == '200901': + idx = 27 + if year == '201001': + idx = 28 + + return idx +# -------------------------------------------------------------------------------------------------- + + + + +# -------------------------------------------------------------------------------------------------- +def mask(path_obs): + import numpy as np + from netCDF4 import Dataset + obs_data = Dataset(path_obs + "ghcn_cams.1x1.1982-2020.mon.nc") + obs_mask_all = obs_data.variables['tmp2m'][::12,:,:] + obs_mask = obs_mask_all[0,:,0:360] + #print(obs_mask[:,100]) + obs_mask = np.where(obs_mask.all == '--', np.nan, obs_mask) + #print(obs_mask[:,100]) + return obs_mask +# -------------------------------------------------------------------------------------------------- diff --git a/parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf b/parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf new file mode 100644 index 000000000..d6e1f475c --- /dev/null +++ b/parm/use_cases/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool.conf @@ -0,0 +1,251 @@ +[config] + +PROCESS_LIST = SeriesAnalysis, GenEnsProd, SeriesAnalysis(run_two), GridStat + +### +# Time Info +### + +LOOP_BY = INIT +INIT_TIME_FMT = %Y%m +INIT_BEG=198201 +INIT_END=201002 +INIT_INCREMENT = 1Y + +LEAD_SEQ = + +LOOP_ORDER = processes + +### +# SERIES_ANALYSIS FIELDINFO +### + +SERIES_ANALYSIS_RUNTIME_FREQ = RUN_ONCE +MODEL = CFSv2 +SERIES_ANALYSIS_CUSTOM_LOOP_LIST = 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 + +BOTH_SERIES_ANALYSIS_VAR1_NAME = fcst +BOTH_SERIES_ANALYSIS_VAR1_LEVELS = "({custom},0,*,*)" +SERIES_ANALYSIS_FCST_FILE_TYPE = NETCDF_NCCF +SERIES_ANALYSIS_OBS_FILE_TYPE = NETCDF_NCCF +SERIES_ANALYSIS_OUTPUT_STATS_CNT = TOTAL, FBAR, FSTDEV +SERIES_ANALYSIS_BLOCK_SIZE = 0 + +### +# File I/O SERIES_ANALYSIS +### + +FCST_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool +FCST_SERIES_ANALYSIS_INPUT_TEMPLATE = CFSv2.tmp2m.{init?fmt=%Y%m}.fcst.nc + +OBS_SERIES_ANALYSIS_INPUT_DIR = {FCST_SERIES_ANALYSIS_INPUT_DIR} +OBS_SERIES_ANALYSIS_INPUT_TEMPLATE = {FCST_SERIES_ANALYSIS_INPUT_TEMPLATE} + +SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/SA_run1 +SERIES_ANALYSIS_OUTPUT_TEMPLATE = mem{custom?fmt=%s}_output.nc + +### +# File I/O Gen_Ens_Prod +### + +GEN_ENS_PROD_INPUT_DIR = {FCST_SERIES_ANALYSIS_INPUT_DIR} + +GEN_ENS_PROD_INPUT_TEMPLATE = {FCST_SERIES_ANALYSIS_INPUT_TEMPLATE} + +#GEN_ENS_PROD_CTRL_INPUT_DIR = +#GEN_ENS_PROD_CTRL_INPUT_TEMPLATE = + +GEN_ENS_PROD_N_MEMBERS = 24 + +GEN_ENS_PROD_OUTPUT_DIR = {OUTPUT_BASE}/GEP +GEN_ENS_PROD_OUTPUT_TEMPLATE = gen_ens_prod_{init?fmt=%Y%m}_ens.nc + +### +# Field Info +### + +ENS_VAR1_NAME = fcst +ENS_VAR1_LEVELS = "(MET_ENS_MEMBER_ID,0,*,*)" +ENS_VAR1_THRESH = <-0.43, >=-0.43&&<=0.43, >0.43 +ENS_FILE_TYPE = NETCDF_NCCF + +### +# GenEnsProd +### + +#LOG_GEN_ENS_PROD_VERBOSITY = 2 + +MODEL = CFSv2 +# GEN_ENS_PROD_DESC = NA + +#GEN_ENS_PROD_REGRID_TO_GRID = NONE +#GEN_ENS_PROD_REGRID_METHOD = NEAREST +#GEN_ENS_PROD_REGRID_WIDTH = 1 +#GEN_ENS_PROD_REGRID_VLD_THRESH = 0.5 +#GEN_ENS_PROD_REGRID_SHAPE = SQUARE + +#GEN_ENS_PROD_CENSOR_THRESH = +#GEN_ENS_PROD_CENSOR_VAL = +GEN_ENS_PROD_NORMALIZE = CLIMO_STD_ANOM +#GEN_ENS_PROD_CAT_THRESH = +#GEN_ENS_PROD_NC_VAR_STR = + +GEN_ENS_PROD_ENS_THRESH = 0.3 +GEN_ENS_PROD_VLD_THRESH = 0.3 + +#GEN_ENS_PROD_NBRHD_PROB_WIDTH = 5 +#GEN_ENS_PROD_NBRHD_PROB_SHAPE = CIRCLE +#GEN_ENS_PROD_NBRHD_PROB_VLD_THRESH = 0.0 + +#GEN_ENS_PROD_NMEP_SMOOTH_VLD_THRESH = 0.0 +#GEN_ENS_PROD_NMEP_SMOOTH_SHAPE = CIRCLE +#GEN_ENS_PROD_NMEP_SMOOTH_GAUSSIAN_DX = 81.27 +#GEN_ENS_PROD_NMEP_SMOOTH_GAUSSIAN_RADIUS = 120 +#GEN_ENS_PROD_NMEP_SMOOTH_METHOD = GAUSSIAN +#GEN_ENS_PROD_NMEP_SMOOTH_WIDTH = 1 + +GEN_ENS_PROD_CLIMO_MEAN_FILE_NAME = {SERIES_ANALYSIS_OUTPUT_DIR}/memMET_ENS_MEMBER_ID_output.nc +GEN_ENS_PROD_CLIMO_MEAN_FIELD = {name="series_cnt_FBAR"; level="(*,*)";} +#GEN_ENS_PROD_CLIMO_MEAN_REGRID_METHOD = +#GEN_ENS_PROD_CLIMO_MEAN_REGRID_WIDTH = +#GEN_ENS_PROD_CLIMO_MEAN_REGRID_VLD_THRESH = +#GEN_ENS_PROD_CLIMO_MEAN_REGRID_SHAPE = +#GEN_ENS_PROD_CLIMO_MEAN_TIME_INTERP_METHOD = +#GEN_ENS_PROD_CLIMO_MEAN_MATCH_MONTH = +#GEN_ENS_PROD_CLIMO_MEAN_DAY_INTERVAL = 31 +#GEN_ENS_PROD_CLIMO_MEAN_HOUR_INTERVAL = 6 + +GEN_ENS_PROD_CLIMO_STDEV_FILE_NAME = {SERIES_ANALYSIS_OUTPUT_DIR}/memMET_ENS_MEMBER_ID_output.nc +GEN_ENS_PROD_CLIMO_STDEV_FIELD = {name="series_cnt_FSTDEV"; level="(*,*)";} +#GEN_ENS_PROD_CLIMO_STDEV_REGRID_METHOD = +#GEN_ENS_PROD_CLIMO_STDEV_REGRID_WIDTH = +#GEN_ENS_PROD_CLIMO_STDEV_REGRID_VLD_THRESH = +#GEN_ENS_PROD_CLIMO_STDEV_REGRID_SHAPE = +#GEN_ENS_PROD_CLIMO_STDEV_TIME_INTERP_METHOD = +#GEN_ENS_PROD_CLIMO_STDEV_MATCH_MONTH = +#GEN_ENS_PROD_CLIMO_STDEV_DAY_INTERVAL = 31 +#GEN_ENS_PROD_CLIMO_STDEV_HOUR_INTERVAL = 6 + +GEN_ENS_PROD_ENSEMBLE_FLAG_LATLON = TRUE +GEN_ENS_PROD_ENSEMBLE_FLAG_MEAN = TRUE +GEN_ENS_PROD_ENSEMBLE_FLAG_STDEV = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_MINUS = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_PLUS = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_MIN = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_MAX = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_RANGE = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_VLD_COUNT = TRUE +GEN_ENS_PROD_ENSEMBLE_FLAG_FREQUENCY = TRUE +#GEN_ENS_PROD_ENSEMBLE_FLAG_NEP = FALSE +#GEN_ENS_PROD_ENSEMBLE_FLAG_NMEP = FALSE +#GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO = FALSE +#GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO_CDF = FALSE + +GEN_ENS_PROD_ENS_MEMBER_IDS = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 +#GEN_ENS_PROD_CONTROL_ID = + +### +# File I/O Grid_Stat +### + +FCST_GRID_STAT_INPUT_DIR = {GEN_ENS_PROD_OUTPUT_DIR} +FCST_GRID_STAT_INPUT_TEMPLATE = {GEN_ENS_PROD_OUTPUT_TEMPLATE} + +OBS_GRID_STAT_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool +OBS_GRID_STAT_INPUT_TEMPLATE = ghcn_cams.1x1.1982-2020.mon.nc + +GRID_STAT_OUTPUT_DIR = {OUTPUT_BASE}/GridStat +GRID_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m} +GRID_STAT_OUTPUT_PREFIX = {init?fmt=%Y%m} + + +### +# Field Info GridStat +### + +FCST_GRID_STAT_VAR1_NAME = fcst_0_0_all_all_ENS_FREQ_lt-0.43 +FCST_GRID_STAT_VAR1_LEVELS = "(*,*)" +FCST_GRID_STAT_VAR1_THRESH = ==0.1 +FCST_GRID_STAT_IS_PROB = True + +OBS_GRID_STAT_VAR1_NAME = tmp2m +OBS_GRID_STAT_VAR1_LEVELS = "({init?fmt=%Y%m%d_%H%M%S},*,*)" +OBS_GRID_STAT_VAR1_THRESH = <=CDP33 +OBS_GRID_STAT_FILE_TYPE = NETCDF_NCCF + + +### +# Field Info for GridStat +### + +GRID_STAT_CLIMO_MEAN_FILE_NAME = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool/ghcn_cams.1x1.1982-2010.mon.clim.nc +GRID_STAT_CLIMO_MEAN_FIELD = {name="clim"; level="(0,*,*)";} +GRID_STAT_CLIMO_MEAN_FILE_TYPE = NETCDF_NCCF + + +GRID_STAT_CLIMO_STDEV_FILE_NAME = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool/ghcn_cams.1x1.1982-2010.mon.stddev.nc +GRID_STAT_CLIMO_STDEV_FIELD = {name="stddev"; level="(0,*,*)";} +GRID_STAT_CLIMO_STDEV_FILE_TYPE = NETCDF_NCCF + +GRID_STAT_REGRID_TO_GRID = FCST +GRID_STAT_OUTPUT_FLAG_PSTD = BOTH +GRID_STAT_NC_PAIRS_FLAG_APPLY_MASK = TRUE +GRID_STAT_NC_PAIRS_FLAG_RAW = TRUE + +[run_two] +### +# FILE I/O of SeriesAnalysis run_two +### + +FCST_SERIES_ANALYSIS_INPUT_DIR = {OUTPUT_BASE}/GEP +FCST_SERIES_ANALYSIS_INPUT_TEMPLATE = gen_ens_prod_{init?fmt=%Y%m}_ens.nc + +OBS_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool +OBS_SERIES_ANALYSIS_INPUT_TEMPLATE = ghcn_cams.1x1.1982-2020.mon.nc + +SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/SA_run2 +SERIES_ANALYSIS_OUTPUT_TEMPLATE = {INIT_BEG}to{INIT_END}_CFSv2_SA.nc + +### +# Field Info for SeriesAnalysis run_two +### +# +#These first entries are empty to override the intial SeriesAnalysis call +#SERIES_ANALYSIS_CUSTOM_LOOP_LIST = + +#BOTH_SERIES_ANALYSIS_VAR1_NAME = +#BOTH_SERIES_ANALYSIS_VAR1_LEVELS = + +FCST_SERIES_ANALYSIS_VAR1_NAME = fcst_0_0_all_all_ENS_FREQ_lt-0.43 +FCST_SERIES_ANALYSIS_VAR1_LEVELS = "(*,*)" + +FCST_CAT_THRESH = ==0.1 +FCST_IS_PROB = True + +OBS_SERIES_ANALYSIS_VAR1_NAME = tmp2m +OBS_SERIES_ANALYSIS_VAR1_LEVELS = "({init?fmt=%Y%m%d_%H%M%S},*,*)" +OBS_SERIES_ANALYSIS_CAT_THRESH = <=CDP33 + +OBS_FILE_TYPE = NETCDF_NCCF + +### +# SeriesAnalysis General for run_two +### + +SERIES_ANALYSIS_REGRID_TO_GRID = FCST +SERIES_ANALYSIS_OUTPUT_STATS_PSTD = TOTAL, BRIER, RELIABILITY, BRIERCL, BSS +SERIES_ANALYSIS_VLD_THRESH = 0.5 + +SERIES_ANALYSIS_BLOCK_SIZE = 0 + +SERIES_ANALYSIS_CLIMO_MEAN_FILE_NAME = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool/ghcn_cams.1x1.1982-2010.mon.clim.nc +SERIES_ANALYSIS_CLIMO_MEAN_FIELD = {name="clim"; level="(0,*,*)";} +SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE = NETCDF_NCCF + + +SERIES_ANALYSIS_CLIMO_STDEV_FILE_NAME = {INPUT_BASE}/model_applications/s2s/SeriesAnalysis_fcstCFSv2_obsGHCNCAMS_climoStandardized_MultiStatisticTool/ghcn_cams.1x1.1982-2010.mon.stddev.nc +SERIES_ANALYSIS_CLIMO_STDEV_FIELD = {name="stddev"; level="(0,*,*)";} +SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE = NETCDF_NCCF +SERIES_ANALYSIS_RUNTIME_FREQ = RUN_ONCE + +SERIES_ANALYSIS_RUN_ONCE_PER_STORM_ID = False diff --git a/scripts/docker/docker_env/README.md b/scripts/docker/docker_env/README.md index 9b8adc0a6..e39351360 100644 --- a/scripts/docker/docker_env/README.md +++ b/scripts/docker/docker_env/README.md @@ -55,13 +55,13 @@ docker push dtcenter/metplus-envs:h5py ./scripts/h5py_env.sh py_embed_base ``` -## metdatadb (from metplus_base) +## metdataio (from metplus_base) ### Docker ``` -docker build -t dtcenter/metplus-envs:metdatadb --build-arg ENV_NAME=metdatadb . -docker push dtcenter/metplus-envs:metdatadb +docker build -t dtcenter/metplus-envs:metdataio --build-arg ENV_NAME=metdataio . +docker push dtcenter/metplus-envs:metdataio ``` ### Local diff --git a/scripts/docker/docker_env/scripts/metdatadb_env.sh b/scripts/docker/docker_env/scripts/metdataio_env.sh similarity index 87% rename from scripts/docker/docker_env/scripts/metdatadb_env.sh rename to scripts/docker/docker_env/scripts/metdataio_env.sh index 4a29a6981..b0808167e 100755 --- a/scripts/docker/docker_env/scripts/metdatadb_env.sh +++ b/scripts/docker/docker_env/scripts/metdataio_env.sh @@ -1,9 +1,9 @@ #! /bin/sh ################################################################################ -# Environment: metdatadb -# Last Updated: 2021-06-08 (mccabe@ucar.edu) -# Notes: Adds Python packages needed to run METdbLoad from METdatadb +# Environment: metdataio +# Last Updated: 2022-07-13 (mccabe@ucar.edu) +# Notes: Adds Python packages needed to run METdbLoad from METdataio # Python Packages: # lxml==3.8.0 # pymysql==1.0.2 @@ -13,7 +13,7 @@ ################################################################################ # Conda environment to create -ENV_NAME=metdatadb +ENV_NAME=metdataio # Conda environment to use as base for new environment BASE_ENV=$1