From ed34891050be7a0a9c711e6062163dfda4792161 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Thu, 20 Jan 2022 20:50:53 -0700 Subject: [PATCH 01/49] Add var_action_test directory to test variable actions. Currently limited to unit conversions --- test/run_tests.sh | 8 + test/var_action_test/.gitignore | 1 + test/var_action_test/CMakeLists.txt | 187 ++++++++++ test/var_action_test/README.md | 6 + test/var_action_test/effr_calc.F90 | 46 +++ test/var_action_test/effr_calc.meta | 66 ++++ test/var_action_test/run_test | 244 ++++++++++++++ test/var_action_test/test_host.F90 | 394 ++++++++++++++++++++++ test/var_action_test/test_host.meta | 38 +++ test/var_action_test/test_host_data.F90 | 40 +++ test/var_action_test/test_host_data.meta | 27 ++ test/var_action_test/test_host_mod.F90 | 69 ++++ test/var_action_test/test_host_mod.meta | 30 ++ test/var_action_test/test_reports.py | 157 +++++++++ test/var_action_test/var_action_files.txt | 1 + test/var_action_test/var_action_suite.xml | 7 + 16 files changed, 1321 insertions(+) create mode 100644 test/var_action_test/.gitignore create mode 100644 test/var_action_test/CMakeLists.txt create mode 100644 test/var_action_test/README.md create mode 100644 test/var_action_test/effr_calc.F90 create mode 100644 test/var_action_test/effr_calc.meta create mode 100755 test/var_action_test/run_test create mode 100644 test/var_action_test/test_host.F90 create mode 100644 test/var_action_test/test_host.meta create mode 100644 test/var_action_test/test_host_data.F90 create mode 100644 test/var_action_test/test_host_data.meta create mode 100644 test/var_action_test/test_host_mod.F90 create mode 100644 test/var_action_test/test_host_mod.meta create mode 100755 test/var_action_test/test_reports.py create mode 100644 test/var_action_test/var_action_files.txt create mode 100644 test/var_action_test/var_action_suite.xml diff --git a/test/run_tests.sh b/test/run_tests.sh index 90a3ebaa..97001d30 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -37,6 +37,14 @@ if [ $res -ne 0 ]; then echo "Failure running advection test" fi +# Run var_action test +./var_action_test/run_test +res=$? +errcnt=$((errcnt + res)) +if [ $res -ne 0 ]; then + echo "Failure running var_action test" +fi + # Run doctests ./run_doctest.sh res=$? diff --git a/test/var_action_test/.gitignore b/test/var_action_test/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/test/var_action_test/.gitignore @@ -0,0 +1 @@ +build diff --git a/test/var_action_test/CMakeLists.txt b/test/var_action_test/CMakeLists.txt new file mode 100644 index 00000000..4b9daab8 --- /dev/null +++ b/test/var_action_test/CMakeLists.txt @@ -0,0 +1,187 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +PROJECT(test_host) +ENABLE_LANGUAGE(Fortran) + +include(CMakeForceCompiler) + +SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules) + +#------------------------------------------------------------------------------ +# +# Set where the CCPP Framework lives +# +#------------------------------------------------------------------------------ +get_filename_component(TEST_ROOT "${CMAKE_SOURCE_DIR}" DIRECTORY) +get_filename_component(CCPP_ROOT "${TEST_ROOT}" DIRECTORY) +#------------------------------------------------------------------------------ +# +# Create list of SCHEME_FILES, HOST_FILES, and SUITE_FILES +# Paths should be relative to CMAKE_SOURCE_DIR (this file's directory) +# +#------------------------------------------------------------------------------ +LIST(APPEND SCHEME_FILES "var_action_files.txt") +LIST(APPEND HOST_FILES "test_host_data" "test_host_mod") +LIST(APPEND SUITE_FILES "var_action_suite.xml") +# HOST is the name of the executable we will build. +# We assume there are files ${HOST}.meta and ${HOST}.F90 in CMAKE_SOURCE_DIR +SET(HOST "${CMAKE_PROJECT_NAME}") + +#------------------------------------------------------------------------------ +# +# End of project-specific input +# +#------------------------------------------------------------------------------ + +# By default, no verbose output +SET(VERBOSITY 0 CACHE STRING "Verbosity level of output (default: 0)") +# By default, generated caps go in ccpp subdir +SET(CCPP_CAP_FILES "${CMAKE_BINARY_DIR}/ccpp" CACHE + STRING "Location of CCPP-generated cap files") + +SET(CCPP_FRAMEWORK ${CCPP_ROOT}/scripts) + +# Use rpaths on MacOSX +set(CMAKE_MACOSX_RPATH 1) + +#------------------------------------------------------------------------------ +# Set a default build type if none was specified +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + #message(STATUS "Setting build type to 'Debug' as none was specified.") + #set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) + + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" + "MinSizeRel" "RelWithDebInfo") +endif() + +ADD_COMPILE_OPTIONS(-O0) + +if (${CMAKE_Fortran_COMPILER_ID} MATCHES "GNU") +# gfortran +# MESSAGE("gfortran being used.") + ADD_COMPILE_OPTIONS(-fcheck=all) + ADD_COMPILE_OPTIONS(-fbacktrace) + ADD_COMPILE_OPTIONS(-ffpe-trap=zero) + ADD_COMPILE_OPTIONS(-finit-real=nan) + ADD_COMPILE_OPTIONS(-ggdb) + ADD_COMPILE_OPTIONS(-ffree-line-length-none) + ADD_COMPILE_OPTIONS(-cpp) +elseif (${CMAKE_Fortran_COMPILER_ID} MATCHES "Intel") +# ifort +# MESSAGE("ifort being used.") + #ADD_COMPILE_OPTIONS(-check all) + ADD_COMPILE_OPTIONS(-fpe0) + ADD_COMPILE_OPTIONS(-warn) + ADD_COMPILE_OPTIONS(-traceback) + ADD_COMPILE_OPTIONS(-debug extended) + ADD_COMPILE_OPTIONS(-fpp) +elseif (${CMAKE_Fortran_COMPILER_ID} MATCHES "PGI") +# pgf90 +# MESSAGE("pgf90 being used.") + ADD_COMPILE_OPTIONS(-g) + ADD_COMPILE_OPTIONS(-Mipa=noconst) + ADD_COMPILE_OPTIONS(-traceback) + ADD_COMPILE_OPTIONS(-Mfree) + ADD_COMPILE_OPTIONS(-Mfptrap) + ADD_COMPILE_OPTIONS(-Mpreprocess) +else (${CMAKE_Fortran_COMPILER_ID} MATCHES "GNU") + message (WARNING "This program has only been compiled with gfortran, pgf90 and ifort. If another compiler is needed, the appropriate flags SHOULD be added in ${CMAKE_SOURCE_DIR}/CMakeLists.txt") +endif (${CMAKE_Fortran_COMPILER_ID} MATCHES "GNU") + +#------------------------------------------------------------------------------ +# CMake Modules +# Set the CMake module path +list(APPEND CMAKE_MODULE_PATH "${CCPP_FRAMEWORK}/cmake") +#------------------------------------------------------------------------------ +# Set OpenMP flags for C/C++/Fortran +if (OPENMP) + include(detect_openmp) + detect_openmp() + set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") + set (CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${OpenMP_Fortran_FLAGS}") + message(STATUS "Enable OpenMP support for C/C++/Fortran compiler") +else(OPENMP) + message (STATUS "Disable OpenMP support for C/C++/Fortran compiler") +endif() + +# Create metadata and source file lists +FOREACH(FILE ${SCHEME_FILES}) + FILE(STRINGS ${FILE} FILENAMES) + LIST(APPEND SCHEME_FILENAMES ${FILENAMES}) +ENDFOREACH(FILE) +string(REPLACE ";" "," SCHEME_METADATA "${SCHEME_FILES}") + +FOREACH(FILE ${SCHEME_FILENAMES}) + # target_sources prefers absolute pathnames + string(REPLACE ".meta" ".F90" TEMP "${FILE}") + get_filename_component(ABS_PATH "${TEMP}" ABSOLUTE) + list(APPEND LIBRARY_LIST ${ABS_PATH}) +ENDFOREACH(FILE) + +FOREACH(FILE ${HOST_FILES}) + LIST(APPEND HOST_METADATA "${FILE}.meta") + # target_sources prefers absolute pathnames + get_filename_component(ABS_PATH "${FILE}.F90" ABSOLUTE) + LIST(APPEND HOST_SOURCE "${ABS_PATH}") +ENDFOREACH(FILE) +list(APPEND LIBRARY_LIST ${HOST_SOURCE}) +string(REPLACE ";" ".meta," HOST_METADATA "${HOST_FILES}") +set(HOST_METADATA "${HOST_METADATA}.meta,${HOST}.meta") + +string(REPLACE ";" "," SUITE_XML "${SUITE_FILES}") + +# Run ccpp_capgen +set(CAPGEN_CMD "${CCPP_FRAMEWORK}/ccpp_capgen.py") +list(APPEND CAPGEN_CMD "--host-files") +list(APPEND CAPGEN_CMD "${HOST_METADATA}") +list(APPEND CAPGEN_CMD "--scheme-files") +list(APPEND CAPGEN_CMD "${SCHEME_METADATA}") +list(APPEND CAPGEN_CMD "--suites") +list(APPEND CAPGEN_CMD "${SUITE_XML}") +list(APPEND CAPGEN_CMD "--host-name") +list(APPEND CAPGEN_CMD "test_host") +list(APPEND CAPGEN_CMD "--output-root") +list(APPEND CAPGEN_CMD "${CCPP_CAP_FILES}") +while (VERBOSITY GREATER 0) + list(APPEND CAPGEN_CMD "--verbose") + MATH(EXPR VERBOSITY "${VERBOSITY} - 1") +endwhile () +string(REPLACE ";" " " CAPGEN_STRING "${CAPGEN_CMD}") +MESSAGE(STATUS "Running: ${CAPGEN_STRING}") +EXECUTE_PROCESS(COMMAND ${CAPGEN_CMD} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE CAPGEN_OUT ERROR_VARIABLE CAPGEN_OUT RESULT_VARIABLE RES) +MESSAGE(STATUS "${CAPGEN_OUT}") +if (RES EQUAL 0) + MESSAGE(STATUS "CCPP cap generation completed") +else(RES EQUAL 0) + MESSAGE(FATAL_ERROR "CCPP cap generation FAILED: result = ${RES}") +endif(RES EQUAL 0) + +# Retrieve the list of files from datatable.xml and set to CCPP_CAPS +set(DTABLE_CMD "${CCPP_FRAMEWORK}/ccpp_datafile.py") +list(APPEND DTABLE_CMD "${CCPP_CAP_FILES}/datatable.xml") +list(APPEND DTABLE_CMD "--ccpp-files") +list(APPEND DTABLE_CMD "--separator=\\;") +string(REPLACE ";" " " DTABLE_STRING "${DTABLE_CMD}") +MESSAGE(STATUS "Running: ${DTABLE_STRING}") +EXECUTE_PROCESS(COMMAND ${DTABLE_CMD} OUTPUT_VARIABLE CCPP_CAPS + RESULT_VARIABLE RES + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE) +message(STATUS "CCPP_CAPS = ${CCPP_CAPS}") +if (RES EQUAL 0) + MESSAGE(STATUS "CCPP cap files retrieved") +else(RES EQUAL 0) + MESSAGE(FATAL_ERROR "CCPP cap file retrieval FAILED: result = ${RES}") +endif(RES EQUAL 0) +list(APPEND LIBRARY_LIST ${CCPP_CAPS}) +add_library(TESTLIB OBJECT ${LIBRARY_LIST}) +ADD_EXECUTABLE(${HOST} ${HOST}.F90 $) + +INCLUDE_DIRECTORIES(${CCPP_CAP_FILES}) + +set_target_properties(${HOST} PROPERTIES + COMPILE_FLAGS "${CMAKE_Fortran_FLAGS}" + LINK_FLAGS "${CMAKE_Fortran_FLAGS}") diff --git a/test/var_action_test/README.md b/test/var_action_test/README.md new file mode 100644 index 00000000..b593a1f9 --- /dev/null +++ b/test/var_action_test/README.md @@ -0,0 +1,6 @@ +var_action test +================ + +To build and run the var_action test, run ./run_test +This script will build and run the test. +The exit code is zero (0) on PASS and non-zero on FAIL. diff --git a/test/var_action_test/effr_calc.F90 b/test/var_action_test/effr_calc.F90 new file mode 100644 index 00000000..50804a24 --- /dev/null +++ b/test/var_action_test/effr_calc.F90 @@ -0,0 +1,46 @@ +!Test unit conversions for intent in, inout, out variables +! + +module effr_calc + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effr_calc_run + +contains + + !> \section arg_table_effr_calc_run Argument Table + !! \htmlinclude arg_table_effr_calc_run.html + !! + subroutine effr_calc_run(ncol, nlev, effrr_in, effrl_inout, & + effri_out, effrs_inout, errmsg, errflg) + + integer, intent(in) :: ncol + integer, intent(in) :: nlev + real(kind_phys), intent(in) :: effrr_in(:,:) + real(kind_phys), intent(inout) :: effrl_inout(:,:) + real(kind_phys), intent(out) :: effri_out(:,:) + real(kind_phys), intent(inout) :: effrs_inout(:,:) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + !---------------------------------------------------------------- + + real(kind_phys), parameter :: re_qc_min = 2.5 ! microns + real(kind_phys), parameter :: re_qc_max = 50. ! microns + real(kind_phys), parameter :: re_qi_avg = 75. ! microns + real(kind_phys) :: effrr_local(ncol,nlev) + + errmsg = '' + errflg = 0 + + effrr_local = effrr_in + effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) + effri_out = re_qi_avg + effrs_inout = effrs_inout + 10.0 ! in micrometer + + end subroutine effr_calc_run + +end module effr_calc diff --git a/test/var_action_test/effr_calc.meta b/test/var_action_test/effr_calc.meta new file mode 100644 index 00000000..12e3dac6 --- /dev/null +++ b/test/var_action_test/effr_calc.meta @@ -0,0 +1,66 @@ +[ccpp-table-properties] + name = effr_calc + type = scheme + dependencies = +[ccpp-arg-table] + name = effr_calc_run + type = scheme +[ ncol ] + standard_name = horizontal_loop_extent + type = integer + units = count + dimensions = () + intent = in +[ nlev ] + standard_name = vertical_layer_dimension + type = integer + units = count + dimensions = () + intent = in +[effrr_in] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in +[effrl_inout] + standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle + long_name = effective radius of cloud liquid water particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = inout +[effri_out] + standard_name = effective_radius_of_stratiform_cloud_ice_particle + long_name = effective radius of cloud ice water particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out +[effrs_inout] + standard_name = effective_radius_of_stratiform_cloud_snow_particle + long_name = effective radius of cloud snow particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/var_action_test/run_test b/test/var_action_test/run_test new file mode 100755 index 00000000..2b4db0ac --- /dev/null +++ b/test/var_action_test/run_test @@ -0,0 +1,244 @@ +#! /bin/bash + +currdir="`pwd -P`" +scriptdir="$( cd $( dirname $0 ); pwd -P )" + +## +## Option default values +## +defdir="ct_build" +build_dir="${currdir}/${defdir}" +cleanup="PASS" # Other supported options are ALWAYS and NEVER +verbosity=0 + +## +## General syntax help function +## Usage: help +## +help () { + local hname="Usage: `basename ${0}`" + local hprefix="`echo ${hname} | tr '[!-~]' ' '`" + echo "${hname} [ --build-dir ] [ --cleanup ]" + echo "${hprefix} [ --verbosity <#> ]" + hprefix=" " + echo "" + echo "${hprefix} : Directory for building and running the test" + echo "${hprefix} default is /${defdir}" + echo "${hprefix} : Cleanup option is ALWAYS, NEVER, or PASS" + echo "${hprefix} default is PASS" + echo "${hprefix} verbosity: 0, 1, or 2" + echo "${hprefix} default is 0" + exit $1 +} + +## +## Error output function (should be handed a string) +## +perr() { + >&2 echo -e "\nERROR: ${@}\n" + exit 1 +} + +## +## Cleanup the build and test directory +## +docleanup() { + # We start off in the build directory + if [ "${build_dir}" == "${currdir}" ]; then + echo "WARNING: Cannot clean ${build_dir}" + else + cd ${currdir} + rm -rf ${build_dir} + fi +} + +## Process our input arguments +while [ $# -gt 0 ]; do + case $1 in + --h | -h | --help | -help) + help 0 + ;; + --build-dir) + if [ $# -lt 2 ]; then + perr "${1} requires a build directory" + fi + build_dir="${2}" + shift + ;; + --cleanup) + if [ $# -lt 2 ]; then + perr "${1} requies a cleanup option (ALWAYS, NEVER, PASS)" + fi + if [ "${2}" == "ALWAYS" -o "${2}" == "NEVER" -o "${2}" == "PASS" ]; then + cleanup="${2}" + else + perr "Allowed cleanup options: ALWAYS, NEVER, PASS" + fi + shift + ;; + --verbosity) + if [ $# -lt 2 ]; then + perr "${1} requires a verbosity value (0, 1, or 2)" + fi + if [ "${2}" == "0" -o "${2}" == "1" -o "${2}" == "2" ]; then + verbosity=$2 + else + perr "allowed verbosity levels are 0, 1, 2" + fi + shift + ;; + *) + perr "Unrecognized option, \"${1}\"" + ;; + esac + shift +done + +# Create the build directory, if necessary +if [ -d "${build_dir}" ]; then + # Always make sure build_dir is not in the test dir + if [ "$( cd ${build_dir}; pwd -P )" == "${currdir}" ]; then + build_dir="${build_dir}/${defdir}" + fi +else + mkdir -p ${build_dir} + res=$? + if [ $res -ne 0 ]; then + perr "Unable to create build directory, '${build_dir}'" + fi +fi +build_dir="$( cd ${build_dir}; pwd -P )" + +## framework is the CCPP Framework root dir +framework="$( cd $( dirname $( dirname ${scriptdir} ) ); pwd -P )" +frame_src="${framework}/src" + +## +## check strings for datafile command-list test +## NB: This has to be after build_dir is finalized +## +host_files="${build_dir}/ccpp/test_host_ccpp_cap.F90" +suite_files="${build_dir}/ccpp/ccpp_var_action_suite_cap.F90" +utility_files="${build_dir}/ccpp/ccpp_kinds.F90" +utility_files="${utility_files},${frame_src}/ccpp_constituent_prop_mod.F90" +utility_files="${utility_files},${frame_src}/ccpp_hashable.F90" +utility_files="${utility_files},${frame_src}/ccpp_hash_table.F90" +ccpp_files="${utility_files}" +ccpp_files="${ccpp_files},${build_dir}/ccpp/test_host_ccpp_cap.F90" +ccpp_files="${ccpp_files},${build_dir}/ccpp/ccpp_var_action_suite_cap.F90" +#process_list="" +module_list="effr_calc" +#dependencies="" +suite_list="var_action_suite" +required_vars_var_action="ccpp_error_code,ccpp_error_message" +required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_ice_particle" +required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_liquid_water_particle" +required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_rain_particle" +required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" +required_vars_var_action="${required_vars_var_action},horizontal_loop_begin" +required_vars_var_action="${required_vars_var_action},horizontal_loop_end" +required_vars_var_action="${required_vars_var_action},vertical_layer_dimension" +input_vars_var_action="effective_radius_of_stratiform_cloud_liquid_water_particle" +input_vars_var_action="${input_vars_var_action},effective_radius_of_stratiform_cloud_rain_particle" +input_vars_var_action="${input_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" +input_vars_var_action="${input_vars_var_action},horizontal_loop_begin" +input_vars_var_action="${input_vars_var_action},horizontal_loop_end" +input_vars_var_action="${input_vars_var_action},vertical_layer_dimension" +output_vars_var_action="ccpp_error_code,ccpp_error_message" +output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_ice_particle" +output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_liquid_water_particle" +output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" + +## +## Run a database report and check the return string +## $1 is the report program file +## $2 is the database file +## $3 is the report string +## $4 is the check string +## $5+ are any optional arguments +## +check_datatable() { + local checkstr=${4} + local teststr + local prog=${1} + local database=${2} + local report=${3} + shift 4 + echo "Checking ${report} report" + teststr="`${prog} ${database} ${report} $@`" + if [ "${teststr}" != "${checkstr}" ]; then + perr "datatable check:\nExpected: '${checkstr}'\nGot: '${teststr}'" + fi +} + +# cd to the build directory +cd ${build_dir} +res=$? +if [ $res -ne 0 ]; then + perr "Unable to cd to build directory, '${build_dir}'" +fi +# Clean build directory +rm -rf * +res=$? +if [ $res -ne 0 ]; then + perr "Unable to clean build directory, '${build_dir}'" +fi +# Run CMake +opts="" +if [ $verbosity -gt 0 ]; then + opts="${opts} -DVERBOSITY=${verbosity}" +fi +# Run cmake +cmake ${scriptdir} ${opts} +res=$? +if [ $res -ne 0 ]; then + perr "CMake failed with exit code, ${res}" +fi +# Test the datafile user interface +report_prog="${framework}/scripts/ccpp_datafile.py" +datafile="${build_dir}/ccpp/datatable.xml" +echo "Running python interface tests" +python3 ${scriptdir}/test_reports.py ${build_dir} ${datafile} +res=$? +if [ $res -ne 0 ]; then + perr "python interface tests failed" +fi +echo "Running command line tests" +echo "Checking required files from command line:" +check_datatable ${report_prog} ${datafile} "--host-files" ${host_files} +check_datatable ${report_prog} ${datafile} "--suite-files" ${suite_files} +check_datatable ${report_prog} ${datafile} "--utility-files" ${utility_files} +check_datatable ${report_prog} ${datafile} "--ccpp-files" ${ccpp_files} +echo -e "\nChecking lists from command line" +#check_datatable ${report_prog} ${datafile} "--process-list" ${process_list} +check_datatable ${report_prog} ${datafile} "--module-list" ${module_list} +#check_datatable ${report_prog} ${datafile} "--dependencies" ${dependencies} +check_datatable ${report_prog} ${datafile} "--suite-list" ${suite_list} \ + --sep ";" +echo -e "\nChecking variables for var_action suite from command line" +check_datatable ${report_prog} ${datafile} "--required-variables" \ + ${required_vars_var_action} "var_action_suite" +check_datatable ${report_prog} ${datafile} "--input-variables" \ + ${input_vars_var_action} "var_action_suite" +check_datatable ${report_prog} ${datafile} "--output-variables" \ + ${output_vars_var_action} "var_action_suite" +# Run make +make +res=$? +if [ $res -ne 0 ]; then + perr "make failed with exit code, ${res}" +fi +# Run test +./test_host +res=$? +if [ $res -ne 0 ]; then + perr "test_host failed with exit code, ${res}" +fi + +if [ "${cleanup}" == "ALWAYS" ]; then + docleanup +elif [ $res -eq 0 -a "${cleanup}" == "PASS" ]; then + docleanup +fi + +exit $res diff --git a/test/var_action_test/test_host.F90 b/test/var_action_test/test_host.F90 new file mode 100644 index 00000000..0ce5d6e1 --- /dev/null +++ b/test/var_action_test/test_host.F90 @@ -0,0 +1,394 @@ +module test_prog + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public test_host + + ! Public data and interfaces + integer, public, parameter :: cs = 16 + integer, public, parameter :: cm = 60 + + !> \section arg_table_suite_info Argument Table + !! \htmlinclude arg_table_suite_info.html + !! + type, public :: suite_info + character(len=cs) :: suite_name = '' + character(len=cs), pointer :: suite_parts(:) => NULL() + character(len=cm), pointer :: suite_input_vars(:) => NULL() + character(len=cm), pointer :: suite_output_vars(:) => NULL() + character(len=cm), pointer :: suite_required_vars(:) => NULL() + end type suite_info + +CONTAINS + + logical function check_list(test_list, chk_list, list_desc, suite_name) + ! Check a list () against its expected value () + + ! Dummy arguments + character(len=*), intent(in) :: test_list(:) + character(len=*), intent(in) :: chk_list(:) + character(len=*), intent(in) :: list_desc + character(len=*), optional, intent(in) :: suite_name + + ! Local variables + logical :: found + integer :: num_items + integer :: lindex, tindex + integer, allocatable :: check_unique(:) + character(len=2) :: sep + character(len=256) :: errmsg + + check_list = .true. + errmsg = '' + + ! Check the list size + num_items = size(chk_list) + if (size(test_list) /= num_items) then + write(errmsg, '(a,i0,2a)') 'ERROR: Found ', size(test_list), & + ' ', trim(list_desc) + if (present(suite_name)) then + write(errmsg(len_trim(errmsg)+1:), '(2a)') ' for suite, ', & + trim(suite_name) + end if + write(errmsg(len_trim(errmsg)+1:), '(a,i0)') ', should be ', num_items + write(6, *) trim(errmsg) + errmsg = '' + check_list = .false. + end if + + ! Now, check the list contents for 1-1 correspondence + if (check_list) then + allocate(check_unique(num_items)) + check_unique = -1 + do lindex = 1, num_items + found = .false. + do tindex = 1, num_items + if (trim(test_list(lindex)) == trim(chk_list(tindex))) then + check_unique(tindex) = lindex + found = .true. + exit + end if + end do + if (.not. found) then + check_list = .false. + write(errmsg, '(5a)') 'ERROR: ', trim(list_desc), ' item, ', & + trim(test_list(lindex)), ', was not found' + if (present(suite_name)) then + write(errmsg(len_trim(errmsg)+1:), '(2a)') ' in suite, ', & + trim(suite_name) + end if + write(6, *) trim(errmsg) + errmsg = '' + end if + end do + if (check_list .and. ANY(check_unique < 0)) then + check_list = .false. + write(errmsg, '(3a)') 'ERROR: The following ', trim(list_desc), & + ' items were not found' + if (present(suite_name)) then + write(errmsg(len_trim(errmsg)+1:), '(2a)') ' in suite, ', & + trim(suite_name) + end if + sep = '; ' + do lindex = 1, num_items + if (check_unique(lindex) < 0) then + write(errmsg(len_trim(errmsg)+1:), '(2a)') sep, & + trim(chk_list(lindex)) + sep = ', ' + end if + end do + write(6, *) trim(errmsg) + errmsg = '' + end if + end if + + end function check_list + + logical function check_suite(test_suite) + use test_host_ccpp_cap, only: ccpp_physics_suite_part_list + use test_host_ccpp_cap, only: ccpp_physics_suite_variables + + ! Dummy argument + type(suite_info), intent(in) :: test_suite + ! Local variables + integer :: sind + logical :: check + integer :: errflg + character(len=512) :: errmsg + character(len=128), allocatable :: test_list(:) + + check_suite = .true. + write(6, *) "Checking suite ", trim(test_suite%suite_name) + ! First, check the suite parts + call ccpp_physics_suite_part_list(test_suite%suite_name, test_list, & + errmsg, errflg) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_parts, 'part names', & + suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check the input variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg, input_vars=.true., output_vars=.false.) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_input_vars, & + 'input variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check the output variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg, input_vars=.false., output_vars=.true.) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_output_vars, & + 'output variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check all required variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_required_vars, & + 'required variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + end function check_suite + + + !> \section arg_table_test_host Argument Table + !! \htmlinclude arg_table_test_host.html + !! + subroutine test_host(retval, test_suites) + + use test_host_mod, only: ncols + use test_host_ccpp_cap, only: test_host_ccpp_physics_initialize + use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_initial + use test_host_ccpp_cap, only: test_host_ccpp_physics_run + use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_final + use test_host_ccpp_cap, only: test_host_ccpp_physics_finalize + use test_host_ccpp_cap, only: ccpp_physics_suite_list + use test_host_mod, only: init_data, compare_data + + type(suite_info), intent(in) :: test_suites(:) + logical, intent(out) :: retval + + logical :: check + integer :: col_start, col_end + integer :: index, sind + integer :: num_suites + character(len=128), allocatable :: suite_names(:) + character(len=512) :: errmsg + integer :: errflg + + ! Initialize our 'data' + call init_data() + + ! Gather and test the inspection routines + num_suites = size(test_suites) + call ccpp_physics_suite_list(suite_names) + retval = check_list(suite_names, test_suites(:)%suite_name, & + 'suite names') + write(6, *) 'Available suites are:' + do index = 1, size(suite_names) + do sind = 1, num_suites + if (trim(test_suites(sind)%suite_name) == & + trim(suite_names(index))) then + exit + end if + end do + write(6, '(i0,3a,i0,a)') index, ') ', trim(suite_names(index)), & + ' = test_suites(', sind, ')' + end do + if (retval) then + do sind = 1, num_suites + check = check_suite(test_suites(sind)) + retval = retval .and. check + end do + end if + !!! Return here if any check failed + if (.not. retval) then + return + end if + + ! Use the suite information to setup the run + do sind = 1, num_suites + call test_host_ccpp_physics_initialize(test_suites(sind)%suite_name, & + errmsg, errflg) + if (errflg /= 0) then + write(6, '(4a)') 'ERROR in initialize of ', & + trim(test_suites(sind)%suite_name), ': ', trim(errmsg) + end if + end do + ! Loop over time steps + !do time_step = 1, num_time_steps + ! Initialize the timestep + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_initial( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + if (errflg /= 0) then + exit + end if + end do + + do col_start = 1, ncols, 5 + if (errflg /= 0) then + exit + end if + col_end = MIN(col_start + 4, ncols) + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + do index = 1, size(test_suites(sind)%suite_parts) + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_run( & + test_suites(sind)%suite_name, & + test_suites(sind)%suite_parts(index), & + col_start, col_end, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(5a)') trim(test_suites(sind)%suite_name), & + '/', trim(test_suites(sind)%suite_parts(index)), & + ': ', trim(errmsg) + exit + end if + end do + end do + end do + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_final( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + end do + !end do ! End time step loop + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_finalize( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') test_suites(sind)%suite_parts(index), ': ', & + trim(errmsg) + write(6,'(2a)') 'An error occurred in ccpp_timestep_final, ', & + 'Exiting...' + exit + end if + end do + + if (errflg == 0) then + ! Run finished without error, check answers + if (compare_data()) then + write(6, *) 'Answers are correct!' + errflg = 0 + else + write(6, *) 'Answers are not correct!' + errflg = -1 + end if + end if + + retval = errflg == 0 + + end subroutine test_host + + end module test_prog + + program test + use test_prog, only: test_host, suite_info, cm, cs + + implicit none + + character(len=cs), target :: test_parts1(1) = (/ 'radiation ' /) + + character(len=cm), target :: test_invars1(3) = (/ & + 'effective_radius_of_stratiform_cloud_rain_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_snow_particle ' /) + + character(len=cm), target :: test_outvars1(5) = (/ & + 'ccpp_error_code ', & + 'ccpp_error_message ', & + 'effective_radius_of_stratiform_cloud_ice_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_snow_particle ' /) + + character(len=cm), target :: test_reqvars1(6) = (/ & + 'ccpp_error_code ', & + 'ccpp_error_message ', & + 'effective_radius_of_stratiform_cloud_rain_particle ', & + 'effective_radius_of_stratiform_cloud_ice_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_snow_particle ' /) + + type(suite_info) :: test_suites(1) + logical :: run_okay + + ! Setup expected test suite info + test_suites(1)%suite_name = 'var_action_suite' + test_suites(1)%suite_parts => test_parts1 + test_suites(1)%suite_input_vars => test_invars1 + test_suites(1)%suite_output_vars => test_outvars1 + test_suites(1)%suite_required_vars => test_reqvars1 + + call test_host(run_okay, test_suites) + + if (run_okay) then + STOP 0 + else + STOP -1 + end if + +end program test diff --git a/test/var_action_test/test_host.meta b/test/var_action_test/test_host.meta new file mode 100644 index 00000000..5d861764 --- /dev/null +++ b/test/var_action_test/test_host.meta @@ -0,0 +1,38 @@ +[ccpp-table-properties] + name = suite_info + type = ddt +[ccpp-arg-table] + name = suite_info + type = ddt + +[ccpp-table-properties] + name = test_host + type = host +[ccpp-arg-table] + name = test_host + type = host +[ col_start ] + standard_name = horizontal_loop_begin + type = integer + units = count + dimensions = () + protected = True +[ col_end ] + standard_name = horizontal_loop_end + type = integer + units = count + dimensions = () + protected = True +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer diff --git a/test/var_action_test/test_host_data.F90 b/test/var_action_test/test_host_data.F90 new file mode 100644 index 00000000..9a17eb4f --- /dev/null +++ b/test/var_action_test/test_host_data.F90 @@ -0,0 +1,40 @@ +module test_host_data + + use ccpp_kinds, only: kind_phys + + !> \section arg_table_physics_state Argument Table + !! \htmlinclude arg_table_physics_state.html + type physics_state + real(kind_phys), dimension(:,:), allocatable :: & + effrr, & ! effective radius of cloud rain + effrl, & ! effective radius of cloud liquid water + effri ! effective radius of cloud ice + end type physics_state + + public allocate_physics_state + +contains + + subroutine allocate_physics_state(cols, levels, state) + integer, intent(in) :: cols + integer, intent(in) :: levels + type(physics_state), intent(out) :: state + + if (allocated(state%effrr)) then + deallocate(state%effrr) + end if + allocate(state%effrr(cols, levels)) + + if (allocated(state%effrl)) then + deallocate(state%effrl) + end if + allocate(state%effrl(cols, levels)) + + if (allocated(state%effri)) then + deallocate(state%effri) + end if + allocate(state%effri(cols, levels)) + + end subroutine allocate_physics_state + +end module test_host_data diff --git a/test/var_action_test/test_host_data.meta b/test/var_action_test/test_host_data.meta new file mode 100644 index 00000000..4a98245f --- /dev/null +++ b/test/var_action_test/test_host_data.meta @@ -0,0 +1,27 @@ +[ccpp-table-properties] + name = physics_state + type = ddt +[ccpp-arg-table] + name = physics_state + type = ddt +[effrr] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys +[effrl] + standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle + long_name = effective radius of cloud liquid water particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys +[effri] + standard_name = effective_radius_of_stratiform_cloud_ice_particle + long_name = effective radius of cloud ice water particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys diff --git a/test/var_action_test/test_host_mod.F90 b/test/var_action_test/test_host_mod.F90 new file mode 100644 index 00000000..c10a4146 --- /dev/null +++ b/test/var_action_test/test_host_mod.F90 @@ -0,0 +1,69 @@ +module test_host_mod + + use ccpp_kinds, only: kind_phys + use test_host_data, only: physics_state, allocate_physics_state + + implicit none + public + + !> \section arg_table_test_host_mod Argument Table + !! \htmlinclude arg_table_test_host_host.html + !! + integer, parameter :: ncols = 12 + integer, parameter :: pver = 4 + type(physics_state) :: phys_state + real(kind_phys) :: effrs(ncols, pver) + + public :: init_data + public :: compare_data + +contains + + subroutine init_data() + + ! Allocate and initialize state + call allocate_physics_state(ncols, pver, phys_state) + phys_state%effrr = 1.0E-3 ! 1000 microns, in meter + phys_state%effrl = 1.0E-4 ! 100 microns, in meter + phys_state%effri = 5.0E-5 ! 50 microns, in meter + effrs = 5.0E-4 ! 500 microns, in meter + + end subroutine init_data + + logical function compare_data() + + real(kind_phys), parameter :: effrr_expected = 1.0E-3 ! 1000 microns, in meter + real(kind_phys), parameter :: effrl_expected = 5.0E-5 ! 50 microns, in meter + real(kind_phys), parameter :: effri_expected = 7.5E-5 ! 75 microns, in meter + real(kind_phys), parameter :: effrs_expected = 5.1E-4 ! 510 microns, in meter + real(kind_phys), parameter :: tolerance = 1.0E-3 ! used as scaling factor for expected value + + compare_data = .true. + + if (maxval(abs(phys_state%effrr - effrr_expected)) > tolerance*effrr_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effrr from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effrr - effrr_expected)), ' > ', tolerance*effrr_expected + compare_data = .false. + end if + + if (maxval(abs(phys_state%effrl - effrl_expected)) > tolerance*effrl_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effrl from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effrl - effrl_expected)), ' > ', tolerance*effrl_expected + compare_data = .false. + end if + + if (maxval(abs(phys_state%effri - effri_expected)) > tolerance*effri_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effri from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effri - effri_expected)), ' > ', tolerance*effri_expected + compare_data = .false. + end if + + if (maxval(abs( effrs - effrs_expected)) > tolerance*effrs_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of effrs from expected value exceeds tolerance: ', & + maxval(abs( effrs - effrs_expected)), ' > ', tolerance*effrs_expected + compare_data = .false. + end if + + end function compare_data + +end module test_host_mod diff --git a/test/var_action_test/test_host_mod.meta b/test/var_action_test/test_host_mod.meta new file mode 100644 index 00000000..0aff8e2f --- /dev/null +++ b/test/var_action_test/test_host_mod.meta @@ -0,0 +1,30 @@ +[ccpp-table-properties] + name = test_host_mod + type = module +[ccpp-arg-table] + name = test_host_mod + type = module +[ ncols] + standard_name = horizontal_dimension + units = count + type = integer + protected = True + dimensions = () +[ pver ] + standard_name = vertical_layer_dimension + units = count + type = integer + protected = True + dimensions = () +[ phys_state ] + standard_name = physics_state_derived_type + long_name = Physics State DDT + type = physics_state + dimensions = () +[effrs] + standard_name = effective_radius_of_stratiform_cloud_snow_particle + long_name = effective radius of cloud snow particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys diff --git a/test/var_action_test/test_reports.py b/test/var_action_test/test_reports.py new file mode 100755 index 00000000..85121595 --- /dev/null +++ b/test/var_action_test/test_reports.py @@ -0,0 +1,157 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Test capgen database report python interface + + Assumptions: + + Command line arguments: build_dir database_filepath + + Usage: python test_reports +----------------------------------------------------------------------- +""" +import sys +import os + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_FRAMEWORK_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, os.pardir)) +_SCRIPTS_DIR = os.path.join(_FRAMEWORK_DIR, "scripts") +_SRC_DIR = os.path.join(_FRAMEWORK_DIR, "src") + +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError("Cannot find scripts directory") +# end if + +if ((sys.version_info[0] < 3) or + (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): + raise Exception("Python 3.7 or greater required") +# end if + +sys.path.append(_SCRIPTS_DIR) +# pylint: disable=wrong-import-position +from ccpp_datafile import datatable_report, DatatableReport +# pylint: enable=wrong-import-position + +def usage(errmsg=None): + """Raise an exception with optional error message and usage message""" + emsg = "usage: {} " + if errmsg: + emsg = errmsg + '\n' + emsg + # end if + raise ValueError(emsg.format(sys.argv[0])) + +if len(sys.argv) != 3: + usage() +# end if + +_BUILD_DIR = os.path.abspath(sys.argv[1]) +_DATABASE = os.path.abspath(sys.argv[2]) +if not os.path.isdir(_BUILD_DIR): + _EMSG = " must be an existing build directory" + usage(_EMSG) +# end if +if (not os.path.exists(_DATABASE)) or (not os.path.isfile(_DATABASE)): + _EMSG = " must be an existing CCPP database file" + usage(_EMSG) +# end if + +# Check data +_HOST_FILES = [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90")] +_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_action_suite_cap.F90")] +_UTILITY_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_kinds.F90"), + os.path.join(_SRC_DIR, "ccpp_constituent_prop_mod.F90"), + os.path.join(_SRC_DIR, "ccpp_hashable.F90"), + os.path.join(_SRC_DIR, "ccpp_hash_table.F90")] +_CCPP_FILES = _UTILITY_FILES + \ + [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), + os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_action_suite_cap.F90")] +#_PROCESS_LIST = [] +_MODULE_LIST = ["effr_calc"] +_SUITE_LIST = ["var_action_suite"] +_INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "vertical_layer_dimension", + "effective_radius_of_stratiform_cloud_liquid_water_particle", + "effective_radius_of_stratiform_cloud_rain_particle", + "effective_radius_of_stratiform_cloud_snow_particle"] +_OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", + "effective_radius_of_stratiform_cloud_ice_particle", + "effective_radius_of_stratiform_cloud_liquid_water_particle", + "effective_radius_of_stratiform_cloud_snow_particle"] +_REQUIRED_VARS_VAR_ACTION = _INPUT_VARS_VAR_ACTION + _OUTPUT_VARS_VAR_ACTION + +def fields_string(field_type, field_list, sep): + """Create an error string for field(s), . + is used to separate items in """ + indent = ' '*11 + if field_list: + if len(field_list) > 1: + field_str = "{} Fields: ".format(field_type) + else: + field_str = "{} Field: ".format(field_type) + # end if + fmsg = "\n{}{}{}".format(indent, field_str, sep.join(field_list)) + else: + fmsg = "" + # end if + return fmsg + +def check_datatable(database, report_type, check_list, + sep=',', excl_prot=False): + """Run a database report and check the return string. + If an error is found, print an error message. + Return the number of errors""" + if sep is None: + sep = ',' + # end if + test_str = datatable_report(database, report_type, sep, excl_prot=excl_prot) + test_list = [x for x in test_str.split(sep) if x] + missing = list() + unexpected = list() + for item in check_list: + if item not in test_list: + missing.append(item) + # end if + # end for + for item in test_list: + if item not in check_list: + unexpected.append(item) + # end if + # end for + if missing or unexpected: + vmsg = "ERROR in {} datafile check:".format(report_type.action) + vmsg += fields_string("Missing", missing, sep) + vmsg += fields_string("Unexpected", unexpected, sep) + print(vmsg) + else: + print("{} report okay".format(report_type.action)) + # end if + return len(missing) + len(unexpected) + +NUM_ERRORS = 0 +print("Checking required files from python:") +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("host_files"), + _HOST_FILES) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_files"), + _SUITE_FILES) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("utility_files"), + _UTILITY_FILES) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("ccpp_files"), + _CCPP_FILES) +print("\nChecking lists from python") +#NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("process_list"), +# _PROCESS_LIST) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("module_list"), + _MODULE_LIST) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_list"), + _SUITE_LIST) +print("\nChecking variables for var_action suite from python") +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("required_variables", + value="var_action_suite"), + _REQUIRED_VARS_VAR_ACTION) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("input_variables", + value="var_action_suite"), + _INPUT_VARS_VAR_ACTION) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("output_variables", + value="var_action_suite"), + _OUTPUT_VARS_VAR_ACTION) + +sys.exit(NUM_ERRORS) diff --git a/test/var_action_test/var_action_files.txt b/test/var_action_test/var_action_files.txt new file mode 100644 index 00000000..5f7db5b2 --- /dev/null +++ b/test/var_action_test/var_action_files.txt @@ -0,0 +1 @@ +effr_calc.meta diff --git a/test/var_action_test/var_action_suite.xml b/test/var_action_test/var_action_suite.xml new file mode 100644 index 00000000..72a7b80f --- /dev/null +++ b/test/var_action_test/var_action_suite.xml @@ -0,0 +1,7 @@ + + + + + effr_calc + + From 2e2d5da88c5c7f3ae65e1a7be00959ab98325121 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Fri, 21 Jan 2022 20:52:56 -0700 Subject: [PATCH 02/49] Added several unit tests for checking compatibility/unit conversions to metavar.py and var_props.py --- scripts/metavar.py | 12 +++++++++++- scripts/var_props.py | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) mode change 100644 => 100755 scripts/var_props.py diff --git a/scripts/metavar.py b/scripts/metavar.py index b50e806b..71609580 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -142,9 +142,19 @@ class Var: >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'ino'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ParseSyntaxError: Invalid intent variable property, 'ino', at :1 - >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm/s', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ParseSyntaxError: Invalid variable property name, 'optional', at :1 + # Check that two variables that differ in their units - m vs km - are compatible + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm', \ + 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, \ + ParseSource('vname', 'SCHEME', ParseContext()), \ + _MVAR_DUMMY_RUN_ENV).compatible(Var({'local_name' : 'bar', \ + 'standard_name' : 'hi_mom', 'units' : 'km', \ + 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, \ + ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV), \ + _MVAR_DUMMY_RUN_ENV) #doctest: +ELLIPSIS + """ ## Prop lists below define all the allowed CCPP Metadata attributes diff --git a/scripts/var_props.py b/scripts/var_props.py old mode 100644 new mode 100755 index b7d7b16f..4e2cadb5 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -764,6 +764,32 @@ class VarCompatObj: "real", "kind_phys", "m", ['horizontal_loop_extent'], \ "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS <__main__.VarCompatObj object at 0x...> + + # Test that a 2-D var with unit conversion m->km works + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ + ['horizontal_dimension'], "var1_lname", "var_stdname", \ + "real", "kind_phys", "km", ['horizontal_dimension'], \ + "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS + <__main__.VarCompatObj object at 0x...> + + # Test that a 2-D var with unit conversion m->km works and that it + # produces the correct forward transformation + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ + ['horizontal_dimension'], "var1_lname", "var_stdname", \ + "real", "kind_phys", "km", ['horizontal_dimension'], \ + "var2_lname", _DOCTEST_RUNENV).forward_transform( \ + "var1_lname", "var2_lname", ('i')) + 'var1_lname(i) = 1.0E-3_kind_phys*var2_lname(i)' + + # Test that a 3-D var with unit conversion m->km and vertical flipping + # works and that it produces the correct reverse transformation + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ + ['horizontal_dimension', 'vertical_layer_dimension'], \ + "var1_lname", "var_stdname", "real", "kind_phys", "km",\ + ['horizontal_dimension', 'vertical_layer_dimension'], \ + "var2_lname", _DOCTEST_RUNENV).reverse_transform( \ + "var1_lname", "var2_lname", ('i','k'), flip_vdim='nk') + 'var1_lname(i,nk-k+1) = 1.0E+3_kind_phys*var2_lname(i,k)' """ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, From a4f2883e7c5c89e7eb08af5a4646ad3f088dce6d Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Wed, 23 Feb 2022 20:20:47 -0700 Subject: [PATCH 03/49] Address reviewer comments --- test/var_action_test/test_host.F90 | 110 ++++++++++++------------- test/var_action_test/test_host_mod.F90 | 2 +- test/var_action_test/test_reports.py | 3 - 3 files changed, 55 insertions(+), 60 deletions(-) diff --git a/test/var_action_test/test_host.F90 b/test/var_action_test/test_host.F90 index 0ce5d6e1..885c2e62 100644 --- a/test/var_action_test/test_host.F90 +++ b/test/var_action_test/test_host.F90 @@ -245,72 +245,70 @@ subroutine test_host(retval, test_suites) trim(test_suites(sind)%suite_name), ': ', trim(errmsg) end if end do - ! Loop over time steps - !do time_step = 1, num_time_steps - ! Initialize the timestep - do sind = 1, num_suites - if (errflg /= 0) then - exit - end if - if (errflg == 0) then - call test_host_ccpp_physics_timestep_initial( & - test_suites(sind)%suite_name, errmsg, errflg) - end if - if (errflg /= 0) then - write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & - trim(errmsg) - exit - end if - if (errflg /= 0) then - exit - end if - end do - do col_start = 1, ncols, 5 + ! Initialize the timestep + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_initial( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + if (errflg /= 0) then + exit + end if + end do + + do col_start = 1, ncols, 5 + if (errflg /= 0) then + exit + end if + col_end = MIN(col_start + 4, ncols) + + do sind = 1, num_suites if (errflg /= 0) then exit end if - col_end = MIN(col_start + 4, ncols) - - do sind = 1, num_suites + do index = 1, size(test_suites(sind)%suite_parts) + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_run( & + test_suites(sind)%suite_name, & + test_suites(sind)%suite_parts(index), & + col_start, col_end, errmsg, errflg) + end if if (errflg /= 0) then + write(6, '(5a)') trim(test_suites(sind)%suite_name), & + '/', trim(test_suites(sind)%suite_parts(index)), & + ': ', trim(errmsg) exit end if - do index = 1, size(test_suites(sind)%suite_parts) - if (errflg /= 0) then - exit - end if - if (errflg == 0) then - call test_host_ccpp_physics_run( & - test_suites(sind)%suite_name, & - test_suites(sind)%suite_parts(index), & - col_start, col_end, errmsg, errflg) - end if - if (errflg /= 0) then - write(6, '(5a)') trim(test_suites(sind)%suite_name), & - '/', trim(test_suites(sind)%suite_parts(index)), & - ': ', trim(errmsg) - exit - end if - end do end do end do + end do - do sind = 1, num_suites - if (errflg /= 0) then - exit - end if - if (errflg == 0) then - call test_host_ccpp_physics_timestep_final( & - test_suites(sind)%suite_name, errmsg, errflg) - end if - if (errflg /= 0) then - write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & - trim(errmsg) - exit - end if - end do - !end do ! End time step loop + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_final( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + end do do sind = 1, num_suites if (errflg /= 0) then diff --git a/test/var_action_test/test_host_mod.F90 b/test/var_action_test/test_host_mod.F90 index c10a4146..d6191814 100644 --- a/test/var_action_test/test_host_mod.F90 +++ b/test/var_action_test/test_host_mod.F90 @@ -36,7 +36,7 @@ logical function compare_data() real(kind_phys), parameter :: effrl_expected = 5.0E-5 ! 50 microns, in meter real(kind_phys), parameter :: effri_expected = 7.5E-5 ! 75 microns, in meter real(kind_phys), parameter :: effrs_expected = 5.1E-4 ! 510 microns, in meter - real(kind_phys), parameter :: tolerance = 1.0E-3 ! used as scaling factor for expected value + real(kind_phys), parameter :: tolerance = 1.0E-10 ! used as scaling factor for expected value compare_data = .true. diff --git a/test/var_action_test/test_reports.py b/test/var_action_test/test_reports.py index 85121595..3d9a8637 100755 --- a/test/var_action_test/test_reports.py +++ b/test/var_action_test/test_reports.py @@ -65,7 +65,6 @@ def usage(errmsg=None): _CCPP_FILES = _UTILITY_FILES + \ [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_action_suite_cap.F90")] -#_PROCESS_LIST = [] _MODULE_LIST = ["effr_calc"] _SUITE_LIST = ["var_action_suite"] _INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "vertical_layer_dimension", @@ -137,8 +136,6 @@ def check_datatable(database, report_type, check_list, NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("ccpp_files"), _CCPP_FILES) print("\nChecking lists from python") -#NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("process_list"), -# _PROCESS_LIST) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("module_list"), _MODULE_LIST) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_list"), From c1c68110af2c7a5801887e1242e08e3ab0bd7491 Mon Sep 17 00:00:00 2001 From: Steve Goldhaber Date: Fri, 23 Dec 2022 16:29:30 +0100 Subject: [PATCH 04/49] Fix error message for unit conversions involving unit "1" --- scripts/var_props.py | 124 ++++++++++++++++--------- test/unit_tests/test_var_transforms.py | 12 +++ 2 files changed, 90 insertions(+), 46 deletions(-) diff --git a/scripts/var_props.py b/scripts/var_props.py index 4e2cadb5..69b6b766 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -133,18 +133,22 @@ def standard_name_to_long_name(prop_dict, context=None): """Translate a standard_name to its default long_name >>> standard_name_to_long_name({'standard_name':'cloud_optical_depth_layers_from_0p55mu_to_0p99mu'}) 'Cloud optical depth layers from 0.55mu to 0.99mu' - >>> standard_name_to_long_name({'local_name':'foo'}) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> standard_name_to_long_name({'local_name':'foo'}) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No standard name to convert foo to long name - >>> standard_name_to_long_name({}) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No standard name to convert foo to long name + >>> standard_name_to_long_name({}) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No standard name to convert to long name - >>> standard_name_to_long_name({'local_name':'foo'}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No standard name to convert to long name + >>> standard_name_to_long_name({'local_name':'foo'}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No standard name to convert foo to long name at foo.F90:3 - >>> standard_name_to_long_name({}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No standard name to convert foo to long name, at foo.F90:4 + >>> standard_name_to_long_name({}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No standard name to convert to long name at foo.F90:3 + ... + parse_source.CCPPError: No standard name to convert to long name, at foo.F90:4 """ # We assume that standar_name has been checked for validity # Make the first char uppercase and replace each underscore with a space @@ -191,18 +195,22 @@ def default_kind_val(prop_dict, context=None): '' >>> default_kind_val({'type':'logical'}) '' - >>> default_kind_val({'local_name':'foo'}) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> default_kind_val({'local_name':'foo'}) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No type to find default kind for foo - >>> default_kind_val({}) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No type to find default kind for foo + >>> default_kind_val({}) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No type to find default kind - >>> default_kind_val({'local_name':'foo'}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No type to find default kind + >>> default_kind_val({'local_name':'foo'}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No type to find default kind for foo at foo.F90:3 - >>> default_kind_val({}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: No type to find default kind for foo, at foo.F90:4 + >>> default_kind_val({}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: No type to find default kind at foo.F90:3 + ... + parse_source.CCPPError: No type to find default kind, at foo.F90:4 """ if 'type' in prop_dict: vtype = prop_dict['type'].lower() @@ -277,29 +285,34 @@ def __init__(self, forward_permutation, reverse_permutation, # Test that bad inputs are trapped: >>> DimTransform((0, 1, 2), (2, 1), 'horizontal_dimension', 0, 1, \ 'horizontal_dimension', \ - 1, 0) #doctest: +IGNORE_EXCEPTION_DETAIL + 1, 0) #doctest: +ELLIPSIS Traceback (most recent call last): + ... parse_source.ParseInternalError: Permutation mismatch, '(0, 1, 2)' and '(2, 1)' - >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 3, 4, \ + >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 3, 2, \ 'horizontal_dimension', \ - 4, 3) #doctest: +IGNORE_EXCEPTION_DETAIL + 4, 3) #doctest: +ELLIPSIS Traceback (most recent call last): + ... parse_source.ParseInternalError: forward_hdim_index (3) out of range [0, 2] >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 0, 4, \ 'horizontal_dimension', \ - 4, 3) #doctest: +IGNORE_EXCEPTION_DETAIL + 4, 3) #doctest: +ELLIPSIS Traceback (most recent call last): - parse_source.ParseInternalError: forward_vdim_index (4) out of range [0, 2 + ... + parse_source.ParseInternalError: forward_vdim_index (4) out of range [0, 2] >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 0, 2, \ 'horizontal_dimension', \ - 4, 3) #doctest: +IGNORE_EXCEPTION_DETAIL + 4, 3) #doctest: +ELLIPSIS Traceback (most recent call last): + ... parse_source.ParseInternalError: reverse_hdim_index (4) out of range [0, 2] - >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 3, 4, \ + >>> DimTransform((2, 0, 1), (1, 2, 0), 'horizontal_dimension', 1, 2, \ 'horizontal_dimension', \ - 0, 3) #doctest: +IGNORE_EXCEPTION_DETAIL + 0, 3) #doctest: +ELLIPSIS Traceback (most recent call last): - parse_source.ParseInternalError: forward_hdim_index (3) out of range [0, 2] + ... + parse_source.ParseInternalError: reverse_vdim_index (3) out of range [0, 2] """ # Store inputs if len(forward_permutation) != len(reverse_permutation): @@ -523,7 +536,7 @@ class VariableProperty: <__main__.VariableProperty object at ...> >>> VariableProperty('local_name', str).name 'local_name' - >>> VariableProperty('standard_name', str).type == str + >>> VariableProperty('standard_name', str).ptype == str True >>> VariableProperty('units', str).is_match('units') True @@ -535,16 +548,18 @@ class VariableProperty: 2 >>> VariableProperty('value', int, valid_values_in=[1, 2 ]).valid_value('3') - >>> VariableProperty('value', int, valid_values_in=[1, 2 ]).valid_value('3', error=True) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> VariableProperty('value', int, valid_values_in=[1, 2 ]).valid_value('3', error=True) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: Invalid value variable property, '3' + ... + parse_source.CCPPError: Invalid value variable property, '3' >>> VariableProperty('units', str, check_fn_in=check_units).valid_value('m s-1') 'm s-1' >>> VariableProperty('units', str, check_fn_in=check_units).valid_value(' ') - >>> VariableProperty('units', str, check_fn_in=check_units).valid_value(' ', error=True) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> VariableProperty('units', str, check_fn_in=check_units).valid_value(' ', error=True) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: ' ' is not a valid unit + ... + parse_source.CCPPError: ' ' is not a valid unit >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('()') [] >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('(x)') @@ -557,12 +572,14 @@ class VariableProperty: ['w:x', 'y:z'] >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value(['size(foo)']) ['size(foo)'] - >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('(w:x,x:y:z:q)', error=True) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('(w:x,x:y:z:q)', error=True) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: 'x:y:z:q' is an invalid dimension range - >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('(x:3y)', error=True) #doctest: +IGNORE_EXCEPTION_DETAIL + ... + parse_source.CCPPError: 'x:y:z:q' is an invalid dimension range + >>> VariableProperty('dimensions', list, check_fn_in=check_dimensions).valid_value('(x:3y)', error=True) #doctest: +ELLIPSIS Traceback (most recent call last): - CCPPError: '3y' is not a valid Fortran identifier + ... + parse_source.CCPPError: '3y' is not a valid Fortran identifier >>> VariableProperty('local_name', str, check_fn_in=check_local_name).valid_value('foo') 'foo' >>> VariableProperty('local_name', str, check_fn_in=check_local_name).valid_value('foo(bar)') @@ -612,7 +629,7 @@ def name(self): return self._name @property - def type(self): + def ptype(self): """Return the type of the property""" return self._type @@ -659,7 +676,7 @@ def valid_value(self, test_value, prop_dict=None, error=False): If is not None, it may be used in value validation. """ valid_val = None - if self.type is int: + if self.ptype is int: try: tval = int(test_value) if self._valid_values is not None: @@ -671,7 +688,7 @@ def valid_value(self, test_value, prop_dict=None, error=False): valid_val = tval except CCPPError: valid_val = None # Redundant but more expressive than pass - elif self.type is list: + elif self.ptype is list: if isinstance(test_value, str): tval = fortran_list_match(test_value) if tval and (len(tval) == 1) and (not tval[0]): @@ -698,7 +715,7 @@ def valid_value(self, test_value, prop_dict=None, error=False): # end for else: pass - elif self.type is bool: + elif self.ptype is bool: if isinstance(test_value, str): if test_value.lower() in VariableProperty.__true_vals + VariableProperty.__false_vals: valid_val = test_value.lower() in VariableProperty.__true_vals @@ -707,7 +724,7 @@ def valid_value(self, test_value, prop_dict=None, error=False): # end if else: valid_val = not not test_value # pylint: disable=unneeded-not - elif self.type is str: + elif self.ptype is str: if isinstance(test_value, str): if self._valid_values is not None: if test_value in self._valid_values: @@ -1037,9 +1054,16 @@ def _get_unit_convstrs(self, var1_units, var2_units): >>> _DOCTEST_VCOMPAT._get_unit_convstrs('C', 'K') ('{var}+273.15{kind}', '{var}-273.15{kind}') + # Try an invalid conversion + >>> _DOCTEST_VCOMPAT._get_unit_convstrs('1', 'none') #doctest: +ELLIPSIS + Traceback (most recent call last): + ... + parse_source.ParseSyntaxError: Unsupported unit conversion, '1' to 'none' for 'var_stdname' + # Try an unsupported conversion - >>> _DOCTEST_VCOMPAT._get_unit_convstrs('C', 'm') #doctest: +IGNORE_EXCEPTION_DETAIL + >>> _DOCTEST_VCOMPAT._get_unit_convstrs('C', 'm') #doctest: +ELLIPSIS Traceback (most recent call last): + ... parse_source.ParseSyntaxError: Unsupported unit conversion, 'C' to 'm' for 'var_stdname' """ u1_str = self.units_to_string(var1_units, self.__v1_context) @@ -1183,8 +1207,7 @@ def char_kind_check(kind_str): # end if (no else, kind_ok already False) return kind_ok - @staticmethod - def units_to_string(units, context=None): + def units_to_string(self, units, context=None): """Replace variable unit description with string that is a legal Python identifier. If the resulting string is a Python keyword, raise an exception.""" @@ -1194,11 +1217,15 @@ def units_to_string(units, context=None): string = string.replace("-","_minus_") # Replace each plus sign with '_plus_' string = string.replace("+","_plus_") + # "1" is a valid unit + if string == "1": + string = "one" + # end if # Test that the resulting string is a valid Python identifier if not string.isidentifier(): - emsg = "Unsupported units entry, '{}'{}" + emsg = "Unsupported units entry for {}, '{}'{}" ctx = context_string(context) - raise ParseSyntaxError(emsg.format(units ,ctx)) + raise ParseSyntaxError(emsg.format(self.__stdname, units ,ctx)) # end if # Test that the resulting string is NOT a Python keyword if keyword.iskeyword(string): @@ -1311,10 +1338,15 @@ def __bool__(self): kind_types=["kind_phys=REAL64", "kind_dyn=REAL32", "kind_host=REAL64"]) + _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') + _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", "m", [], "var1_lname", "var_stdname", "real", "kind_phys", "m", [], - "var2_lname", _DOCTEST_RUNENV) - fail, _ = doctest.testmod() + "var2_lname", _DOCTEST_RUNENV, + v1_context=_DOCTEST_CONTEXT1, + v2_context=_DOCTEST_CONTEXT2) + OPTIONS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE + fail, _ = doctest.testmod(optionflags=OPTIONS) sys.exit(fail) # end if diff --git a/test/unit_tests/test_var_transforms.py b/test/unit_tests/test_var_transforms.py index 5178ce9b..70f31d1e 100644 --- a/test/unit_tests/test_var_transforms.py +++ b/test/unit_tests/test_var_transforms.py @@ -163,12 +163,24 @@ def test_unsupported_unit_change(self): 'real', vkind='kind_phys') real_scalar2 = self._new_var('real_stdname1', 'd', [], 'real', vkind='kind_phys') + char_nounit1 = self._new_var('char_stdname1', 'none', [], + 'character', vkind='len=256') + char_nounit2 = self._new_var('char_stdname1', '1', [], + 'character', vkind='len=256') with self.assertRaises(ParseSyntaxError) as context: compat = real_scalar1.compatible(real_scalar2, self.__run_env) # end with + #Test bad conversion for real time variables #Verify correct error message returned emsg = "Unsupported unit conversion, 'min' to 'd' for 'real_stdname1'" self.assertTrue(emsg in str(context.exception)) + #Test bad conversion for unitless variables + with self.assertRaises(ParseSyntaxError) as context: + compat = char_nounit1.compatible(char_nounit2, self.__run_env) + # end with + #Verify correct error message returned + emsg = "Unsupported unit conversion, 'none' to '1' for 'char_stdname1'" + self.assertTrue(emsg in str(context.exception)) def test_valid_kind_change(self): """Test that valid kind changes are detected""" From 69e2bf95e2a4aa0f1b64553b010533d9b2632e1e Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 21 Aug 2023 13:33:43 -0600 Subject: [PATCH 05/49] initial stab at constituents-only PR --- scripts/ccpp_capgen.py | 36 +- scripts/ccpp_datafile.py | 3 +- scripts/ccpp_suite.py | 20 +- scripts/constituents.py | 310 +-- scripts/host_cap.py | 180 +- scripts/host_model.py | 47 +- scripts/metavar.py | 67 +- scripts/parse_tools/__init__.py | 3 +- scripts/parse_tools/parse_checkers.py | 53 + scripts/suite_objects.py | 16 + scripts/var_props.py | 20 +- src/ccpp_constituent_prop_mod.F90 | 2399 ++++++++++++++++-------- src/ccpp_constituent_prop_mod.meta | 47 + test/advection_test/test_host.F90 | 274 ++- test/advection_test/test_host_data.F90 | 19 +- test/advection_test/test_host_mod.F90 | 38 +- 16 files changed, 2438 insertions(+), 1094 deletions(-) create mode 100644 src/ccpp_constituent_prop_mod.meta diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 7a12729a..8938a43f 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -26,11 +26,13 @@ from host_model import HostModel from metadata_table import parse_metadata_file, SCHEME_HEADER_TYPE from parse_tools import init_log, set_log_level, context_string +from parse_tools import register_fortran_ddt_name from parse_tools import CCPPError, ParseInternalError ## Capture the Framework root -__SCRIPT_PATH = os.path.dirname(__file__) -__FRAMEWORK_ROOT = os.path.abspath(os.path.join(__SCRIPT_PATH, os.pardir)) +_SCRIPT_PATH = os.path.dirname(__file__) +_FRAMEWORK_ROOT = os.path.abspath(os.path.join(__SCRIPT_PATH, os.pardir)) +_SRC_ROOT = os.path.join(_FRAMEWORK_ROOT, "src") ## Init this now so that all Exceptions can be trapped _LOGGER = init_log(os.path.basename(__file__)) @@ -43,6 +45,11 @@ ## Metadata table types where order is significant _ORDERED_TABLE_TYPES = [SCHEME_HEADER_TYPE] +## CCPP Framework supported DDT types +_CCPP_FRAMEWORK_DDT_TYPES = ["ccpp_hash_table_t", + "ccpp_hashable_t", + "ccpp_hashable_char_t"] + ############################################################################### def delete_pathnames_from_file(capfile, logger): ############################################################################### @@ -578,12 +585,22 @@ def capgen(run_env): # Try to create output_dir (let it crash if it fails) os.makedirs(run_env.output_dir) # end if + # Pre-register base CCPP DDT types: + for ddt_name in _CCPP_FRAMEWORK_DDT_TYPES: + register_fortran_ddt_name(ddt_name) + # end for + src_dir = os.path.join(_FRAMEWORK_ROOT, "src") host_files = run_env.host_files host_name = run_env.host_name scheme_files = run_env.scheme_files # We need to create three lists of files, hosts, schemes, and SDFs host_files = create_file_list(run_env.host_files, ['meta'], 'Host', run_env.logger) + # The host model needs to know about the constituents module + const_mod = os.path.join(_SRC_ROOT, "ccpp_constituent_prop_mod.meta") + if const_mod not in host_files: + host_files.append(const_mod) + # end if scheme_files = create_file_list(run_env.scheme_files, ['meta'], 'Scheme', run_env.logger) sdfs = create_file_list(run_env.suites, ['xml'], 'Suite', run_env.logger) @@ -594,11 +611,19 @@ def capgen(run_env): # end if # First up, handle the host files host_model = parse_host_model_files(host_files, host_name, run_env) + # We always need to parse the ccpp_constituent_prop_ptr_t DDT + ##XXgoldyXX: Should this be in framework_env.py? + const_prop_mod = os.path.join(src_dir, "ccpp_constituent_prop_mod.meta") + if const_prop_mod and not in scheme_files: + scheme_files = [const_prop_mod] + scheme_files + # end if # Next, parse the scheme files scheme_headers, scheme_tdict = parse_scheme_files(scheme_files, run_env) - ddts = host_model.ddt_lib.keys() - if ddts and run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): - run_env.logger.debug("DDT definitions = {}".format(ddts)) + if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + ddts = host_model.ddt_lib.keys() + if ddts: + run_env.logger.debug("DDT definitions = {}".format(ddts)) + # end if # end if plist = host_model.prop_list('local_name') if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): @@ -646,7 +671,6 @@ def capgen(run_env): # end if # Finally, create the database of generated files and caps # This can be directly in output_dir because it will not affect dependencies - src_dir = os.path.join(__FRAMEWORK_ROOT, "src") generate_ccpp_datatable(run_env, host_model, ccpp_api, scheme_headers, scheme_tdict, host_files, cap_filenames, kinds_file, src_dir) diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 158d9dec..7acd37f1 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -657,7 +657,8 @@ def _new_var_entry(parent, var, full_entry=True): prop_list.extend(["allocatable", "active", "default_value", "diagnostic_name", "diagnostic_name_fixed", "kind", "persistence", "polymorphic", "protected", - "state_variable", "type", "units"]) + "state_variable", "type", "units", "molar_mass", + "advected"]) prop_list.extend(Var.constituent_property_names()) # end if ventry = ET.SubElement(parent, "var") diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 8b9a1aeb..d28a026f 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -810,32 +810,46 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): input_vars = [set(), set(), set()] # leaves, arrrays, leaf elements inout_vars = [set(), set(), set()] # leaves, arrrays, leaf elements output_vars = [set(), set(), set()] # leaves, arrrays, leaf elements + const_initialized_in_physics = {} for part in suite.groups: for var in part.call_list.variable_list(): + phase = part.phase() stdname = var.get_prop_value("standard_name") intent = var.get_prop_value("intent") protected = var.get_prop_value("protected") + constituent = var.is_constituent() + if stdname not in const_initialized_in_physics: + const_initialized_in_physics[stdname] = False + # end if if (parent is not None) and (not protected): pvar = parent.find_variable(standard_name=stdname) if pvar is not None: protected = pvar.get_prop_value("protected") # end if # end if - elements = var.intrinsic_elements(check_dict=self.parent) - if (intent == 'in') and (not protected): + elements = var.intrinsic_elements(check_dict=self.parent, + ddt_lib=self.__ddt_lib) + if (intent == 'in') and (not protected) and (not const_initialized_in_physics[stdname]): if isinstance(elements, list): input_vars[1].add(stdname) input_vars[2].update(elements) else: input_vars[0].add(stdname) # end if - elif intent == 'inout': + elif intent == 'inout' and (not const_initialized_in_physics[stdname]): if isinstance(elements, list): inout_vars[1].add(stdname) inout_vars[2].update(elements) else: inout_vars[0].add(stdname) # end if + elif intent == 'out' and phase != 'initialize' and constituent + and not const_initialized_in_physics[stdname]: + # constituents HAVE to be initialized in the init phase because the dycore needs to advect them + emsg = "constituent variable '{}' cannot be initialized in the '{}' phase" + raise CCPPError(emsg.format(stdname, phase)) + elif intent == 'out' and constituent and phase == 'initialize': + const_initialized_in_physics[stdname] = True elif intent == 'out': if isinstance(elements, list): output_vars[1].add(stdname) diff --git a/scripts/constituents.py b/scripts/constituents.py index be8d4774..7a60af04 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -8,20 +8,17 @@ to implement this support. """ -# Python library imports -from __future__ import print_function -import os # CCPP framework imports -from file_utils import KINDS_MODULE -from fortran_tools import FortranWriter -from parse_tools import ParseInternalError -from metavar import Var, VarDictionary +from parse_tools import ParseInternalError, type_name +from metavar import VarDictionary ######################################################################## CONST_DDT_NAME = "ccpp_model_constituents_t" CONST_DDT_MOD = "ccpp_constituent_prop_mod" CONST_PROP_TYPE = "ccpp_constituent_properties_t" +CONST_PROP_PTR_TYPE = "ccpp_constituent_prop_ptr_t" +CONST_OBJ_STDNAME = "ccpp_model_constituents_object" ######################################################################## @@ -34,7 +31,6 @@ class ConstituentVarDict(VarDictionary): __const_prop_array_name = "ccpp_constituent_array" __const_prop_init_name = "ccpp_constituents_initialized" __const_prop_init_consts = "ccpp_create_constituent_array" - __const_prop_type_name = "ccpp_constituent_properties_t" __constituent_type = "suite" def __init__(self, name, parent_dict, run_env, variables=None): @@ -47,9 +43,8 @@ def __init__(self, name, parent_dict, run_env, variables=None): because this dictionary must be connected to a host model. """ self.__run_env = run_env - super(ConstituentVarDict, self).__init__(name, run_env, - variables=variables, - parent_dict=parent_dict) + super().__init__(name, run_env, + variables=variables, parent_dict=parent_dict) def find_variable(self, standard_name=None, source_var=None, any_scope=True, clone=None, @@ -117,7 +112,7 @@ def find_variable(self, standard_name=None, source_var=None, # end for newdims.append(':'.join(new_dnames)) # end for - var = source_var.clone({'dimensions' : newdims}, remove_intent=True, + var = source_var.clone({'dimensions' : newdims}, remove_intent=False, source_type=self.__constituent_type) self.add_variable(var, self.__run_env) return var @@ -155,7 +150,7 @@ def declare_private_data(self, outfile, indent): outfile.write("! Private constituent module data", indent) if self: stmt = "type({}), private, allocatable :: {}(:)" - outfile.write(stmt.format(self.constituent_prop_type_name(), + outfile.write(stmt.format(CONST_PROP_TYPE, self.constituent_prop_array_name()), indent) # end if @@ -298,10 +293,15 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): len(self)), indent+1) outfile.write("index = 0", indent+1) # end if + for evar in err_vars: + self.__init_err_var(evar, outfile, indent+1) + # end for for std_name, var in self.items(): outfile.write("index = index + 1", indent+1) long_name = var.get_prop_value('long_name') + units = var.get_prop_value('units') dims = var.get_dim_stdnames() + default_value = var.get_prop_value('default_value') if 'vertical_layer_dimension' in dims: vertical_dim = 'vertical_layer_dimension' elif 'vertical_interface_dimension' in dims: @@ -310,13 +310,18 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): vertical_dim = '' # end if advect_str = self.TF_string(var.get_prop_value('advected')) - stmt = 'call {}(index)%initialize("{}", "{}", "{}", {}{})' + init_args = [f'std_name="{std_name}"', f'long_name="{long_name}"', + f'units="{units}"', f'vertical_dim="{vertical_dim}"', + f'advected={advect_str}', + f'errcode={errvar_names["ccpp_error_code"]}', + f'errmsg={errvar_names["ccpp_error_message"]}'] + if default_value is not None: + init_args.append(f'default_value={default_value}') + stmt = 'call {}(index)%instantiate({})' + outfile.write(f'if ({errvar_names["ccpp_error_code"]} == 0) then', indent+1) outfile.write(stmt.format(self.constituent_prop_array_name(), - std_name, long_name, vertical_dim, - advect_str, errvar_call2), indent+1) - # end for - for evar in err_vars: - self.__init_err_var(evar, outfile, indent+1) + ", ".join(init_args)), indent+2) + outfile.write("end if", indent+1) # end for outfile.write("{} = .true.".format(self.constituent_prop_init_name()), indent+1) @@ -363,9 +368,12 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): self._write_index_check(outfile, indent, suite_name, err_vars, use_errcode) if self: - stmt = "call {}(index)%standard_name(name_out{})" + init_args = ['std_name=name_out', + f'errcode={errvar_names["ccpp_error_code"]}', + f'errmsg={errvar_names["ccpp_error_message"]}'] + stmt = "call {}(index)%standard_name({})" outfile.write(stmt.format(self.constituent_prop_array_name(), - errvar_call2), indent+1) + ", ".join(init_args)), indent+1) # end if outfile.write("end subroutine {}".format(self.const_name_subname()), indent) @@ -377,8 +385,8 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): outfile.write("! Copy the data for a constituent", indent+1) outfile.write("! Dummy arguments", indent+1) outfile.write("integer, intent(in) :: index", indent+1) - stmt = "type({}), intent(out) :: cnst_out" - outfile.write(stmt.format(self.constituent_prop_type_name()), indent+1) + stmt = f"type({CONST_PROP_TYPE}), intent(out) :: cnst_out" + outfile.write(stmt, indent+1) for evar in err_vars: evar.write_def(outfile, indent+1, self, dummy=True) # end for @@ -398,7 +406,7 @@ def constituent_module_name(self): if not ((self.parent is not None) and hasattr(self.parent.parent, "constituent_module")): emsg = "ConstituentVarDict parent not HostModel?" - emsg += "\nparent is '{}'".format(type(self.parent.parent)) + emsg += f"\nparent is '{type_name(self.parent.parent)}'" raise ParseInternalError(emsg) # end if return self.parent.parent.constituent_module @@ -447,17 +455,23 @@ def write_constituent_use_statements(cap, suite_list, indent): # end for @staticmethod - def write_host_routines(cap, host, reg_funcname, num_const_funcname, + def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcname, copy_in_funcname, copy_out_funcname, const_obj_name, - const_names_name, const_indices_name, - suite_list, err_vars): + const_names_name, const_indices_name, const_array_func, + advect_array_func, prop_array_func, + const_index_func, suite_list, err_vars): """Write out the host model routine which will instantiate constituent fields for all the constituents in . is a list of the host model's error variables. Also write out the following routines: + : Initialize constituent data : Number of constituents : Collect constituent fields for host : Update constituent fields from host + : Return a pointer to the constituent array + : Return a pointer to the advected constituent array + : Return a pointer to the constituent properties array + : Return the index of a provided constituent name Output is written to . """ # XXgoldyXX: v need to generalize host model error var type support @@ -476,121 +490,157 @@ def write_host_routines(cap, host, reg_funcname, num_const_funcname, errmsg=herrmsg) # XXgoldyXX: ^ need to generalize host model error var type support # First up, the registration routine - substmt = "subroutine {}".format(reg_funcname) - stmt = "{}(suite_list, ncols, num_layers, num_interfaces, {})" - cap.write(stmt.format(substmt, err_dummy_str), 1) - cap.write("! Create constituent object for suites in ", 2) + substmt = f"subroutine {reg_funcname}" + args = "suite_list, host_constituents " + stmt = f"{substmt}({args}, {err_dummy_str})" + cap.write(stmt, 1) + cap.comment("Create constituent object for suites in ", 2) cap.write("", 0) ConstituentVarDict.write_constituent_use_statements(cap, suite_list, 2) cap.write("", 0) - cap.write("! Dummy arguments", 2) - cap.write("character(len=*), intent(in) :: suite_list(:)", 2) - cap.write("integer, intent(in) :: ncols", 2) - cap.write("integer, intent(in) :: num_layers", 2) - cap.write("integer, intent(in) :: num_interfaces", 2) + cap.comment("Dummy arguments", 2) + cap.write("character(len=*), intent(in) :: suite_list(:)", 2) + cap.write(f"type({CONST_PROP_TYPE}), target, intent(in) :: " + \ + "host_constituents(:)", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for - cap.write("! Local variables", 2) + cap.comment("Local variables", 2) spc = ' '*37 cap.write("integer{} :: num_suite_consts".format(spc), 2) cap.write("integer{} :: num_consts".format(spc), 2) cap.write("integer{} :: index".format(spc), 2) cap.write("integer{} :: field_ind".format(spc), 2) - cap.write("type({}), pointer :: const_prop".format(CONST_PROP_TYPE), 2) + cap.write(f"type({CONST_PROP_TYPE}), pointer :: const_prop", 2) cap.write("", 0) cap.write("{} = 0".format(herrcode), 2) - cap.write("num_consts = 0", 2) + cap.write("num_consts = size(host_constituents, 1)", 2) for suite in suite_list: const_dict = suite.constituent_dictionary() funcname = const_dict.num_consts_funcname() - cap.write("! Number of suite constants for {}".format(suite.name), - 2) + cap.comment(f"Number of suite constants for {suite.name}", 2) errvar_str = ConstituentVarDict.__errcode_callstr(herrcode, herrmsg, suite) - cap.write("num_suite_consts = {}({})".format(funcname, - errvar_str), 2) + cap.write(f"num_suite_consts = {funcname}({errvar_str})", 2) cap.write("num_consts = num_consts + num_suite_consts", 2) # end for cap.write("if ({} == 0) then".format(herrcode), 2) - cap.write("! Initialize constituent data and field object", 3) + cap.comment("Initialize constituent data and field object", 3) stmt = "call {}%initialize_table(num_consts)" cap.write(stmt.format(const_obj_name), 3) + # Register host model constituents + cap.comment("Add host model constituent metadata", 3) + cap.write("do index = 1, size(host_constituents, 1)", 3) + cap.write(f"if ({herrcode} == 0) then", 4) + cap.write("const_prop => host_constituents(index)", 5) + stmt = "call {}%new_field(const_prop, {})" + cap.write(stmt.format(const_obj_name, obj_err_callstr), 5) + cap.write("end if", 4) + cap.write("nullify(const_prop)", 4) + cap.write("if ({} /= 0) then".format(herrcode), 4) + cap.write("exit", 5) + cap.write("end if", 4) + cap.write("end do", 3) cap.write("end if", 2) + cap.write("", 0) + # Register suite constituents for suite in suite_list: errvar_str = ConstituentVarDict.__errcode_callstr(herrcode, herrmsg, suite) - cap.write("if ({} == 0) then".format(herrcode), 2) - cap.write("! Add {} constituent metadata".format(suite.name), 3) + cap.write(f"if ({herrcode} == 0) then", 2) + cap.comment(f"Add {suite.name} constituent metadata", 3) const_dict = suite.constituent_dictionary() funcname = const_dict.num_consts_funcname() - cap.write("num_suite_consts = {}({})".format(funcname, - errvar_str), 3) + cap.write(f"num_suite_consts = {funcname}({errvar_str})", 3) cap.write("end if", 2) funcname = const_dict.copy_const_subname() - cap.write("do index = 1, num_suite_consts", 2) - cap.write("allocate(const_prop, stat={})".format(herrcode), 3) - cap.write("if ({} /= 0) then".format(herrcode), 3) - cap.write('{} = "ERROR allocating const_prop"'.format(herrmsg), 4) - cap.write("end if", 3) - cap.write("if ({} == 0) then".format(herrcode), 3) + cap.write(f"if ({herrcode} == 0) then", 2) + cap.write("do index = 1, num_suite_consts", 3) + cap.write(f"if ({herrcode} == 0) then", 4) + cap.write(f"allocate(const_prop, stat={herrcode})", 5) + cap.write("end if", 4) + cap.write(f"if ({herrcode} /= 0) then", 4) + cap.write(f'{herrmsg} = "ERROR allocating const_prop"', 5) + cap.write("exit", 5) + cap.write("end if", 4) + cap.write(f"if ({herrcode} == 0) then", 4) stmt = "call {}(index, const_prop, {})" - cap.write(stmt.format(funcname, errvar_str), 4) - cap.write("end if", 3) - cap.write("if ({} == 0) then".format(herrcode), 3) + cap.write(stmt.format(funcname, errvar_str), 5) + cap.write("end if", 4) + cap.write(f"if ({herrcode} == 0) then", 4) stmt = "call {}%new_field(const_prop, {})" - cap.write(stmt.format(const_obj_name, obj_err_callstr), 4) - cap.write("end if", 3) - cap.write("nullify(const_prop)", 3) - cap.write("if ({} /= 0) then".format(herrcode), 3) - cap.write("exit", 4) - cap.write("end if", 3) - cap.write("end do", 2) + cap.write(stmt.format(const_obj_name, obj_err_callstr), 5) + cap.write("end if", 4) + cap.write("nullify(const_prop)", 4) + cap.write(f"if ({herrcode} /= 0) then", 4) + cap.write("exit", 5) + cap.write("end if", 4) + cap.write("end do", 3) + cap.write("end if", 2) cap.write("", 0) # end for - cap.write("if ({} == 0) then".format(herrcode), 2) - stmt = "call {}%lock_table(ncols, num_layers, num_interfaces, {})" + cap.write(f"if ({herrcode} == 0) then", 2) + stmt = "call {}%lock_table({})" cap.write(stmt.format(const_obj_name, obj_err_callstr), 3) cap.write("end if", 2) - cap.write("! Set the index for each active constituent", 2) - cap.write("do index = 1, SIZE({})".format(const_indices_name), 2) - stmt = "field_ind = {}%field_index({}(index), {})" + cap.write(f"if ({herrcode} == 0) then", 2) + cap.comment("Set the index for each active constituent", 3) + cap.write(f"do index = 1, SIZE({const_indices_name})", 3) + stmt = "call {}%const_index(field_ind, {}(index), {})" cap.write(stmt.format(const_obj_name, const_names_name, - obj_err_callstr), 3) - cap.write("if (field_ind > 0) then", 3) - cap.write("{}(index) = field_ind".format(const_indices_name), 4) - cap.write("else", 3) - cap.write("{} = 1".format(herrcode), 4) + obj_err_callstr), 4) + cap.write("if (field_ind > 0) then", 4) + cap.write(f"{const_indices_name}(index) = field_ind", 5) + cap.write("else", 4) + cap.write(f"{herrcode} = 1", 5) stmt = "{} = 'No field index for '//trim({}(index))" - cap.write(stmt.format(herrmsg, const_names_name), 4) - cap.write("end if", 3) - cap.write("if ({} /= 0) then".format(herrcode), 3) - cap.write("exit", 4) - cap.write("end if", 3) - cap.write("end do", 2) - cap.write("end {}".format(substmt), 1) - # Next, write num_consts routine - substmt = "function {}".format(num_const_funcname) + cap.write(stmt.format(herrmsg, const_names_name), 5) + cap.write("end if", 4) + cap.write(f"if ({herrcode} /= 0) then", 4) + cap.write("exit", 5) + cap.write("end if", 4) + cap.write("end do", 3) + cap.write("end if", 2) + cap.write(f"end {substmt}", 1) + # Write constituent_init routine + substmt = f"subroutine {init_funcname}" + cap.write("", 0) + cap.write(f"{substmt}(ncols, num_layers, {err_dummy_str})", 1) + cap.comment("Initialize constituent data", 2) + cap.write("", 0) + cap.comment("Dummy arguments", 2) + cap.write("integer, intent(in) :: ncols", 2) + cap.write("integer, intent(in) :: num_layers", 2) + for evar in err_vars: + evar.write_def(cap, 2, host, dummy=True, add_intent="out") + # end for evar + cap.write("", 0) + call_str = f"call {const_obj_name}%lock_data(ncols, num_layers, {obj_err_callstr})" + cap.write(call_str, 2) + cap.write(f"end {substmt}", 1) + # Write num_consts routine + substmt = f"subroutine {num_const_funcname}" cap.write("", 0) - cap.write("integer {}({})".format(substmt, err_dummy_str), 1) - cap.write("! Return the number of constituent fields for this run", 2) + cap.write(f"{substmt}(num_flds, advected, {err_dummy_str})", 1) + cap.comment("Return the number of constituent fields for this run", 2) cap.write("", 0) - cap.write("! Dummy arguments", 2) + cap.comment("Dummy arguments", 2) + cap.write("integer, intent(out) :: num_flds", 2) + cap.write("logical, optional, intent(in) :: advected", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for cap.write("", 0) - cap.write("{} = {}%num_constituents({})".format(num_const_funcname, - const_obj_name, - obj_err_callstr), 2) + call_str = "call {}%num_constituents(num_flds, advected=advected, {})" + cap.write(call_str.format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) - # Next, write copy_in routine + # Write copy_in routine substmt = "subroutine {}".format(copy_in_funcname) cap.write("", 0) cap.write("{}(const_array, {})".format(substmt, err_dummy_str), 1) - cap.write("! Copy constituent field info into ", 2) + cap.comment("Copy constituent field info into ", 2) cap.write("", 0) - cap.write("! Dummy arguments", 2) + cap.comment("Dummy arguments", 2) cap.write("real(kind_phys), intent(out) :: const_array(:,:,:)", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") @@ -599,20 +649,77 @@ def write_host_routines(cap, host, reg_funcname, num_const_funcname, cap.write("call {}%copy_in(const_array, {})".format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) - # Next, write copy_out routine + # Write copy_out routine substmt = "subroutine {}".format(copy_out_funcname) cap.write("", 0) cap.write("{}(const_array, {})".format(substmt, err_dummy_str), 1) - cap.write("! Update constituent field info from ", 2) + cap.comment("Update constituent field info from ", 2) cap.write("", 0) - cap.write("! Dummy arguments", 2) + cap.comment("Dummy arguments", 2) cap.write("real(kind_phys), intent(in) :: const_array(:,:,:)", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for cap.write("", 0) cap.write("call {}%copy_out(const_array, {})".format(const_obj_name, - obj_err_callstr), 2) + obj_err_callstr), + 2) + cap.write("end {}".format(substmt), 1) + # Write constituents routine + cap.write("", 0) + cap.write(f"function {const_array_func}() result(const_ptr)", 1) + cap.write("", 0) + cap.comment("Return pointer to constituent array", 2) + cap.write("", 0) + cap.comment("Dummy argument", 2) + cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) + cap.write("", 0) + cap.write(f"const_ptr => {const_obj_name}%field_data_ptr()", 2) + cap.write(f"end function {const_array_func}", 1) + # Write advected constituents routine + cap.write("", 0) + cap.write(f"function {advect_array_func}() result(const_ptr)", 1) + cap.write("", 0) + cap.comment("Return pointer to advected constituent array", 2) + cap.write("", 0) + cap.comment("Dummy argument", 2) + cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) + cap.write("", 0) + cap.write(f"const_ptr => {const_obj_name}%advected_constituents_ptr()", + 2) + cap.write(f"end function {advect_array_func}", 1) + # Write the constituent property array routine + cap.write("", 0) + cap.write(f"function {prop_array_func}() result(const_ptr)", 1) + cap.write(f"use {CONST_DDT_MOD}, only: {CONST_PROP_PTR_TYPE}", 2) + cap.write("", 0) + cap.comment("Return pointer to array of constituent properties", 2) + cap.write("", 0) + cap.comment("Dummy argument", 2) + cap.write("type(ccpp_constituent_prop_ptr_t), pointer :: const_ptr(:)", + 2) + cap.write("", 0) + cap.write(f"const_ptr => {const_obj_name}%constituent_props_ptr()", + 2) + cap.write(f"end function {prop_array_func}", 1) + # Write constituent index function + substmt = f"subroutine {const_index_func}" + cap.write("", 0) + cap.write(f"{substmt}(stdname, const_index, {err_dummy_str})", 1) + cap.comment("Set to the constituent array index " + \ + "for .", 2) + cap.comment("If is not found, set to -1 " + \ + "set an error condition", 2) + cap.write("", 0) + cap.comment("Dummy arguments", 2) + cap.write("character(len=*), intent(in) :: stdname", 2) + cap.write("integer, intent(out) :: const_index", 2) + for evar in err_vars: + evar.write_def(cap, 2, host, dummy=True, add_intent="out") + # end for + cap.write("", 0) + cap.write(f"call {const_obj_name}%const_index(const_index, " + \ + f"stdname, {obj_err_callstr})", 2) cap.write("end {}".format(substmt), 1) @staticmethod @@ -636,20 +743,13 @@ def constituent_prop_init_consts(): properties array for this suite""" return ConstituentVarDict.__const_prop_init_consts - @staticmethod - def constituent_prop_type_name(): - """Return the name of the derived type which holds constituent - properties.""" - return ConstituentVarDict.__const_prop_type_name - @staticmethod def write_suite_use(outfile, indent): """Write use statements for any modules needed by the suite cap. The statements are written to at indent, . """ - omsg = "use ccpp_constituent_prop_mod, only: {}" - cpt_name = ConstituentVarDict.constituent_prop_type_name() - outfile.write(omsg.format(cpt_name), indent) + omsg = f"use ccpp_constituent_prop_mod, only: {CONST_PROP_TYPE}" + outfile.write(omsg, indent) @staticmethod def TF_string(tf_val): diff --git a/scripts/host_cap.py b/scripts/host_cap.py index 9c88cf34..d833c649 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -8,9 +8,10 @@ import logging import os # CCPP framework imports -from ccpp_suite import API +from ccpp_suite import API, API_SOURCE_NAME from ccpp_state_machine import CCPP_STATE_MACH from constituents import ConstituentVarDict, CONST_DDT_NAME, CONST_DDT_MOD +from constituents import CONST_OBJ_STDNAME from ddt_library import DDTLibrary from file_utils import KINDS_MODULE from framework_env import CCPPFrameworkEnv @@ -32,9 +33,7 @@ end subroutine {host_model}_ccpp_physics_{stage} ''' -_API_SRC_NAME = "CCPP_API" - -_API_SOURCE = ParseSource(_API_SRC_NAME, "MODULE", +_API_SOURCE = ParseSource(API_SOURCE_NAME, "MODULE", ParseContext(filename="host_cap.F90")) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', @@ -60,7 +59,7 @@ 'suites':''}) # Used to prevent loop substitution lookups -_BLANK_DICT = VarDictionary(_API_SRC_NAME, _MVAR_DUMMY_RUN_ENV) +_BLANK_DICT = VarDictionary(API_SOURCE_NAME, _MVAR_DUMMY_RUN_ENV) ############################################################################### def suite_part_list(suite, stage): @@ -79,14 +78,29 @@ def suite_part_list(suite, stage): # End if return spart_list +############################################################################### +def constituent_num_suite_subname(host_model): +############################################################################### + """Return the name of the number of suite constituents for this run + Because this is a user interface API function, the name is fixed.""" + return "{}_ccpp_num_suite_constituents".format(host_model.name) + ############################################################################### def constituent_register_subname(host_model): ############################################################################### - """Return the name of the subroutine used to register (initialize) the - constituents for this run. + """Return the name of the subroutine used to register the constituent + properties for this run. Because this is a user interface API function, the name is fixed.""" return "{}_ccpp_register_constituents".format(host_model.name) +############################################################################### +def constituent_initialize_subname(host_model): +############################################################################### + """Return the name of the subroutine used to initialize the + constituents for this run. + Because this is a user interface API function, the name is fixed.""" + return "{}_ccpp_initialize_constituents".format(host_model.name) + ############################################################################### def constituent_num_consts_funcname(host_model): ############################################################################### @@ -129,10 +143,13 @@ def unique_local_name(loc_name, host_model): ############################################################################### def constituent_model_object_name(host_model): ############################################################################### - """Return the variable name of the object which holds the constiteunt - medata and field information.""" - hstr = "{}_constituents_obj".format(host_model.name) - return unique_local_name(hstr, host_model) + """Return the variable name of the object which holds the constituent + metadata and field information.""" + hvar = host_model.find_variable(CONST_OBJ_STDNAME) + if not hvar: + raise CCPPError(f"Host model does not contain Var, {CONST_OBJ_STDNAME}") + # end if + return hvar.get_prop_value('local_name') ############################################################################### def constituent_model_const_stdnames(host_model): @@ -148,6 +165,37 @@ def constituent_model_const_indices(host_model): hstr = "{}_model_const_indices".format(host_model.name) return unique_local_name(hstr, host_model) +############################################################################### +def constituent_model_consts(host_model): +############################################################################### + """Return the name of the function that will return a pointer to the + array of all constituents""" + hstr = "{}_constituents_array".format(host_model.name) + return unique_local_name(hstr, host_model) + +############################################################################### +def constituent_model_advected_consts(host_model): +############################################################################### + """Return the name of the function that will return a pointer to the + array of advected constituents""" + hstr = "{}_advected_constituents_array".format(host_model.name) + return unique_local_name(hstr, host_model) + +############################################################################### +def constituent_model_const_props(host_model): +############################################################################### + """Return the name of the array of constituent property object pointers""" + hstr = "{}_model_const_properties".format(host_model.name) + return unique_local_name(hstr, host_model) + +############################################################################### +def constituent_model_const_index(host_model): +############################################################################### + """Return the name of the interface that returns the array index of + a constituent array given its standard name""" + hstr = "{}_const_get_index".format(host_model.name) + return unique_local_name(hstr, host_model) + ############################################################################### def add_constituent_vars(cap, host_model, suite_list, run_env): ############################################################################### @@ -161,47 +209,24 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): to create the dictionary. """ # First create a MetadataTable for the constituents DDT - stdname_layer = "ccpp_constituents_num_layer_consts" - stdname_interface = "ccpp_constituents_num_interface_consts" - stdname_2d = "ccpp_constituents_num_2d_consts" + stdname_layer = "ccpp_num_constituents" horiz_dim = "horizontal_dimension" vert_layer_dim = "vertical_layer_dimension" vert_interface_dim = "vertical_interface_dimension" array_layer = "vars_layer" - array_interface = "vars_interface" - array_2d = "vars_2d" # Table preamble (leave off ccpp-table-properties header) ddt_mdata = [ #"[ccpp-table-properties]", - " name = {}".format(CONST_DDT_NAME), " type = ddt", + f" name = {CONST_DDT_NAME}", " type = ddt", "[ccpp-arg-table]", - " name = {}".format(CONST_DDT_NAME), " type = ddt", + f" name = {CONST_DDT_NAME}", " type = ddt", "[ num_layer_vars ]", - " standard_name = {}".format(stdname_layer), - " units = count", " dimensions = ()", " type = integer", - "[ num_interface_vars ]", - " standard_name = {}".format(stdname_interface), - " units = count", " dimensions = ()", " type = integer", - "[ num_2d_vars ]", - " standard_name = {}".format(stdname_2d), + f" standard_name = {stdname_layer}", " units = count", " dimensions = ()", " type = integer", - "[ {} ]".format(array_layer), - " standard_name = ccpp_constituents_array_of_layer_consts", + f"[ {array_layer} ]", + " standard_name = ccpp_constituent_array", " units = none", - " dimensions = ({}, {}, {})".format(horiz_dim, vert_layer_dim, - stdname_layer), - " type = real", " kind = kind_phys", - "[ {} ]".format(array_interface), - " standard_name = ccpp_constituents_array_of_interface_consts", - " units = none", - " dimensions = ({}, {}, {})".format(horiz_dim, - vert_interface_dim, - stdname_interface), - " type = real", " kind = kind_phys", - "[ {} ]".format(array_2d), - " standard_name = ccpp_constituents_array_of_2d_consts", - " units = none", - " dimensions = ({}, {})".format(horiz_dim, stdname_2d), + f" dimensions = ({horiz_dim}, {vert_layer_dim}, {stdname_layer})", " type = real", " kind = kind_phys"] # Add entries for each constituent (once per standard name) const_stdnames = set() @@ -235,8 +260,6 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): vdim = dims[1].split(':')[-1] if vdim == vert_layer_dim: cvar_array_name = array_layer - elif vdim == vert_interface_dim: - cvar_array_name = array_interface else: emsg = "Unsupported vertical constituent dimension, " emsg += "'{}', must be '{}' or '{}'" @@ -244,44 +267,47 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): vert_interface_dim)) # end if else: - cvar_array_name = array_2d + emsg = f"Unsupported 2-D variable, '{std_name}'" + raise CCPPError(emsg) # end if # First, create an index variable for ind_std_name = "index_of_{}".format(std_name) - loc_name = "{}(:,:,{})".format(cvar_array_name, ind_std_name) - ddt_mdata.append("[ {} ]".format(loc_name)) - ddt_mdata.append(" standard_name = {}".format(std_name)) + loc_name = f"{cvar_array_name}(:,:,{ind_std_name})" + ddt_mdata.append(f"[ {loc_name} ]") + ddt_mdata.append(f" standard_name = {std_name}") units = cvar.get_prop_value('units') - ddt_mdata.append(" units = {}".format(units)) - dimstr = "({})".format(", ".join(dims)) - ddt_mdata.append(" dimensions = {}".format(dimstr)) + ddt_mdata.append(f" units = {units}") + dimstr = f"({', '.join(dims)})" + ddt_mdata.append(f" dimensions = {dimstr}") vtype = cvar.get_prop_value('type') vkind = cvar.get_prop_value('kind') - ddt_mdata.append(" type = {} | kind = {}".format(vtype, vkind)) + ddt_mdata.append(f" type = {vtype} | kind = {vkind}") const_stdnames.add(std_name) # end if # end for # end for # Parse this table using a fake filename - parse_obj = ParseObject("{}_constituent_mod.meta".format(host_model.name), + parse_obj = ParseObject(f"{host_model.name}_constituent_mod.meta", ddt_mdata) ddt_table = MetadataTable(run_env, parse_object=parse_obj) - ddt_name = ddt_table.sections()[0].title - ddt_lib = DDTLibrary('{}_constituent_ddtlib'.format(host_model.name), + ddt_lib = DDTLibrary(f"{host_model.name}_constituent_ddtlib", run_env, ddts=ddt_table.sections()) # A bit of cleanup del parse_obj del ddt_mdata # Now, create the "host constituent module" dictionary - const_dict = VarDictionary("{}_constituents".format(host_model.name), + const_dict = VarDictionary(f"{host_model.name}_constituents", run_env, parent_dict=host_model) - # Add in the constituents object - prop_dict = {'standard_name' : "ccpp_model_constituents_object", - 'local_name' : constituent_model_object_name(host_model), - 'dimensions' : '()', 'units' : "None", 'ddt_type' : ddt_name} - const_var = Var(prop_dict, _API_SOURCE, run_env) - const_var.write_def(cap, 1, const_dict) - ddt_lib.collect_ddt_fields(const_dict, const_var, run_env) + # Add the constituents object to const_dict and write its declaration + const_var = host_model.find_variable(CONST_OBJ_STDNAME) + if const_var: + const_dict.add_variable(const_var, run_env) + const_var.write_def(cap, 1, const_dict) + else: + raise CCPPError(f"Missing Var, {CONST_OBJ_STDNAME}, in host model") + # end if + ddt_lib.collect_ddt_fields(const_dict, const_var, run_env, + skip_duplicates=True) # Declare variable for the constituent standard names array max_csname = max([len(x) for x in const_stdnames]) if const_stdnames else 0 num_const_fields = len(const_stdnames) @@ -377,10 +403,9 @@ def suite_part_call_list(host_model, const_dict, suite_part, subst_loop_vars): return ', '.join(hmvars) ############################################################################### -def write_host_cap(host_model, api, output_dir, run_env): +def write_host_cap(host_model, api, module_name, output_dir, run_env): ############################################################################### """Write an API to allow to call any configured CCPP suite""" - module_name = "{}_ccpp_cap".format(host_model.name) cap_filename = os.path.join(output_dir, '{}.F90'.format(module_name)) if run_env.logger is not None: msg = 'Writing CCPP Host Model Cap for {} to {}' @@ -404,14 +429,13 @@ def write_host_cap(host_model, api, output_dir, run_env): cap.write("use {}, {}only: {}".format(mod[0], mspc, mod[1]), 1) # End for mspc = ' '*(maxmod - len(CONST_DDT_MOD)) - cap.write("use {}, {}only: {}".format(CONST_DDT_MOD, mspc, - CONST_DDT_NAME), 1) + cap.write(f"use {CONST_DDT_MOD}, {mspc}only: {CONST_DDT_NAME}", 1) cap.write_preamble() max_suite_len = 0 for suite in api.suites: max_suite_len = max(max_suite_len, len(suite.module)) # End for - cap.write("! Public Interfaces", 1) + cap.comment("Public Interfaces", 1) # CCPP_STATE_MACH.transitions represents the host CCPP interface for stage in CCPP_STATE_MACH.transitions(): stmt = "public :: {host_model}_ccpp_physics_{stage}" @@ -421,20 +445,30 @@ def write_host_cap(host_model, api, output_dir, run_env): # Write the host-model interfaces for constituents reg_name = constituent_register_subname(host_model) cap.write("public :: {}".format(reg_name), 1) + init_name = constituent_initialize_subname(host_model) + cap.write("public :: {}".format(init_name), 1) numconsts_name = constituent_num_consts_funcname(host_model) cap.write("public :: {}".format(numconsts_name), 1) copyin_name = constituent_copyin_subname(host_model) cap.write("public :: {}".format(copyin_name), 1) copyout_name = constituent_copyout_subname(host_model) cap.write("public :: {}".format(copyout_name), 1) + const_array_func = constituent_model_consts(host_model) + cap.write(f"public :: {const_array_func}", 1) + advect_array_func = constituent_model_advected_consts(host_model) + cap.write(f"public :: {advect_array_func}", 1) + prop_array_func = constituent_model_const_props(host_model) + cap.write(f"public :: {prop_array_func}", 1) + const_index_func = constituent_model_const_index(host_model) + cap.write(f"public :: {const_index_func}", 1) cap.write("", 0) cap.write("! Private module variables", 1) const_dict = add_constituent_vars(cap, host_model, api.suites, run_env) cap.end_module_header() for stage in CCPP_STATE_MACH.transitions(): # Create a dict of local variables for stage - host_local_vars = VarDictionary("{}_{}".format(host_model.name, - stage), run_env) + host_local_vars = VarDictionary(f"{host_model.name}_{stage}", + run_env) # Create part call lists # Look for any loop-variable mismatch for suite in api.suites: @@ -475,7 +509,7 @@ def write_host_cap(host_model, api, output_dir, run_env): subst_dict['intent'] = 'inout' # End if hdvars.append(hvar.clone(subst_dict, - source_name=_API_SRC_NAME)) + source_name=API_SOURCE_NAME)) # End for lnames = [x.get_prop_value('local_name') for x in apivars + hdvars] api_vlist = ", ".join(lnames) @@ -563,11 +597,15 @@ def write_host_cap(host_model, api, output_dir, run_env): cap.write("", 0) const_names_name = constituent_model_const_stdnames(host_model) const_indices_name = constituent_model_const_indices(host_model) - ConstituentVarDict.write_host_routines(cap, host_model, reg_name, + ConstituentVarDict.write_host_routines(cap, host_model, reg_name, init_name, numconsts_name, copyin_name, copyout_name, const_obj_name, const_names_name, const_indices_name, + const_array_func, + advect_array_func, + prop_array_func, + const_index_func, api.suites, err_vars) # End with return cap_filename diff --git a/scripts/host_model.py b/scripts/host_model.py index eae2479d..68a969e7 100644 --- a/scripts/host_model.py +++ b/scripts/host_model.py @@ -5,10 +5,11 @@ """ # CCPP framework imports -from metavar import VarDictionary +from constituents import CONST_DDT_NAME, CONST_PROP_TYPE, CONST_OBJ_STDNAME +from metavar import Var, VarDictionary from ddt_library import VarDDT, DDTLibrary -from parse_tools import ParseContext, CCPPError, ParseInternalError -from parse_tools import context_string +from parse_tools import ParseContext, ParseSource, CCPPError, ParseInternalError +from parse_tools import context_string, registered_fortran_ddt_name from parse_tools import FORTRAN_SCALAR_REF_RE ############################################################################### @@ -17,7 +18,7 @@ class HostModel(VarDictionary): def __init__(self, meta_tables, name_in, run_env): """Initialize this HostModel object. - is a list of parsed host metadata tables. + is a dictionary of parsed host metadata tables. is the name for this host model. is the CCPPFrameworkEnv object for this framework run. """ @@ -35,11 +36,12 @@ def __init__(self, meta_tables, name_in, run_env): # Initialize our dictionaries # Initialize variable dictionary super().__init__(self.name, run_env) + ddt_headers = [d for d in meta_headers if d.header_type == 'ddt'] self.__ddt_lib = DDTLibrary('{}_ddts'.format(self.name), run_env, - ddts=[d for d in meta_headers - if d.header_type == 'ddt']) + ddts=ddt_headers) self.__ddt_dict = VarDictionary("{}_ddt_vars".format(self.name), run_env, parent_dict=self) + del ddt_headers # Now, process the code headers by type self.__metadata_tables = meta_tables for header in [h for h in meta_headers if h.header_type != 'ddt']: @@ -111,6 +113,20 @@ def __init__(self, meta_tables, name_in, run_env): errmsg = 'No name found for host model, add a host metadata entry' raise CCPPError(errmsg) # End if + # Add in the constituents object + if registered_fortran_ddt_name(CONST_PROP_TYPE): + prop_dict = {'standard_name' : CONST_OBJ_STDNAME, + 'local_name' : self.constituent_model_object_name(), + 'dimensions' : '()', 'units' : "None", + 'ddt_type' : CONST_DDT_NAME, 'target' : 'True'} + host_source = ParseSource(self.ccpp_cap_name(), "MODULE", + ParseContext(filename=f"{self.ccpp_cap_name()}.F90")) + const_var = Var(prop_dict, host_source, run_env) + self.add_variable(const_var, run_env) + lname = const_var.get_prop_value('local_name') + self.__var_locations[lname] = self.ccpp_cap_name() + self.ddt_lib.collect_ddt_fields(self.__ddt_dict, const_var, run_env) + # end if # Finally, turn on the use meter so we know which module variables # to 'use' in a host cap. self.__used_variables = set() # Local names which have been requested @@ -131,12 +147,10 @@ def ddt_lib(self): """Return this host model's DDT library""" return self.__ddt_lib -# XXgoldyXX: v needed? @property def constituent_module(self): """Return the name of host model constituent module""" return "{}_ccpp_constituents".format(self.name) -# XXgoldyXX: ^ needed? def argument_list(self, loop_vars=True): """Return a string representing the host model variable arg list""" @@ -157,8 +171,9 @@ def host_variable_module(self, local_name): def variable_locations(self): """Return a set of module-variable and module-type pairs. - These represent the locations of all host model data with a listed - source location (variables with no source are omitted).""" + These represent the locations of all host model data with a listed + source location (variables with no source or for which the + source is the CCPP host cap are omitted).""" varset = set() lnames = self.prop_list('local_name') # Attempt to realize deferred lookups @@ -171,10 +186,11 @@ def variable_locations(self): # End for # End if # Now, find all the used module variables + cap_modname = self.ccpp_cap_name() for name in lnames: module = self.host_variable_module(name) used = self.__used_variables and (name in self.__used_variables) - if module and used: + if module and used and (module != cap_modname): varset.add((module, name)) # No else, either no module or a zero-length module name # End if @@ -299,6 +315,15 @@ def call_list(self, phase): # End for return hdvars + def constituent_model_object_name(self): + """Return the variable name of the object which holds the constituent + metadata and field information.""" + return "{}_constituents_obj".format(self.name) + + def ccpp_cap_name(self): + """Return the name of the CCPP host model cap module name.""" + return f"{self.name}_ccpp_cap" + ############################################################################### if __name__ == "__main__": diff --git a/scripts/metavar.py b/scripts/metavar.py index 71609580..29390a61 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -20,6 +20,7 @@ from parse_tools import check_units, check_dimensions, check_cf_standard_name from parse_tools import check_diagnostic_id, check_diagnostic_fixed from parse_tools import check_default_value, check_valid_values +from parse_tools import check_molar_mass from parse_tools import ParseContext, ParseSource from parse_tools import ParseInternalError, ParseSyntaxError, CCPPError from var_props import CCPP_LOOP_DIM_SUBSTS, VariableProperty, VarCompatObj @@ -196,6 +197,8 @@ class Var: VariableProperty('active', str, optional_in=True, default_in='.true.'), VariableProperty('polymorphic', bool, optional_in=True, + default_in='.false.'), + VariableProperty('target', bool, optional_in=True, default_in='.false.')] # XXgoldyXX: v debug only @@ -212,7 +215,10 @@ class Var: # Note that all constituent properties must be optional and contain either # a default value or default function. __constituent_props = [VariableProperty('advected', bool, - optional_in=True, default_in=False)] + optional_in=True, default_in=False), + VariableProperty('molar_mass', float, + optional_in=True, default_in=0.0, + check_fn_in=check_molar_mass)] __constituent_prop_dict = {x.name : x for x in __constituent_props} @@ -610,7 +616,8 @@ def call_dimstring(self, var_dicts=None, # end if # end for if dvar: - dnames.append(dvar.get_prop_value('local_name')) + # vdict is the dictionary where was found + dnames.append(dvar.call_string(vdict)) # end if if not dvar: emsg += sepstr + "No variable found in " @@ -687,9 +694,15 @@ def call_string(self, var_dict, loop_vars=None): dvar = var_dict.find_variable(standard_name=item, any_scope=False) if dvar is None: - iname = None + try: + dval = int(item) + iname = item + except ValueError: + iname = None + # end try else: - iname = dvar.get_prop_value('local_name') + iname = dvar.call_string(var_dict, + loop_vars=loop_vars) # end if else: iname = '' @@ -749,7 +762,7 @@ def array_ref(self, local_name=None): match = FORTRAN_SCALAR_REF_RE.match(local_name) return match - def intrinsic_elements(self, check_dict=None): + def intrinsic_elements(self, check_dict=None, ddt_lib=None): """Return a list of the standard names of this Var object's 'leaf' intrinsic elements or this Var object's standard name if it is an intrinsic 'leaf' variable. @@ -765,9 +778,23 @@ def intrinsic_elements(self, check_dict=None): Fortran does not support a way to reference those elements. """ if self.is_ddt(): - element_names = None - raise ValueError("shouldn't happen?") - # To Do, find and process named elements of DDT + dtitle = self.get_prop_value('type') + if ddt_lib and (dtitle in ddt_lib): + element_names = [] + ddt_def = ddt_lib[dtitle] + for dvar in ddt_def.variable_list(): + delems = dvar.intrinsic_elements(check_dict=check_dict, + ddt_lib=ddt_lib) + if delems: + element_names.extend(delems) + # end if + # end for + if not element_names: + element_names = None + # end if + else: + element_names = None + # end if # end if children = self.children() if (not children) and check_dict: @@ -844,6 +871,11 @@ def children(self): # end if return iter(children) if children else None + @property + def var(self): + "Return this object (base behavior for derived classes such as VarDDT)" + return self + @property def context(self): """Return this variable's parsed context""" @@ -932,7 +964,7 @@ def has_vertical_dimension(self, dims=None): return find_vertical_dimension(vdims)[0] def write_def(self, outfile, indent, wdict, allocatable=False, - dummy=False, add_intent=None, extra_space=0): + dummy=False, add_intent=None, extra_space=0, public=False): """Write the definition line for the variable to . If is True, include the variable's intent. If is True but the variable has no intent, add the @@ -994,10 +1026,9 @@ def write_def(self, outfile, indent, wdict, allocatable=False, elif intent is not None: alloval = self.get_prop_value('allocatable') if (intent.lower()[-3:] == 'out') and alloval: - intent_str = 'allocatable, intent({})'.format(intent) + intent_str = f"allocatable, intent({intent})" else: - intent_str = 'intent({}){}'.format(intent, - ' '*(5 - len(intent))) + intent_str = f"intent({intent}){' '*(5 - len(intent))}" # end if elif not dummy: intent_str = '' @@ -1009,6 +1040,13 @@ def write_def(self, outfile, indent, wdict, allocatable=False, else: comma = ' ' # end if + if self.get_prop_value('target'): + targ = ", target" + else: + targ = "" + # end if + comma = targ + comma + extra_space -= len(targ) if self.is_ddt(): if polymorphic: dstr = "class({kind}){cspc}{intent} :: {name}{dims} ! {sname}" @@ -1026,6 +1064,7 @@ def write_def(self, outfile, indent, wdict, allocatable=False, cspc = comma + ' '*(extra_space + 19 - len(vtype)) # end if # end if + outfile.write(dstr.format(type=vtype, kind=kind, intent=intent_str, name=name, dims=dimstr, cspc=cspc, sname=stdname), indent) @@ -1427,7 +1466,9 @@ def __init__(self, name, run_env, variables=None, self[stdname] = variables[key] # end for elif variables is not None: - raise ParseInternalError('Illegal type for variables, {} in {}'.format(type(variables), self.name)) + emsg = "Illegal type for variables, {} in {}" + raise ParseInternalError(emsg.format(type(variables), + self.name)) # end if @property diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 03dd0429..4f1a7b76 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -22,7 +22,7 @@ from parse_checkers import registered_fortran_ddt_name from parse_checkers import register_fortran_ddt_name from parse_checkers import check_units, check_dimensions, check_cf_standard_name -from parse_checkers import check_default_value, check_valid_values +from parse_checkers import check_default_value, check_valid_values, check_molar_mass from parse_log import init_log, set_log_level, flush_log from parse_log import set_log_to_stdout, set_log_to_null from parse_log import set_log_to_file @@ -47,6 +47,7 @@ 'check_fortran_type', 'check_local_name', 'check_valid_values', + 'check_molar_mass', 'context_string', 'find_schema_file', 'find_schema_version', diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index 487478e6..29786797 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -13,6 +13,7 @@ ######################################################################## _UNITS_RE = re.compile(r"^[^/@#$%^&*()\|<>\[\]{}?,.]+$") +_MAX_MOLAR_MASS = 10000.0 def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None @@ -936,6 +937,58 @@ def check_diagnostic_id(test_val, prop_dict, error): ######################################################################## +def check_molar_mass(test_val, prop_dict, error): + """Return if valid molar mass, otherwise, None + if is True, raise an Exception if is not valid. + >>> check_molar_mass('1', None, True) + 1.0 + >>> check_molar_mass('1.0', None, True) + 1.0 + >>> check_molar_mass('1.0', None, False) + 1.0 + >>> check_molar_mass('-1', None, False) + + >>> check_molar_mass('-1.0', None, False) + + >>> check_molar_mass('string', None, False) + + >>> check_molar_mass(10001, None, False) + + >>> check_molar_mass('-1', None, True) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + CCPPError: '-1' is not a valid molar mass + >>> check_molar_mass('-1.0', None, True) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + CCPPError: '-1.0' is not a valid molar mass + >>> check_molar_mass('string', None, True) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + CCPPError: '-1.0' is not a valid molar mass + >>> check_molar_mass(10001, None, True) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + CCPPError: '10001' is not a valid molar mass + """ + # Check if input value is an int or float + try: + test_val = float(test_val) + if test_val < 0.0 or test_val > _MAX_MOLAR_MASS: + if error: + raise CCPPError("{} is not a valid molar mass".format(test_val)) + else: + test_val = None + # end if + # end if + except: + # not an int or float, conditionally throw error + if error: + raise CCPPError("{} is invalid; not a float or int".format(test_val)) + else: + test_val=None + # end if + # end try + return test_val + +######################################################################## + def check_balanced_paren(string, start=0, error=False): """Return indices delineating a balance set of parentheses. Parentheses in character context do not count. diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 472556cb..9bdc154c 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -90,6 +90,22 @@ def add_vars(self, call_list, run_env, gen_unique=False): # end if # end for + def add_variable(self, newvar, run_env, exists_ok=False, gen_unique=False, + adjust_intent=False): + """Add as for VarDictionary but make sure that the variable + has an intent with the default being intent(in). + """ + # We really need an intent on a dummy argument + if newvar.get_prop_value("intent") is None: + subst_dict = {'intent' : 'in'} + oldvar = newvar + newvar = oldvar.clone(subst_dict, source_name=self.name, + source_type=_API_GROUP_VAR_NAME, + context=oldvar.context) + # end if + super().add_variable(newvar, run_env, exists_ok=exists_ok, + gen_unique=gen_unique, adjust_intent=adjust_intent) + def call_string(self, cldicts=None, is_func_call=False, subname=None): """Return a dummy argument string for this call list. may be a list of VarDictionary objects to search for diff --git a/scripts/var_props.py b/scripts/var_props.py index 69b6b766..e1287463 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -14,6 +14,7 @@ from conversion_tools import unit_conversion from framework_env import CCPPFrameworkEnv from parse_tools import check_local_name, check_fortran_type, context_string +from parse_tools import check_molar_mass from parse_tools import FORTRAN_DP_RE, FORTRAN_SCALAR_REF_RE, fortran_list_match from parse_tools import check_units, check_dimensions, check_cf_standard_name from parse_tools import check_diagnostic_id, check_diagnostic_fixed @@ -586,6 +587,8 @@ class VariableProperty: 'foo(bar)' >>> VariableProperty('local_name', str, check_fn_in=check_local_name).valid_value('q(:,:,index_of_water_vapor_specific_humidity)') 'q(:,:,index_of_water_vapor_specific_humidity)' + >>> VariableProperty('molar_mass', float, check_fn_in=check_molar_mass).valid_value('12.1') + 12.1 """ __true_vals = ['t', 'true', '.true.'] @@ -597,7 +600,7 @@ def __init__(self, name_in, type_in, valid_values_in=None, """Conduct sanity checks and initialize this variable property.""" self._name = name_in self._type = type_in - if self._type not in [bool, int, list, str]: + if self._type not in [bool, int, list, str, float]: emsg = "{} has invalid VariableProperty type, '{}'" raise CCPPError(emsg.format(name_in, type_in)) # end if @@ -688,6 +691,21 @@ def valid_value(self, test_value, prop_dict=None, error=False): valid_val = tval except CCPPError: valid_val = None # Redundant but more expressive than pass + elif self.ptype is float: + try: + tval = float(test_value) + if self._valid_values is not None: + if tval in self._valid_values: + valid_val = tval + else: + valid_val = None # i.e. pass + # end if + else: + valid_val = tval + # end if + except CCPPError: + valid_val = None + # end try elif self.ptype is list: if isinstance(test_value, str): tval = fortran_list_match(test_value) diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index 419af297..df80ec0d 100644 --- a/src/ccpp_constituent_prop_mod.F90 +++ b/src/ccpp_constituent_prop_mod.F90 @@ -10,99 +10,178 @@ module ccpp_constituent_prop_mod implicit none private - integer, parameter :: int_unassigned = -1 + !!XXgoldyXX: Implement "last_error" method so that functions do not + !! need to have output variables. + + ! Private module data + integer, parameter :: stdname_len = 256 + integer, parameter :: dimname_len = 32 + integer, parameter :: errmsg_len = 256 + integer, parameter :: dry_mixing_ratio = -2 + integer, parameter :: moist_mixing_ratio = -3 + integer, parameter :: wet_mixing_ratio = -4 + integer, parameter :: mass_mixing_ratio = -5 + integer, parameter :: volume_mixing_ratio = -6 + integer, parameter :: number_concentration = -7 + integer, parameter :: int_unassigned = -HUGE(1) real(kind_phys), parameter :: kphys_unassigned = HUGE(1.0_kind_phys) - !!XXgoldyXX: NB: We end up with two copies of each metadata object, FIX!! - type, public, extends(ccpp_hashable_char_t) :: ccpp_constituent_properties_t ! A ccpp_constituent_properties_t object holds relevant metadata ! for a constituent species and provides interfaces to access that data. character(len=:), private, allocatable :: var_std_name character(len=:), private, allocatable :: var_long_name + character(len=:), private, allocatable :: var_units character(len=:), private, allocatable :: vert_dim integer, private :: const_ind = int_unassigned - integer, private :: field_ind = int_unassigned logical, private :: advected = .false. + ! While the quantities below can be derived from the standard name, + ! this implementation avoids string searching in parameterizations + ! const_type distinguishes mass, volume, and number conc. mixing ratios + integer, private :: const_type = int_unassigned + ! const_water distinguishes dry, moist, and "wet" mixing ratios + integer, private :: const_water = int_unassigned + ! minimum_mr is the minimum allowed value (default zero) + real(kind_phys), private :: min_val = 0.0_kind_phys + ! molar_mass is the molecular weight of the constituent (g mol-1) + real(kind_phys), private :: molar_mass = kphys_unassigned + ! default_value is the default value that the constituent array will be + ! initialized to + real(kind_phys), private :: const_default_value = kphys_unassigned contains ! Required hashable method procedure :: key => ccp_properties_get_key ! Informational methods - procedure :: is_initialized => ccp_is_initialized - procedure :: standard_name => ccp_get_standard_name - procedure :: long_name => ccp_get_long_name - procedure :: is_layer_var => ccp_is_layer_var - procedure :: is_interface_var => ccp_is_interface_var - procedure :: is_2d_var => ccp_is_2d_var - procedure :: vertical_dimension => ccp_get_vertical_dimension - procedure :: const_index => ccp_const_index - procedure :: field_index => ccp_field_index - procedure :: is_advected => ccp_is_advected - procedure :: equivalent => ccp_is_equivalent + procedure :: is_instantiated => ccp_is_instantiated + procedure :: standard_name => ccp_get_standard_name + procedure :: long_name => ccp_get_long_name + procedure :: is_layer_var => ccp_is_layer_var + procedure :: is_interface_var => ccp_is_interface_var + procedure :: is_2d_var => ccp_is_2d_var + procedure :: vertical_dimension => ccp_get_vertical_dimension + procedure :: const_index => ccp_const_index + procedure :: is_advected => ccp_is_advected + procedure :: equivalent => ccp_is_equivalent + procedure :: is_mass_mixing_ratio => ccp_is_mass_mixing_ratio + procedure :: is_volume_mixing_ratio => ccp_is_volume_mixing_ratio + procedure :: is_number_concentration => ccp_is_number_concentration + procedure :: is_dry => ccp_is_dry + procedure :: is_moist => ccp_is_moist + procedure :: is_wet => ccp_is_wet + procedure :: minimum => ccp_min_val + procedure :: molec_weight => ccp_molec_weight + procedure :: default_value => ccp_default_value + procedure :: has_default => ccp_has_default ! Copy method (be sure to update this anytime fields are added) procedure :: copyConstituent generic :: assignment(=) => copyConstituent - ! Methods that change state - procedure :: initialize => ccp_initialize + ! Methods that change state (XXgoldyXX: make private?) + procedure :: instantiate => ccp_instantiate procedure :: deallocate => ccp_deallocate procedure :: set_const_index => ccp_set_const_index - procedure :: set_field_index => ccp_set_field_index end type ccpp_constituent_properties_t +!! \section arg_table_ccpp_constituent_prop_ptr_t +!! \htmlinclude ccpp_constituent_prop_ptr_t.html +!! + type, public :: ccpp_constituent_prop_ptr_t + type(ccpp_constituent_properties_t), private, pointer :: prop => NULL() + contains + ! Informational methods + procedure :: standard_name => ccpt_get_standard_name + procedure :: long_name => ccpt_get_long_name + procedure :: is_layer_var => ccpt_is_layer_var + procedure :: is_interface_var => ccpt_is_interface_var + procedure :: is_2d_var => ccpt_is_2d_var + procedure :: vertical_dimension => ccpt_get_vertical_dimension + procedure :: const_index => ccpt_const_index + procedure :: is_advected => ccpt_is_advected + procedure :: is_mass_mixing_ratio => ccpt_is_mass_mixing_ratio + procedure :: is_volume_mixing_ratio => ccpt_is_volume_mixing_ratio + procedure :: is_number_concentration => ccpt_is_number_concentration + procedure :: is_dry => ccpt_is_dry + procedure :: is_moist => ccpt_is_moist + procedure :: is_wet => ccpt_is_wet + procedure :: minimum => ccpt_min_val + procedure :: molec_weight => ccpt_molec_weight + procedure :: default_value => ccpt_default_value + procedure :: has_default => ccpt_has_default + ! ccpt_set: Set the internal pointer + procedure :: set => ccpt_set + ! Methods that change state (XXgoldyXX: make private?) + procedure :: deallocate => ccpt_deallocate + procedure :: set_const_index => ccpt_set_const_index + end type ccpp_constituent_prop_ptr_t + +!! \section arg_table_ccpp_model_constituents_t +!! \htmlinclude ccpp_model_constituents_t.html +!! type, public :: ccpp_model_constituents_t ! A ccpp_model_constituents_t object holds all the metadata and field ! data for a model run's constituents along with data and methods ! to initialize and access the data. - integer, private :: num_layer_vars = 0 - integer, private :: num_interface_vars = 0 - integer, private :: num_2d_vars = 0 + !!XXgoldyXX: To do: allow accessor functions as CCPP local variable + !! names so that members can be private. + integer :: num_layer_vars = 0 + integer :: num_advected_vars = 0 integer, private :: num_layers = 0 - integer, private :: num_interfaces = 0 type(ccpp_hash_table_t), private :: hash_table logical, private :: table_locked = .false. + logical, private :: data_locked = .false. ! These fields are public to allow for efficient (i.e., no copying) ! usage even though it breaks object independence real(kind_phys), allocatable :: vars_layer(:,:,:) - real(kind_phys), allocatable :: vars_interface(:,:,:) - real(kind_phys), allocatable :: vars_2d(:,:) + real(kind_phys), allocatable :: vars_minvalue(:) ! An array containing all the constituent metadata - ! XXgoldyXX: Is this needed? Source of duplicate metadata? - type(ccpp_constituent_properties_t), allocatable :: const_metadata(:) + ! Each element contains a pointer to a constituent from the hash table + type(ccpp_constituent_prop_ptr_t), allocatable :: const_metadata(:) contains ! Return .true. if a constituent matches pattern procedure, private :: is_match => ccp_model_const_is_match ! Return a constituent from the hash table procedure, private :: find_const => ccp_model_const_find_const - ! Is the table locked (i.e., ready to be used)? + ! Are both the properties table and data array locked (i.e., ready to be used)? procedure :: locked => ccp_model_const_locked + ! Is the properties table locked (i.e., ready to be used)? + procedure :: const_props_locked => ccp_model_const_props_locked + ! Is the data array locked (i.e., ready to be used)? + procedure :: const_data_locked => ccp_model_const_data_locked ! Is it okay to add new metadata fields? procedure :: okay_to_add => ccp_model_const_okay_to_add ! Add a constituent's metadata to the master hash table procedure :: new_field => ccp_model_const_add_metadata ! Initialize hash table procedure :: initialize_table => ccp_model_const_initialize - ! Freeze hash table and initialize constituent field arrays - procedure :: lock_table => ccp_model_const_lock + ! Freeze hash table and set constituents properties + procedure :: lock_table => ccp_model_const_table_lock + ! Freeze and initialize constituent field arrays + procedure :: lock_data => ccp_model_const_data_lock ! Empty (reset) the entire object procedure :: reset => ccp_model_const_reset ! Query number of constituents matching pattern procedure :: num_constituents => ccp_model_const_num_match + ! Return index of constituent matching standard name + procedure :: const_index => ccp_model_const_index + ! Return metadata matching standard name + procedure :: field_metadata => ccp_model_const_metadata ! Gather constituent fields matching pattern - !!XXgoldyXX: Might need a 2D version of this procedure :: copy_in => ccp_model_const_copy_in_3d ! Update constituent fields matching pattern - !!XXgoldyXX: Might need a 2D version of this procedure :: copy_out => ccp_model_const_copy_out_3d - ! Return index of constituent matching standard name - procedure :: const_index => ccp_model_const_index - ! Return index of field matching standard name - procedure :: field_index => ccp_model_const_field_index - ! Return metadata matching standard name - procedure :: field_metada => ccp_model_const_metadata + ! Return pointer to constituent array (for use by host model) + procedure :: field_data_ptr => ccp_field_data_ptr + ! Return pointer to advected constituent array (for use by host model) + procedure :: advected_constituents_ptr => ccp_advected_data_ptr + ! Return pointer to constituent properties array (for use by host model) + procedure :: constituent_props_ptr => ccp_constituent_props_ptr end type ccpp_model_constituents_t - private int_unassigned + ! Private interfaces + private to_str + private initialize_errvars + private set_errvars private handle_allocate_error + private check_var_bounds CONTAINS @@ -121,12 +200,91 @@ subroutine copyConstituent(outConst, inConst) outConst%var_long_name = inConst%var_long_name outConst%vert_dim = inConst%vert_dim outConst%const_ind = inConst%const_ind - outConst%field_ind = inConst%field_ind outConst%advected = inConst%advected + outConst%const_type = inConst%const_type + outConst%const_water = inConst%const_water + outConst%min_val = inConst%min_val + outConst%const_default_value = inConst%const_default_value end subroutine copyConstituent !####################################################################### + character(len=10) function to_str(val) + ! return default integer as a left justified string + + ! Dummy argument + integer, intent(in) :: val + + write(to_str,'(i0)') val + + end function to_str + + !####################################################################### + + subroutine initialize_errvars(errcode, errmsg) + ! Initialize error variables, if present + + ! Dummy arguments + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + + if (present(errcode)) then + errcode = 0 + end if + if (present(errmsg)) then + errmsg = '' + end if + end subroutine initialize_errvars + + !####################################################################### + + subroutine set_errvars(errcode_val, errmsg_val, errcode, errmsg, & + errmsg2, errmsg3, errmsg4, errmsg5) + ! Set error variables, if present + + ! Dummy arguments + integer, optional, intent(in) :: errcode_val + character(len=*), optional, intent(in) :: errmsg_val + integer, optional, intent(inout) :: errcode + character(len=*), optional, intent(inout) :: errmsg + character(len=*), optional, intent(in) :: errmsg2 + character(len=*), optional, intent(in) :: errmsg3 + character(len=*), optional, intent(in) :: errmsg4 + character(len=*), optional, intent(in) :: errmsg5 + ! Local variable + integer :: emsg_len + + if (present(errcode)) then + errcode = errcode + errcode_val + end if + if (present(errmsg)) then + emsg_len = len_trim(errmsg) + if (emsg_len > 0) then + errmsg(emsg_len+1:) = '; ' + end if + emsg_len = len_trim(errmsg) + errmsg(emsg_len+1:) = trim(errmsg_val) + if (present(errmsg2)) then + emsg_len = len_trim(errmsg) + errmsg(emsg_len+1:) = trim(errmsg2) + end if + if (present(errmsg3)) then + emsg_len = len_trim(errmsg) + errmsg(emsg_len+1:) = trim(errmsg3) + end if + if (present(errmsg4)) then + emsg_len = len_trim(errmsg) + errmsg(emsg_len+1:) = trim(errmsg4) + end if + if (present(errmsg5)) then + emsg_len = len_trim(errmsg) + errmsg(emsg_len+1:) = trim(errmsg5) + end if + end if + end subroutine set_errvars + + !####################################################################### + subroutine handle_allocate_error(astat, fieldname, errcode, errmsg) ! Generate an error message if indicates an allocation failure @@ -136,28 +294,37 @@ subroutine handle_allocate_error(astat, fieldname, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg + call initialize_errvars(errcode, errmsg) if (astat /= 0) then - if (present(errcode)) then - errcode = astat - end if - if (present(errmsg)) then - write(errmsg, '(4a,i0)') 'Error allocating ', & - 'ccpp_constituent_properties_t object component, ', & - trim(fieldname), ', error code = ', astat - end if - else - if (present(errcode)) then - errcode = 0 - end if - if (present(errmsg)) then - errmsg = '' - end if + call set_errvars(astat, "Error allocating ", errcode=errcode, & + errmsg=errmsg, errmsg2="ccpp_constituent_properties_t", & + errmsg3="object component, "//trim(fieldname), & + errmsg4=", error code = ", errmsg5=to_str(astat)) end if end subroutine handle_allocate_error !####################################################################### + subroutine check_var_bounds(var, var_bound, varname, errcode, errmsg) + ! Generate an error message if indicates an allocation failure + + ! Dummy arguments + integer, intent(in) :: var + integer, intent(in) :: var_bound + character(len=*), intent(in) :: varname + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + + call initialize_errvars(errcode, errmsg) + if (var > var_bound) then + call set_errvars(1, trim(varname)//" exceeds its upper bound, ", & + errcode=errcode, errmsg=errmsg, errmsg2=to_str(var_bound)) + end if + end subroutine check_var_bounds + + !####################################################################### + function ccp_properties_get_key(hashable) ! Return the constituent properties class key (var_std_name) @@ -171,11 +338,11 @@ end function ccp_properties_get_key !####################################################################### - logical function ccp_is_initialized(this, errcode, errmsg) - ! Return .true. iff is initialized - ! If is *not* initialized and and/or is present, + logical function ccp_is_instantiated(this, errcode, errmsg) + ! Return .true. iff is instantiated + ! If is *not* instantiated and and/or is present, ! fill these fields with an error status - ! If *is* initialized and and/or is present, + ! If *is* instantiated and and/or is present, ! clear these fields. ! Dummy arguments @@ -183,44 +350,33 @@ logical function ccp_is_initialized(this, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - ccp_is_initialized = allocated(this%var_std_name) - if (ccp_is_initialized) then - if (present(errcode)) then - errcode = 0 - end if - if (present(errmsg)) then - errmsg = '' - end if - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) 'ccpp_constituent_properties_t object ', & - 'is not initialized' - end if + ccp_is_instantiated = allocated(this%var_std_name) + call initialize_errvars(errcode, errmsg) + if (.not. ccp_is_instantiated) then + call set_errvars(1, "ccpp_constituent_properties_t object ", & + errcode=errcode, errmsg=errmsg, errmsg2="is not initialized") end if - end function ccp_is_initialized + end function ccp_is_instantiated !####################################################################### - subroutine ccp_initialize(this, std_name, long_name, vertical_dim, & - advected, errcode, errmsg) + subroutine ccp_instantiate(this, std_name, long_name, units, vertical_dim, & + advected, default_value, errcode, errmsg) ! Initialize all fields in ! Dummy arguments class(ccpp_constituent_properties_t), intent(inout) :: this character(len=*), intent(in) :: std_name character(len=*), intent(in) :: long_name + character(len=*), intent(in) :: units character(len=*), intent(in) :: vertical_dim logical, optional, intent(in) :: advected + real(kind_phys), optional, intent(in) :: default_value integer, intent(out) :: errcode character(len=*), intent(out) :: errmsg - ! Local variable - integer :: astat - if (this%is_initialized()) then + if (this%is_instantiated()) then errcode = 1 write(errmsg, *) 'ccpp_constituent_properties_t object, ', & trim(std_name), ', is already initialized as ', this%var_std_name @@ -231,17 +387,43 @@ subroutine ccp_initialize(this, std_name, long_name, vertical_dim, & end if if (errcode == 0) then this%var_long_name = trim(long_name) + this%var_units = trim(units) this%vert_dim = trim(vertical_dim) if (present(advected)) then this%advected = advected else this%advected = .false. end if + if (present(default_value)) then + this%const_default_value = default_value + end if + end if + if (errcode == 0) then + if (index(this%var_std_name, "volume_mixing_ratio") > 0) then + this%const_type = volume_mixing_ratio + else if (index(this%var_std_name, "number_concentration") > 0) then + this%const_type = number_concentration + else + this%const_type = mass_mixing_ratio + end if + ! Determine if this is a (moist) mixing ratio or volume mixing ratio + end if + if (errcode == 0) then + ! Determine if this mixing ratio is dry, moist, or "wet". + if (index(this%var_std_name, "wrt_moist_air") > 0) then + this%const_water = moist_mixing_ratio + else if (this%var_std_name == "specific_humidity") then + this%const_water = moist_mixing_ratio + else if (this%var_std_name == "wrt_total_mass") then + this%const_water = wet_mixing_ratio + else + this%const_water = dry_mixing_ratio + end if end if if (errcode /= 0) then call this%deallocate() end if - end subroutine ccp_initialize + end subroutine ccp_instantiate !####################################################################### @@ -260,8 +442,11 @@ subroutine ccp_deallocate(this) if (allocated(this%vert_dim)) then deallocate(this%vert_dim) end if - this%field_ind = int_unassigned + this%const_ind = int_unassigned this%advected = .false. + this%const_type = int_unassigned + this%const_water = int_unassigned + this%const_default_value = kphys_unassigned end subroutine ccp_deallocate @@ -276,9 +461,12 @@ subroutine ccp_get_standard_name(this, std_name, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then + if (this%is_instantiated(errcode, errmsg)) then std_name = this%var_std_name + else + std_name = '' end if + end subroutine ccp_get_standard_name !####################################################################### @@ -292,9 +480,12 @@ subroutine ccp_get_long_name(this, long_name, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then + if (this%is_instantiated(errcode, errmsg)) then long_name = this%var_long_name + else + long_name = '' end if + end subroutine ccp_get_long_name !####################################################################### @@ -308,9 +499,12 @@ subroutine ccp_get_vertical_dimension(this, vert_dim, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then + if (this%is_instantiated(errcode, errmsg)) then vert_dim = this%vert_dim + else + vert_dim = '' end if + end subroutine ccp_get_vertical_dimension !####################################################################### @@ -321,7 +515,7 @@ logical function ccp_is_layer_var(this) result(is_layer) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this ! Local variable - character(len=32) :: dimname + character(len=dimname_len) :: dimname call this%vertical_dimension(dimname) is_layer = trim(dimname) == 'vertical_layer_dimension' @@ -336,7 +530,7 @@ logical function ccp_is_interface_var(this) result(is_interface) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this ! Local variable - character(len=32) :: dimname + character(len=dimname_len) :: dimname call this%vertical_dimension(dimname) is_interface = trim(dimname) == 'vertical_interface_dimension' @@ -351,7 +545,7 @@ logical function ccp_is_2d_var(this) result(is_2d) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this ! Local variable - character(len=32) :: dimname + character(len=dimname_len) :: dimname call this%vertical_dimension(dimname) is_2d = len_trim(dimname) == 0 @@ -361,33 +555,20 @@ end function ccp_is_2d_var !####################################################################### integer function ccp_const_index(this, errcode, errmsg) - ! Return this constituent's master index (or -1 of not assigned) + ! Return this constituent's array index (or -1 of not assigned) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then + if (this%is_instantiated(errcode, errmsg)) then ccp_const_index = this%const_ind - end if - end function ccp_const_index - - !####################################################################### - - integer function ccp_field_index(this, errcode, errmsg) - ! Return this constituent's field index (or -1 of not assigned) - - ! Dummy arguments - class(ccpp_constituent_properties_t), intent(in) :: this - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - - if (this%is_initialized(errcode, errmsg)) then - ccp_field_index = this%field_ind + else + ccp_const_index = int_unassigned end if - end function ccp_field_index + end function ccp_const_index !####################################################################### @@ -401,17 +582,13 @@ subroutine ccp_set_const_index(this, index, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then - if (this%const_ind /= int_unassigned) then + if (this%is_instantiated(errcode, errmsg)) then + if (this%const_ind == int_unassigned) then this%const_ind = index else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) 'ccpp_constituent_properties_t ', & - 'const index is already set' - end if + call set_errvars(1, "ccpp_constituent_properties_t ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="const index is already set") end if end if @@ -419,304 +596,494 @@ end subroutine ccp_set_const_index !####################################################################### - subroutine ccp_set_field_index(this, findex, errcode, errmsg) - ! Set this constituent's field index - ! It is an error to try to set an index if it is already set - - ! Dummy arguments - class(ccpp_constituent_properties_t), intent(inout) :: this - integer, intent(in) :: findex - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - - if (this%is_initialized(errcode, errmsg)) then - if (this%field_ind == int_unassigned) then - this%field_ind = findex - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) 'ccpp_constituent_properties_t ', & - 'field index is already set' - end if - end if - end if - end subroutine ccp_set_field_index - - !####################################################################### - - logical function ccp_is_advected(this, errcode, errmsg) + subroutine ccp_is_advected(this, val_out, errcode, errmsg) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg)) then - ccp_is_advected = this%advected + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%advected + else + val_out = .false. end if - end function ccp_is_advected + end subroutine ccp_is_advected !####################################################################### - logical function ccp_is_equivalent(this, oconst, & - errcode, errmsg) result(equiv) + subroutine ccp_is_equivalent(this, oconst, equiv, errcode, errmsg) ! Dummy arguments class(ccpp_constituent_properties_t), intent(in) :: this type(ccpp_constituent_properties_t), intent(in) :: oconst + logical, intent(out) :: equiv integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg - if (this%is_initialized(errcode, errmsg) .and. & - oconst%is_initialized(errcode, errmsg)) then + if (this%is_instantiated(errcode, errmsg) .and. & + oconst%is_instantiated(errcode, errmsg)) then equiv = (trim(this%var_std_name) == trim(oconst%var_std_name)) .and. & (trim(this%var_long_name) == trim(oconst%var_long_name)) .and. & (trim(this%vert_dim) == trim(oconst%vert_dim)) .and. & - (this%advected .eqv. oconst%advected) + (this%advected .eqv. oconst%advected) .and. & + (this%const_default_value == oconst%const_default_value) else equiv = .false. end if - end function ccp_is_equivalent + end subroutine ccp_is_equivalent - !######################################################################## - ! - ! CCPP_MODEL_CONSTITUENTS_T (constituent field data) methods - ! !######################################################################## - logical function ccp_model_const_locked(this, errcode, errmsg, warn_func) - ! Return .true. iff is locked (i.e., ready to use) - ! Optionally fill out and if object not initialized + subroutine ccp_is_mass_mixing_ratio(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - character(len=*), optional, intent(in) :: warn_func - ! Local variable - character(len=*), parameter :: subname = 'ccp_model_const_locked' + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg - if (present(errcode)) then - errcode = 0 - end if - if (present(errmsg)) then - errmsg = '' - end if - ccp_model_const_locked = .false. - ! Use an initialized hash table as double check - if (this%hash_table%is_initialized()) then - ccp_model_const_locked = this%table_locked - if ( (.not. this%table_locked) .and. & - present(errmsg) .and. present(warn_func)) then - ! Write a warning as a courtesy to calling function but do not set - ! errcode (let caller decide). - write(errmsg, *) trim(warn_func), & - ' WARNING: Model constituents not ready to use' - end if + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_type == mass_mixing_ratio else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - if (present(warn_func)) then - write(errmsg, *) trim(warn_func), & - ' WARNING: Model constituents not initialized' - else - write(errmsg, *) subname, & - ' WARNING: Model constituents not initialized' - end if - end if + val_out = .false. end if - - end function ccp_model_const_locked + end subroutine ccp_is_mass_mixing_ratio !######################################################################## - logical function ccp_model_const_okay_to_add(this, errcode, errmsg, & - warn_func) - ! Return .true. iff is initialized and not locked - ! Optionally fill out and if the conditions are not met. + subroutine ccp_is_volume_mixing_ratio(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(inout) :: this - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - character(len=*), optional, intent(in) :: warn_func - ! Local variable - character(len=*), parameter :: subname = 'ccp_model_const_okay_to_add' + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg - ccp_model_const_okay_to_add = this%hash_table%is_initialized() - if (ccp_model_const_okay_to_add) then - ccp_model_const_okay_to_add = .not. this%locked(errcode=errcode, & - errmsg=errmsg, warn_func=subname) - if (.not. ccp_model_const_okay_to_add) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - if (present(warn_func)) then - write(errmsg, *) trim(warn_func), & - ' WARNING: Model constituents are locked' - else - errmsg = subname//' WARNING: Model constituents are locked' - end if - end if - end if + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_type == volume_mixing_ratio else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - if (present(warn_func)) then - write(errmsg, *) trim(warn_func), & - ' WARNING: Model constituents not initialized' - else - errmsg = subname//' WARNING: Model constituents not initialized' - end if - end if + val_out = .false. end if + end subroutine ccp_is_volume_mixing_ratio - end function ccp_model_const_okay_to_add + !######################################################################## + + subroutine ccp_is_number_concentration(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_type == number_concentration + else + val_out = .false. + end if + end subroutine ccp_is_number_concentration !######################################################################## - subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) - ! Add a constituent's metadata to the master hash table + subroutine ccp_is_dry(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(inout) :: this - type(ccpp_constituent_properties_t), target, intent(in) :: field_data - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - character(len=256) :: error - character(len=*), parameter :: subnam = 'ccp_model_const_add_metadata' + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg - if (this%okay_to_add(errcode=errcode, errmsg=errmsg, warn_func=subnam)) then - error = '' -!!XXgoldyXX: Add check on key to see if incompatible item already there. - call this%hash_table%add_hash_key(field_data, error) - if (len_trim(error) > 0) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - errmsg = trim(error) - end if - else - ! If we get here we are successful, add to variable count - if (field_data%is_layer_var()) then - this%num_layer_vars = this%num_layer_vars + 1 - else if (field_data%is_interface_var()) then - this%num_interface_vars = this%num_interface_vars + 1 - else if (field_data%is_2d_var()) then - this%num_2d_vars = this%num_2d_vars + 1 - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call field_data%vertical_dimension(error, & - errcode=errcode, errmsg=errmsg) - if (len_trim(errmsg) == 0) then - write(errmsg, *) "ERROR: Unknown vertical dimension, '", & - trim(error), "'" - end if - end if - end if - end if + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_water == dry_mixing_ratio else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - errmsg = 'ERROR: Model contituents are locked' - end if + val_out = .false. end if - end subroutine ccp_model_const_add_metadata + end subroutine ccp_is_dry !######################################################################## - subroutine ccp_model_const_initialize(this, num_elements) - ! Initialize hash table, is total number of elements + subroutine ccp_is_moist(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(inout) :: this - integer, intent(in) :: num_elements - ! Local variable - integer :: tbl_size + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg - ! Clear any data - this%num_layer_vars = 0 - this%num_interface_vars = 0 - this%num_2d_vars = 0 - if (allocated(this%vars_layer)) then - deallocate(this%vars_layer) - end if - if (allocated(this%vars_interface)) then - deallocate(this%vars_interface) - end if - if (allocated(this%vars_2d)) then - deallocate(this%vars_2d) - end if - if (allocated(this%const_metadata)) then - deallocate(this%const_metadata) + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_water == moist_mixing_ratio + else + val_out = .false. end if - ! Figure a log base 2 for initializing hash table - tbl_size = num_elements * 10 ! Hash padding - tbl_size = int((log(real(tbl_size, kind_phys)) / log(2.0_kind_phys)) + & - 1.0_kind_phys) - ! Initialize hash table - call this%hash_table%initialize(tbl_size) - this%table_locked = .false. - end subroutine ccp_model_const_initialize + end subroutine ccp_is_moist !######################################################################## - function ccp_model_const_find_const(this, standard_name, errcode, errmsg) & - result(cprop) - ! Return a constituent with key, , from the hash table - ! must be locked to execute this function - ! Since this is a private function, error checking for locked status - ! is *not* performed. + subroutine ccp_is_wet(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - character(len=*), intent(in) :: standard_name - integer, optional, intent(out) :: errcode + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_water == wet_mixing_ratio + else + val_out = .false. + end if + + end subroutine ccp_is_wet + + !######################################################################## + + subroutine ccp_min_val(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%min_val + else + val_out = kphys_unassigned + end if + + end subroutine ccp_min_val + + !######################################################################## + + subroutine ccp_molec_weight(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%molar_mass + else + val_out = kphys_unassigned + end if + + end subroutine ccp_molec_weight + + !######################################################################## + + subroutine ccp_default_value(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_default_value + else + val_out = kphys_unassigned + end if + + end subroutine ccp_default_value + + !######################################################################## + + subroutine ccp_has_default(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccp_has_default' + + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_default_value /= kphys_unassigned + else + val_out = .false. + end if + + end subroutine ccp_has_default + + !######################################################################## + ! + ! CCPP_MODEL_CONSTITUENTS_T (constituent field data) methods + ! + !######################################################################## + + logical function ccp_model_const_locked(this, errcode, errmsg, warn_func) + ! Return .true. iff is locked (i.e., ready to use) + ! Optionally fill out and if object not initialized + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + character(len=*), optional, intent(in) :: warn_func + ! Local variable + character(len=*), parameter :: subname = 'ccp_model_const_locked' + + call initialize_errvars(errcode, errmsg) + ccp_model_const_locked = .false. + ! Use an initialized hash table as double check + if (this%hash_table%is_initialized()) then + ccp_model_const_locked = this%table_locked .and. this%data_locked + if ( (.not. (this%table_locked .and. this%data_locked)) .and. & + present(errmsg) .and. present(warn_func)) then + ! Write a warning as a courtesy to calling function but do not set + ! errcode (let caller decide). + write(errmsg, *) trim(warn_func), & + ' WARNING: Model constituents not ready to use' + end if + else + if (present(warn_func)) then + call set_errvars(1, trim(warn_func), & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents not initialized") + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents not initialized") + end if + end if + + end function ccp_model_const_locked + + !######################################################################## + + logical function ccp_model_const_props_locked(this, errcode, errmsg, warn_func) + ! Return .true. iff 's constituent properties are ready to use + ! Optionally fill out and if object not initialized + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + character(len=*), optional, intent(in) :: warn_func + ! Local variable + character(len=*), parameter :: subname = 'ccp_model_const_table_locked' + + call initialize_errvars(errcode, errmsg) + ccp_model_const_props_locked = .false. + ! Use an initialized hash table as double check + if (this%hash_table%is_initialized()) then + ccp_model_const_props_locked = this%table_locked + if ( .not. this%table_locked .and. & + present(errmsg) .and. present(warn_func)) then + ! Write a warning as a courtesy to calling function but do not set + ! errcode (let caller decide). + write(errmsg, *) trim(warn_func), & + ' WARNING: Model constituent properties not ready to use' + end if + else + if (present(warn_func)) then + call set_errvars(1, trim(warn_func), & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent properties not initialized") + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent properties not initialized") + end if + end if + + end function ccp_model_const_props_locked + + !######################################################################## + + logical function ccp_model_const_data_locked(this, errcode, errmsg, warn_func) + ! Return .true. iff 's data are ready to use + ! Optionally fill out and if object not initialized + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + character(len=*), optional, intent(in) :: warn_func + ! Local variable + character(len=*), parameter :: subname = 'ccp_model_const_data_locked' + + call initialize_errvars(errcode, errmsg) + ccp_model_const_data_locked = .false. + ! Use an initialized hash table as double check + if (this%hash_table%is_initialized()) then + ccp_model_const_data_locked = this%data_locked + if ( .not. this%data_locked .and. & + present(errmsg) .and. present(warn_func)) then + ! Write a warning as a courtesy to calling function but do not set + ! errcode (let caller decide). + write(errmsg, *) trim(warn_func), & + ' WARNING: Model constituent data not ready to use' + end if + else + if (present(warn_func)) then + call set_errvars(1, trim(warn_func), & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent data not initialized") + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent data not initialized") + end if + end if + + end function ccp_model_const_data_locked + + !######################################################################## + + logical function ccp_model_const_okay_to_add(this, errcode, errmsg, & + warn_func) + ! Return .true. iff is initialized and not locked + ! Optionally fill out and if the conditions + ! are not met. + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(inout) :: this + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + character(len=*), optional, intent(in) :: warn_func + ! Local variable + character(len=*), parameter :: subname = 'ccp_model_const_okay_to_add' + + ccp_model_const_okay_to_add = this%hash_table%is_initialized() + if (ccp_model_const_okay_to_add) then + ccp_model_const_okay_to_add = .not. (this%const_props_locked(errcode=errcode, & + errmsg=errmsg, warn_func=subname) .or. this%const_data_locked(errcode=errcode, & + errmsg=errmsg, warn_func=subname)) + if (.not. ccp_model_const_okay_to_add) then + if (present(warn_func)) then + call set_errvars(1, trim(warn_func), & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents are locked") + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents are locked") + end if + end if + else + if (present(warn_func)) then + call set_errvars(1, trim(warn_func), & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents not initialized") + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents not initialized") + end if + end if + + end function ccp_model_const_okay_to_add + + !######################################################################## + + subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) + ! Add a constituent's metadata to the master hash table + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(inout) :: this + type(ccpp_constituent_properties_t), target, intent(in) :: field_data + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + character(len=errmsg_len) :: error + character(len=*), parameter :: subname = 'ccp_model_const_add_metadata' + + if (this%okay_to_add(errcode=errcode, errmsg=errmsg, & + warn_func=subname)) then + error = '' +!!XXgoldyXX: Add check on key to see if incompatible item already there. + call this%hash_table%add_hash_key(field_data, error) + if (len_trim(error) > 0) then + call set_errvars(1, trim(error), errcode=errcode, errmsg=errmsg) + else + ! If we get here we are successful, add to variable count + if (field_data%is_layer_var()) then + this%num_layer_vars = this%num_layer_vars + 1 + else + if (present(errmsg)) then + call field_data%vertical_dimension(error, & + errcode=errcode, errmsg=errmsg) + if (len_trim(errmsg) == 0) then + call set_errvars(1, & + "ERROR: Unknown vertical dimension, '", & + errcode=errcode, errmsg=errmsg, & + errmsg2=trim(error), errmsg3="'") + end if + end if + end if + end if + else + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituents are locked") + end if + + end subroutine ccp_model_const_add_metadata + + !######################################################################## + + subroutine ccp_model_const_initialize(this, num_elements) + ! Initialize hash table, is total number of elements + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(inout) :: this + integer, intent(in) :: num_elements + ! Local variable + integer :: tbl_size + + ! Clear any data + call this%reset() + ! Figure a log base 2 for initializing hash table + tbl_size = num_elements * 10 ! Hash padding + tbl_size = int((log(real(tbl_size, kind_phys)) / log(2.0_kind_phys)) + & + 1.0_kind_phys) + ! Initialize hash table + call this%hash_table%initialize(tbl_size) + this%table_locked = .false. + + end subroutine ccp_model_const_initialize + + !######################################################################## + + function ccp_model_const_find_const(this, standard_name, errcode, errmsg) & + result(cprop) + ! Return a constituent with key, , from the hash table + ! must be locked to execute this function + ! Since this is a private function, error checking for locked status + ! is *not* performed. + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + character(len=*), intent(in) :: standard_name + integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg type(ccpp_constituent_properties_t), pointer :: cprop ! Local variables class(ccpp_hashable_t), pointer :: hval - character(len=256) :: error + character(len=errmsg_len) :: error character(len=*), parameter :: subname = 'ccp_model_const_find_const' nullify(cprop) hval => this%hash_table%table_value(standard_name, errmsg=error) if (len_trim(error) > 0) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, ': ', trim(error) - end if + call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & + errmsg2=": "//trim(error)) else select type(hval) type is (ccpp_constituent_properties_t) cprop => hval class default - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, ' ERROR: Bad hash table value', & - trim(standard_name) - end if + call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: Bad hash table value", & + errmsg3=trim(standard_name)) end select end if @@ -724,118 +1091,121 @@ end function ccp_model_const_find_const !######################################################################## - subroutine ccp_model_const_lock(this, ncols, num_layers, num_interfaces, & - errcode, errmsg) - ! Freeze hash table and initialize constituent field arrays + subroutine ccp_model_const_table_lock(this, errcode, errmsg) + ! Freeze hash table and initialize constituent properties ! Dummy arguments class(ccpp_model_constituents_t), intent(inout) :: this - integer, intent(in) :: ncols - integer, intent(in) :: num_layers - integer, intent(in) :: num_interfaces integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg ! Local variables - integer :: index_layer - integer :: index_interface - integer :: index_2d integer :: index_const + integer :: index_advect + integer :: num_vars integer :: astat + logical :: check type(ccpp_hash_iterator_t) :: hiter class(ccpp_hashable_t), pointer :: hval type(ccpp_constituent_properties_t), pointer :: cprop - character(len=32) :: dimname - character(len=*), parameter :: subname = 'ccp_model_const_lock' - - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - if (len_trim(errmsg) == 0) then - write(errmsg, *) subname, & - ' WARNING: Model constituents already locked, ignoring' - end if - end if + character(len=dimname_len) :: dimname + character(len=*), parameter :: subname = 'ccp_model_const_table_lock' + + astat = 0 + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent properites already locked, ignoring") + astat = astat + 1 else - index_layer = 0 - index_interface = 0 - index_2d = 0 - index_const = 0 ! Make sure everything is really initialized - if (allocated(this%vars_layer)) then - deallocate(this%vars_layer) - end if - if (allocated(this%vars_interface)) then - deallocate(this%vars_interface) - end if - if (allocated(this%vars_2d)) then - deallocate(this%vars_2d) - end if - if (allocated(this%const_metadata)) then - deallocate(this%const_metadata) - end if + call this%reset(clear_hash_table=.false.) + this%num_advected_vars = 0 ! Allocate the constituent array - allocate(this%const_metadata(this%hash_table%num_values()), stat=astat) + num_vars = this%hash_table%num_values() + allocate(this%const_metadata(num_vars), stat=astat) call handle_allocate_error(astat, 'const_metadata', & errcode=errcode, errmsg=errmsg) - ! Iterate through the hash table to find entries + ! We want to pack the advected constituents at the beginning of + ! the field array so we need to know how many there are if (astat == 0) then call hiter%initialize(this%hash_table) do if (hiter%valid()) then - index_const = index_const + 1 - if (index_const > SIZE(this%const_metadata)) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, & - " ERROR: const index out of bounds" - end if - exit - end if hval => hiter%value() select type(hval) type is (ccpp_constituent_properties_t) cprop => hval - call cprop%set_const_index(index_const, & - errcode=errcode, errmsg=errmsg) - ! Figure out which type of variable this is - if (cprop%is_layer_var()) then - index_layer = index_layer + 1 - call cprop%set_field_index(index_layer, & - errcode=errcode, errmsg=errmsg) - else if (cprop%is_interface_var()) then - index_interface = index_interface + 1 - call cprop%set_field_index(index_interface, & - errcode=errcode, errmsg=errmsg) - else if (cprop%is_2d_var()) then - index_2d = index_2d + 1 - call cprop%set_field_index(index_2d, & - errcode=errcode, errmsg=errmsg) - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call cprop%vertical_dimension(dimname, & - errcode=errcode, errmsg=errmsg) - if (len_trim(errmsg) == 0) then - write(errmsg, *) subname, & - " ERROR: Bad vertical dimension, '", & - trim(dimname), "'" - end if - end if - end if - this%const_metadata(index_const) = cprop - class default - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, 'ERROR: Bad hash table value' + call cprop%is_advected(check) + if (check) then + this%num_advected_vars = this%num_advected_vars + 1 end if + end select + call hiter%next() + else + exit + end if + end do + ! Sanity check on num_advect + if (this%num_advected_vars > num_vars) then + astat = 1 + call set_errvars(astat, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: num_advected_vars index out of bounds") + astat = astat + 1 + end if + end if + index_advect = 0 + index_const = this%num_advected_vars + ! Iterate through the hash table to find entries + if (astat == 0) then + call hiter%initialize(this%hash_table) + do + if (hiter%valid()) then + hval => hiter%value() + select type(hval) + type is (ccpp_constituent_properties_t) + cprop => hval + call cprop%is_advected(check) + if (check) then + index_advect = index_advect + 1 + if (index_advect > this%num_advected_vars) then + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: const a index out of bounds") + astat = astat + 1 + exit + end if + call cprop%set_const_index(index_advect, & + errcode=errcode, errmsg=errmsg) + call this%const_metadata(index_advect)%set(cprop) + else + index_const = index_const + 1 + if (index_const > num_vars) then + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: const v index out of bounds") + astat = astat + 1 + exit + end if + call cprop%set_const_index(index_const, & + errcode=errcode, errmsg=errmsg) + call this%const_metadata(index_const)%set(cprop) + end if + ! Make sure this is a layer variable + if (.not. cprop%is_layer_var()) then + call cprop%vertical_dimension(dimname, & + errcode=errcode, errmsg=errmsg) + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: Bad vertical dimension, '", & + errmsg3=trim(dimname)) + astat = astat + 1 + exit + end if + class default + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2="ERROR: Bad hash table value") + astat = astat + 1 exit end select call hiter%next() @@ -845,63 +1215,17 @@ subroutine ccp_model_const_lock(this, ncols, num_layers, num_interfaces, & end do ! Some size sanity checks if (index_const /= this%hash_table%num_values()) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, & - " ERROR: Too few constituents found in hash table" - end if - else if (index_layer /= this%num_layer_vars) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, '(2a,i0,a,i0)') subname, & - " ERROR: Wrong number of layer variables found (", & - index_layer, ") should be ", this%num_layer_vars - end if - else if (index_interface /= this%num_interface_vars) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, '(2a,i0,a,i0)') subname, & - " ERROR: Wrong number of interface variables found (", & - index_interface, ") should be ", this%num_interface_vars - end if - else if (index_2d /= this%num_2d_vars) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, '(2a,i0,a,i0)') subname, & - " ERROR: Wrong number of 2D variables found (", & - index_2d, ") should be ", this%num_2d_vars - end if + call set_errvars(errcode + 1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: Too few constituents found in hash table") + astat = astat + 1 end if - ! Everything looks okay, allocate field arrays - allocate(this%vars_layer(ncols, num_layers, index_layer), & - stat=astat) - call handle_allocate_error(astat, 'vars_layer', & - errcode=errcode, errmsg=errmsg) - if (astat == 0) then - this%num_layers = num_layers - this%vars_layer = kphys_unassigned - allocate(this%vars_interface(ncols, num_interfaces, & - index_layer), stat=astat) - call handle_allocate_error(astat, 'vars_interface', & - errcode=errcode, errmsg=errmsg) - end if - if (astat == 0) then - this%num_interfaces = num_interfaces - this%vars_interface = kphys_unassigned - allocate(this%vars_2d(ncols, index_2d), stat=astat) - call handle_allocate_error(astat, 'vars_2d', & - errcode=errcode, errmsg=errmsg) - end if - if (astat == 0) then - this%vars_2d = kphys_unassigned + if (index_advect /= this%num_advected_vars) then + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=" ERROR: Too few advected constituents found ", & + errmsg3="in hash table") + astat = astat + 1 end if if (present(errcode)) then if (errcode /= 0) then @@ -914,351 +1238,912 @@ subroutine ccp_model_const_lock(this, ncols, num_layers, num_interfaces, & end if end if - end subroutine ccp_model_const_lock + end subroutine ccp_model_const_table_lock + + !######################################################################## + + subroutine ccp_model_const_data_lock(this, ncols, num_layers, errcode, errmsg) + ! Freeze hash table and initialize constituent arrays + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(inout) :: this + integer, intent(in) :: ncols + integer, intent(in) :: num_layers + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + integer :: astat, index + real(kind=kind_phys) :: default_value + character(len=*), parameter :: subname = 'ccp_model_const_data_lock' + + if (this%const_data_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent data already locked, ignoring") + astat = astat + 1 + else if (.not. this%const_props_locked(errcode=errcode, errmsg=errmsg, & + warn_func=subname)) then + call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & + errmsg2=" WARNING: Model constituent properties not yet locked, ignoring") + astat = astat + 1 + else + allocate(this%vars_layer(ncols, num_layers, this%hash_table%num_values()), & + stat=astat) + call handle_allocate_error(astat, 'vars_layer', & + errcode=errcode, errmsg=errmsg) + if (astat == 0) then + allocate(this%vars_minvalue(this%hash_table%num_values()), stat=astat) + call handle_allocate_error(astat, 'vars_minvalue', & + errcode=errcode, errmsg=errmsg) + end if + if (astat == 0) then + this%num_layers = num_layers + do index = 1, this%hash_table%num_values() + call this%const_metadata(index)%default_value(default_value, & + errcode, errmsg) + this%vars_layer(:,:,index) = default_value + end do + this%vars_minvalue = 0.0_kind_phys + end if + if (present(errcode)) then + if (errcode /= 0) then + astat = 1 + end if + end if + if (astat == 0) then + this%data_locked = .true. + end if + end if + + end subroutine ccp_model_const_data_lock + + !######################################################################## + + subroutine ccp_model_const_reset(this, clear_hash_table) + ! Empty (reset) the entire object + ! Optionally do not clear the hash table (and its data) + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(inout) :: this + logical, optional, intent(in) :: clear_hash_table + ! Local variables + logical :: clear_table + integer :: index + + if (present(clear_hash_table)) then + clear_table = clear_hash_table + else + clear_table = .true. + end if + if (allocated(this%vars_layer)) then + deallocate(this%vars_layer) + end if + if (allocated(this%vars_minvalue)) then + deallocate(this%vars_minvalue) + end if + if (allocated(this%const_metadata)) then + if (clear_table) then + do index = 1, size(this%const_metadata, 1) + call this%const_metadata(index)%deallocate() + end do + end if + deallocate(this%const_metadata) + end if + if (clear_table) then + this%num_layer_vars = 0 + this%num_advected_vars = 0 + this%num_layers = 0 + call this%hash_table%clear() + end if + + end subroutine ccp_model_const_reset + + !######################################################################## + + logical function ccp_model_const_is_match(this, index, advected) & + result(is_match) + ! Return .true. iff the constituent at matches a pattern + ! Each (optional) property which is present represents something + ! which is required as part of a match. + ! Since this is a private function, error checking for locked status + ! is *not* performed. + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + integer, intent(in) :: index + logical, optional, intent(in) :: advected + ! Local variable + logical :: check + + ! By default, every constituent is a match + is_match = .true. + if (present(advected)) then + call this%const_metadata(index)%is_advected(check) + if (advected .neqv. check) then + is_match = .false. + end if + end if + + end function ccp_model_const_is_match + + !######################################################################## + + subroutine ccp_model_const_num_match(this, nmatch, advected, errcode, errmsg) + ! Query number of constituents matching pattern + ! Each (optional) property which is present represents something + ! which is required as part of a match. + ! must be locked to execute this function + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + integer, intent(out) :: nmatch + logical, optional, intent(in) :: advected + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + integer :: index + character(len=*), parameter :: subname = "ccp_model_const_num_match" + + nmatch = 0 + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + do index = 1, SIZE(this%const_metadata) + if (this%is_match(index, advected=advected)) then + nmatch = nmatch + 1 + end if + end do + end if + + end subroutine ccp_model_const_num_match + + !######################################################################## + + subroutine ccp_model_const_index(this, index, standard_name, errcode, errmsg) + ! Return index of metadata matching . + ! must be locked to execute this function + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + character(len=*), intent(in) :: standard_name + integer, intent(out) :: index + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + type(ccpp_constituent_properties_t), pointer :: cprop + character(len=*), parameter :: subname = "ccp_model_const_index" + + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + cprop => this%find_const(standard_name, errcode=errcode, errmsg=errmsg) + if (associated(cprop)) then + index = cprop%const_index() + else + index = int_unassigned + end if + else + index = int_unassigned + end if + + end subroutine ccp_model_const_index + + !######################################################################## + + subroutine ccp_model_const_metadata(this, standard_name, const_data, & + errcode, errmsg) + ! Return metadata matching standard name + ! must be locked to execute this function + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + character(len=*), intent(in) :: standard_name + type(ccpp_constituent_properties_t), intent(out) :: const_data + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + type(ccpp_constituent_properties_t), pointer :: cprop + character(len=*), parameter :: subname = "ccp_model_const_metadata" + + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + cprop => this%find_const(standard_name, errcode=errcode, errmsg=errmsg) + if (associated(cprop)) then + const_data = cprop + end if + end if + + end subroutine ccp_model_const_metadata + + !######################################################################## + + subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & + errcode, errmsg) + ! Gather constituent fields matching pattern + ! Each (optional) property which is present represents something + ! which is required as part of a match. + ! must be locked to execute this function + + ! Dummy arguments + class(ccpp_model_constituents_t), intent(in) :: this + real(kind_phys), intent(out) :: const_array(:,:,:) + logical, optional, intent(in) :: advected + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + integer :: index ! const_metadata index + integer :: cindex ! const_array index + integer :: fld_ind ! const field index + integer :: max_cind ! Size of const_array + integer :: num_levels ! Levels of const_array + character(len=stdname_len) :: std_name + character(len=*), parameter :: subname = "ccp_model_const_copy_in_3d" + + if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + cindex = 0 + max_cind = SIZE(const_array, 3) + num_levels = SIZE(const_array, 2) + do index = 1, SIZE(this%const_metadata) + if (this%is_match(index, advected=advected)) then + ! See if we have room for another constituent + cindex = cindex + 1 + if (cindex > max_cind) then + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=": Too many constituents for ") + exit + end if + ! Copy this constituent's field data to + call this%const_metadata(index)%const_index(fld_ind) + if (fld_ind /= index) then + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname//": ERROR: ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="bad field index, "//to_str(fld_ind), & + errmsg3=" for '"//trim(std_name)//"', ", & + errmsg4="should have been "//to_str(index)) + exit + else if (this%const_metadata(index)%is_layer_var()) then + if (this%num_layers == num_levels) then + const_array(:,:,cindex) = this%vars_layer(:,:,fld_ind) + else + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=": Wrong number of vertical levels for '", & + errmsg3=trim(std_name)//"', "//to_str(num_levels), & + errmsg4=", expected"//to_str(this%num_layers)) + exit + end if + else + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname//": Unsupported var type, ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="wrong number of vertical levels for '", & + errmsg3=trim(std_name)//"', "//to_str(num_levels), & + errmsg4=", expected"//to_str(this%num_layers)) + exit + end if + end if + end do + end if + + end subroutine ccp_model_const_copy_in_3d + + !######################################################################## + + subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & + errcode, errmsg) + ! Update constituent fields matching pattern + ! Each (optional) property which is present represents something + ! which is required as part of a match. + ! must be locked to execute this function + + ! Dummy argument + class(ccpp_model_constituents_t), intent(inout) :: this + real(kind_phys), intent(in) :: const_array(:,:,:) + logical, optional, intent(in) :: advected + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variables + integer :: index ! const_metadata index + integer :: cindex ! const_array index + integer :: fld_ind ! const field index + integer :: max_cind ! Size of const_array + integer :: num_levels ! Levels of const_array + character(len=stdname_len) :: std_name + character(len=*), parameter :: subname = "ccp_model_const_copy_out_3d" + + if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + cindex = 0 + max_cind = SIZE(const_array, 3) + num_levels = SIZE(const_array, 2) + do index = 1, SIZE(this%const_metadata) + if (this%is_match(index, advected=advected)) then + ! See if we have room for another constituent + cindex = cindex + 1 + if (cindex > max_cind) then + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=": Too many constituents for ") + exit + end if + ! Copy this field of to to constituent's field data + call this%const_metadata(index)%const_index(fld_ind) + if (fld_ind /= index) then + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname//": ERROR: ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="bad field index, "//to_str(fld_ind), & + errmsg3=" for '"//trim(std_name)//"', ", & + errmsg4="should have been "//to_str(index)) + exit + else if (this%const_metadata(index)%is_layer_var()) then + if (this%num_layers == num_levels) then + this%vars_layer(:,:,fld_ind) = const_array(:,:,cindex) + else + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname, & + errcode=errcode, errmsg=errmsg, & + errmsg2=": Wrong number of vertical levels for '", & + errmsg3=trim(std_name)//"', "//to_str(num_levels), & + errmsg4=", expected"//to_str(this%num_layers)) + exit + end if + else + call this%const_metadata(index)%standard_name(std_name) + call set_errvars(1, subname//": Unsupported var type, ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="wrong number of vertical levels for '", & + errmsg3=trim(std_name)//"', "//to_str(num_levels), & + errmsg4=", expected"//to_str(this%num_layers)) + exit + end if + end if + end do + end if + + end subroutine ccp_model_const_copy_out_3d + + !######################################################################## + + function ccp_field_data_ptr(this) result(const_ptr) + ! Return pointer to constituent array (for use by host model) + + ! Dummy arguments + class(ccpp_model_constituents_t), target, intent(inout) :: this + real(kind_phys), pointer :: const_ptr(:,:,:) + ! Local variables + integer :: errcode + character(len=errmsg_len) :: errmsg + character(len=*), parameter :: subname = 'ccp_field_data_ptr' + + if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + const_ptr => this%vars_layer + else + ! We don't want output variables in a function so just nullify + ! See note above about creating a 'last_error' method + nullify(const_ptr) + end if + + end function ccp_field_data_ptr + + !######################################################################## + + function ccp_advected_data_ptr(this) result(const_ptr) + ! Return pointer to advected constituent array (for use by host model) + + ! Dummy arguments + class(ccpp_model_constituents_t), target, intent(inout) :: this + real(kind_phys), pointer :: const_ptr(:,:,:) + ! Local variables + integer :: errcode + character(len=errmsg_len) :: errmsg + character(len=*), parameter :: subname = 'ccp_advected_data_ptr' + + if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + const_ptr => this%vars_layer(:,:,1:this%num_advected_vars) + else + ! We don't want output variables in a function so just nullify + ! See note above about creating a 'last_error' method + nullify(const_ptr) + end if + + end function ccp_advected_data_ptr + + function ccp_constituent_props_ptr(this) result(const_ptr) + ! Return pointer to constituent properties array (for use by host model) + + ! Dummy arguments + class(ccpp_model_constituents_t), target, intent(inout) :: this + type(ccpp_constituent_prop_ptr_t), pointer :: const_ptr(:) + ! Local variables + integer :: errcode + character(len=errmsg_len) :: errmsg + character(len=*), parameter :: subname = 'ccp_constituent_props_ptr' + + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + const_ptr => this%const_metadata + else + ! We don't want output variables in a function so just nullify + ! See note above about creating a 'last_error' method + nullify(const_ptr) + end if + + end function ccp_constituent_props_ptr + + !######################################################################## + + !##################################### + ! ccpp_constituent_prop_ptr_t methods + !##################################### + + !####################################################################### + + subroutine ccpt_get_standard_name(this, std_name, errcode, errmsg) + ! Return this constituent's standard name + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + character(len=*), intent(out) :: std_name + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_get_standard_name' + + if (associated(this%prop)) then + call this%prop%standard_name(std_name, errcode, errmsg) + else + std_name = '' + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_get_standard_name + + !####################################################################### + + subroutine ccpt_get_long_name(this, long_name, errcode, errmsg) + ! Return this constituent's long name (description) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + character(len=*), intent(out) :: long_name + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_get_long_name' + + if (associated(this%prop)) then + call this%prop%long_name(long_name, errcode, errmsg) + else + long_name = '' + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_get_long_name + + !####################################################################### + + subroutine ccpt_get_vertical_dimension(this, vert_dim, errcode, errmsg) + ! Return the standard name of this constituent's vertical dimension + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + character(len=*), intent(out) :: vert_dim + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_get_vertical_dimension' + + if (associated(this%prop)) then + if (this%prop%is_instantiated(errcode, errmsg)) then + vert_dim = this%prop%vert_dim + end if + else + vert_dim = '' + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_get_vertical_dimension + + !####################################################################### + + logical function ccpt_is_layer_var(this) result(is_layer) + ! Return .true. iff this constituent has a layer vertical dimension + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + ! Local variables + character(len=dimname_len) :: dimname + character(len=*), parameter :: subname = 'ccpt_is_layer_var' + + if (associated(this%prop)) then + call this%prop%vertical_dimension(dimname) + is_layer = trim(dimname) == 'vertical_layer_dimension' + else + is_layer = .false. + end if + + end function ccpt_is_layer_var + + !####################################################################### + + logical function ccpt_is_interface_var(this) result(is_interface) + ! Return .true. iff this constituent has a interface vertical dimension + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + ! Local variables + character(len=dimname_len) :: dimname + character(len=*), parameter :: subname = 'ccpt_is_interface_var' + + if (associated(this%prop)) then + call this%prop%vertical_dimension(dimname) + is_interface = trim(dimname) == 'vertical_interface_dimension' + else + is_interface = .false. + end if + + end function ccpt_is_interface_var + + !####################################################################### + + logical function ccpt_is_2d_var(this) result(is_2d) + ! Return .true. iff this constituent has a 2d vertical dimension + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + ! Local variables + character(len=dimname_len) :: dimname + character(len=*), parameter :: subname = 'ccpt_is_2d_var' + + if (associated(this%prop)) then + call this%prop%vertical_dimension(dimname) + is_2d = len_trim(dimname) == 0 + else + is_2d = .false. + end if + + end function ccpt_is_2d_var + + !####################################################################### + + subroutine ccpt_const_index(this, index, errcode, errmsg) + ! Return this constituent's master index (or -1 of not assigned) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + integer, intent(out) :: index + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_const_index' + + if (associated(this%prop)) then + index = this%prop%const_index(errcode, errmsg) + else + index = int_unassigned + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_const_index + + !####################################################################### + + subroutine ccpt_is_advected(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_advected' + + if (associated(this%prop)) then + call this%prop%is_advected(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_advected + + !######################################################################## + + subroutine ccpt_is_mass_mixing_ratio(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_mass_mixing_ratio' + + if (associated(this%prop)) then + call this%prop%is_mass_mixing_ratio(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_mass_mixing_ratio + + !######################################################################## + + subroutine ccpt_is_volume_mixing_ratio(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_volume_mixing_ratio' + + if (associated(this%prop)) then + call this%prop%is_volume_mixing_ratio(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_volume_mixing_ratio !######################################################################## - subroutine ccp_model_const_reset(this) - ! Empty (reset) the entire object + subroutine ccpt_is_number_concentration(this, val_out, errcode, errmsg) - ! Dummy argument - class(ccpp_model_constituents_t), intent(inout) :: this + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_number_concentration' - if (allocated(this%vars_layer)) then - deallocate(this%vars_layer) - end if - if (allocated(this%vars_interface)) then - deallocate(this%vars_interface) + if (associated(this%prop)) then + call this%prop%is_number_concentration(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - if (allocated(this%vars_2d)) then - deallocate(this%vars_2d) + + end subroutine ccpt_is_number_concentration + + !######################################################################## + + subroutine ccpt_is_dry(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_dry' + + if (associated(this%prop)) then + call this%prop%is_dry(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - if (allocated(this%const_metadata)) then - deallocate(this%const_metadata) + + end subroutine ccpt_is_dry + + !######################################################################## + + subroutine ccpt_is_moist(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_moist' + + if (associated(this%prop)) then + call this%prop%is_moist(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - call this%hash_table%clear() - end subroutine ccp_model_const_reset + end subroutine ccpt_is_moist !######################################################################## - logical function ccp_model_const_is_match(this, index, advected) & - result(is_match) - ! Return .true. iff the constituent at matches a pattern - ! Each (optional) property which is present represents something - ! which is required as part of a match. - ! Since this is a private function, error checking for locked status - ! is *not* performed. + subroutine ccpt_is_wet(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - integer, intent(in) :: index - logical, optional, intent(in) :: advected + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_wet' - ! By default, every constituent is a match - is_match = .true. - if (present(advected)) then - if (advected .neqv. this%const_metadata(index)%is_advected()) then - is_match = .false. - end if + if (associated(this%prop)) then + call this%prop%is_wet(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end function ccp_model_const_is_match + end subroutine ccpt_is_wet !######################################################################## - integer function ccp_model_const_num_match(this, advected, & - errcode, errmsg) result(nmatch) - ! Query number of constituents matching pattern - ! Each (optional) property which is present represents something - ! which is required as part of a match. - ! must be locked to execute this function + subroutine ccpt_min_val(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - logical, optional, intent(in) :: advected - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - integer :: index - character(len=*), parameter :: subname = "ccp_model_const_num_match" + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_min_val' - nmatch = 0 - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then - nmatch = nmatch + 1 - end if - end do + if (associated(this%prop)) then + call this%prop%minimum(val_out, errcode, errmsg) + else + val_out = kphys_unassigned + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end function ccp_model_const_num_match + end subroutine ccpt_min_val !######################################################################## - subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & - errcode, errmsg) - ! Gather constituent fields matching pattern - ! Each (optional) property which is present represents something - ! which is required as part of a match. - ! must be locked to execute this function + subroutine ccpt_molec_weight(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - real(kind_phys), intent(out) :: const_array(:,:,:) - logical, optional, intent(in) :: advected - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - integer :: index ! const_metadata index - integer :: cindex ! const_array index - integer :: fld_ind ! const field index - integer :: max_cind ! Size of const_array - integer :: num_levels ! Levels of const_array - character(len=64) :: std_name - character(len=*), parameter :: subname = "ccp_model_const_copy_in_3d" + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_molec_weight' - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - cindex = 0 - max_cind = SIZE(const_array, 3) - num_levels = SIZE(const_array, 2) - do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then - ! See if we have room for another constituent - cindex = cindex + 1 - if (cindex > max_cind) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, & - ": Too many constituents for " - end if - exit - end if - ! Copy this constituent's field data to - fld_ind = this%const_metadata(index)%field_index() - if (fld_ind < 1) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": No field index for '", trim(std_name), "'" - end if - else if (this%const_metadata(index)%is_layer_var()) then - if (this%num_layers == num_levels) then - const_array(:,:,cindex) = this%vars_layer(:,:,fld_ind) - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": Wrong number of vertical levels for ", & - trim(std_name), ', ', num_levels, & - ', expected ', this%num_layers - end if - exit - end if - else if (this%const_metadata(index)%is_interface_var()) then - if (this%num_interfaces == num_levels) then - const_array(:,:,cindex) = this%vars_interface(:,:,fld_ind) - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": Wrong number of vertical levels for ", & - std_name, ', ', num_levels, ', expected ', & - this%num_interfaces - end if - exit - end if - end if - end if - end do + if (associated(this%prop)) then + call this%prop%molec_weight(val_out, errcode, errmsg) + else + val_out = kphys_unassigned + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end subroutine ccp_model_const_copy_in_3d + end subroutine ccpt_molec_weight !######################################################################## - subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & - errcode, errmsg) - ! Update constituent fields matching pattern - ! Each (optional) property which is present represents something - ! which is required as part of a match. - ! must be locked to execute this function + subroutine ccpt_default_value(this, val_out, errcode, errmsg) - ! Dummy argument - class(ccpp_model_constituents_t), intent(inout) :: this - real(kind_phys), intent(in) :: const_array(:,:,:) - logical, optional, intent(in) :: advected - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - integer :: index ! const_metadata index - integer :: cindex ! const_array index - integer :: fld_ind ! const field index - integer :: max_cind ! Size of const_array - integer :: num_levels ! Levels of const_array - character(len=64) :: std_name - character(len=*), parameter :: subname = "ccp_model_const_copy_out_3d" + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + real(kind_phys), intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_default_value' - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - cindex = 0 - max_cind = SIZE(const_array, 3) - num_levels = SIZE(const_array, 2) - do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then - ! See if we have room for another constituent - cindex = cindex + 1 - if (cindex > max_cind) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - write(errmsg, *) subname, & - ": Too many constituents for " - end if - exit - end if - ! Copy this field of to to constituent's field data - fld_ind = this%const_metadata(index)%field_index() - if (fld_ind < 1) then - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": No field index for '", trim(std_name), "'" - end if - else if (this%const_metadata(index)%is_layer_var()) then - if (this%num_layers == num_levels) then - this%vars_layer(:,:,fld_ind) = const_array(:,:,cindex) - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": Wrong number of vertical levels for ", & - trim(std_name), ', ', num_levels, & - ', expected ', this%num_layers - end if - exit - end if - else if (this%const_metadata(index)%is_interface_var()) then - if (this%num_interfaces == num_levels) then - this%vars_interface(:,:,fld_ind) = const_array(:,:,cindex) - else - if (present(errcode)) then - errcode = 1 - end if - if (present(errmsg)) then - call this%const_metadata(index)%standard_name(std_name) - write(errmsg, '(4a,i0,a,i0)') subname, & - ": Wrong number of vertical levels for ", & - std_name, ', ', num_levels, ', expected ', & - this%num_interfaces - end if - exit - end if - end if - end if - end do + if (associated(this%prop)) then + call this%prop%default_value(val_out, errcode, errmsg) + else + val_out = kphys_unassigned + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end subroutine ccp_model_const_copy_out_3d + end subroutine ccpt_default_value !######################################################################## - integer function ccp_model_const_index(this, standard_name, errcode, errmsg) - ! Return index of metadata matching . - ! must be locked to execute this function + subroutine ccpt_has_default(this, val_out, errcode, errmsg) ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - character(len=*), intent(in) :: standard_name - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - type(ccpp_constituent_properties_t), pointer :: cprop - character(len=*), parameter :: subname = "ccp_model_const_index" + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, intent(out) :: errcode + character(len=*), intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_has_default' - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - cprop => this%find_const(standard_name, errcode=errcode, errmsg=errmsg) - if (associated(cprop)) then - ccp_model_const_index = cprop%const_index() - else - ccp_model_const_index = int_unassigned - end if + if (associated(this%prop)) then + call this%prop%has_default(val_out, errcode, errmsg) else - ccp_model_const_index = int_unassigned + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end function ccp_model_const_index + end subroutine ccpt_has_default !######################################################################## - integer function ccp_model_const_field_index(this, standard_name, & - errcode, errmsg) - ! Return index of field matching . - ! must be locked to execute this function + subroutine ccpt_set(this, const_ptr, errcode, errmsg) + ! Set the pointer to , however, an error is recorded if + ! the pointer is already set. ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - character(len=*), intent(in) :: standard_name - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg + class(ccpp_constituent_prop_ptr_t), intent(inout) :: this + type(ccpp_constituent_properties_t), pointer :: const_ptr + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg ! Local variables - type(ccpp_constituent_properties_t), pointer :: cprop - character(len=*), parameter :: subname = "ccp_model_field_index" - - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - cprop => this%find_const(standard_name, errcode=errcode, errmsg=errmsg) - if (associated(cprop)) then - ccp_model_const_field_index = cprop%field_index() - else - ccp_model_const_field_index = int_unassigned + character(len=stdname_len) :: stdname + character(len=errmsg_len) :: errmsg2 + + call initialize_errvars(errcode, errmsg) + if (associated(this%prop)) then + call this%standard_name(stdname, errcode=errcode, errmsg=errmsg2) + if (errcode == 0) then + write(errmsg2, *) "Pointer already allocated as '", & + trim(stdname), "'" end if + errcode = errcode + 1 + call set_errvars(1, "ccpt_set: ", errcode=errcode, errmsg=errmsg, & + errmsg2=trim(errmsg2)) else - ccp_model_const_field_index = int_unassigned + this%prop => const_ptr end if - end function ccp_model_const_field_index + end subroutine ccpt_set !######################################################################## - subroutine ccp_model_const_metadata(this, standard_name, const_data, & - errcode, errmsg) - ! Return metadata matching standard name - ! must be locked to execute this function + subroutine ccpt_deallocate(this) + ! Deallocate the constituent object pointer if it is allocated. + + ! Dummy argument + class(ccpp_constituent_prop_ptr_t), intent(inout) :: this + + if (associated(this%prop)) then + call this%prop%deallocate() + deallocate(this%prop) + end if + nullify(this%prop) + + end subroutine ccpt_deallocate + + !####################################################################### + + subroutine ccpt_set_const_index(this, index, errcode, errmsg) + ! Set this constituent's index in the master constituent array + ! It is an error to try to set an index if it is already set ! Dummy arguments - class(ccpp_model_constituents_t), intent(in) :: this - character(len=*), intent(in) :: standard_name - type(ccpp_constituent_properties_t), intent(out) :: const_data - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg - ! Local variables - type(ccpp_constituent_properties_t), pointer :: cprop - character(len=*), parameter :: subname = "ccp_model_const_metadata" + class(ccpp_constituent_prop_ptr_t), intent(inout) :: this + integer, intent(in) :: index + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_set_const_index' - if (this%locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - cprop => this%find_const(standard_name, errcode=errcode, errmsg=errmsg) - if (associated(cprop)) then - const_data = cprop + if (associated(this%prop)) then + if (this%prop%is_instantiated(errcode, errmsg)) then + if (this%prop%const_ind == int_unassigned) then + this%prop%const_ind = index + else + call set_errvars(1, "ccpp_constituent_prop_ptr_t ", & + errcode=errcode, errmsg=errmsg, & + errmsg2="const index is already set") + end if end if + else + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) end if - end subroutine ccp_model_const_metadata + end subroutine ccpt_set_const_index end module ccpp_constituent_prop_mod diff --git a/src/ccpp_constituent_prop_mod.meta b/src/ccpp_constituent_prop_mod.meta new file mode 100644 index 00000000..dd60eb13 --- /dev/null +++ b/src/ccpp_constituent_prop_mod.meta @@ -0,0 +1,47 @@ +######################################################################## +[ccpp-table-properties] + name = ccpp_constituent_prop_ptr_t + type = ddt + +[ccpp-arg-table] + name = ccpp_constituent_prop_ptr_t + type = ddt + +######################################################################## +[ccpp-table-properties] + name = ccpp_model_constituents_t + type = ddt + +[ccpp-arg-table] + name = ccpp_model_constituents_t + type = ddt +[ num_layer_vars ] + standard_name = ccpp_num_constituents + long_name = Number of constituents managed by CCPP Framework + units = count + dimensions = () + type = integer +[ num_advected_vars ] + standard_name = ccpp_num_advected_constituents + long_name = Number of advected constituents managed by CCPP Framework + units = count + dimensions = () + type = integer +[ vars_layer ] + standard_name = ccpp_constituent_array + long_name = Array of constituents managed by CCPP Framework + units = none + state_variable = true + dimensions = (horizontal_dimension, vertical_layer_dimension, ccpp_num_constituents) + type = real | kind = kind_phys +[ const_metadata ] + standard_name = ccpp_constituent_properties_array + units = None + type = ccpp_constituent_prop_ptr_t + dimensions = (ccpp_num_constituents) +[ vars_minvalue ] + standard_name = ccpp_constituent_array_minimum_values + units = kg kg-1 + type = real | kind = kind_phys + dimensions = (ccpp_num_constituents) + protected = True diff --git a/test/advection_test/test_host.F90 b/test/advection_test/test_host.F90 index 61bd8657..9c7abd42 100644 --- a/test/advection_test/test_host.F90 +++ b/test/advection_test/test_host.F90 @@ -1,6 +1,7 @@ module test_prog - use ccpp_kinds, only: kind_phys + use ccpp_kinds, only: kind_phys + use ccpp_constituent_prop_mod, only: ccpp_constituent_properties_t implicit none private @@ -22,14 +23,29 @@ module test_prog character(len=cm), pointer :: suite_required_vars(:) => NULL() end type suite_info + type(ccpp_constituent_properties_t), private, target :: host_constituents(1) + + private :: check_list private :: check_suite - private :: constituents_in ! Data from suites to dycore array - private :: constituents_out ! Data from dycore array to suires private :: advect_constituents ! Move data around + private :: check_errflg CONTAINS + subroutine check_errflg(subname, errflg, errmsg) + ! If errflg is not zero, print an error message + character(len=*), intent(in) :: subname + integer, intent(in) :: errflg + character(len=*), intent(in) :: errmsg + + if (errflg /= 0) then + write(6, '(a,i0,4a)') "Error ", errflg, " from ", trim(subname), & + ':', trim(errmsg) + end if + + end subroutine check_errflg + logical function check_list(test_list, chk_list, list_desc, suite_name) ! Check a list () against its expected value () @@ -120,7 +136,6 @@ logical function check_suite(test_suite) ! Dummy argument type(suite_info), intent(in) :: test_suite ! Local variables - integer :: sind logical :: check integer :: errflg character(len=512) :: errmsg @@ -185,54 +200,8 @@ logical function check_suite(test_suite) end if end function check_suite - logical function constituents_in(num_host_fields) result(okay) - ! Copy advected species from physics to 'dynamics' array - use test_host_mod, only: phys_state, ncnst, index_qv - use test_host_ccpp_cap, only: test_host_ccpp_gather_constituents - - ! Dummy argument - integer, intent(in) :: num_host_fields ! Packed at beginning of Q - ! Local variables - integer :: q_off - integer :: errflg - character(len=512) :: errmsg - - okay = .true. - q_off = num_host_fields + 1 - call test_host_ccpp_gather_constituents(phys_state%q(:,:,q_off:), & - errflg=errflg, errmsg=errmsg) - if (errflg /= 0) then - write(6, *) "ERROR: gather_constituents failed, '", trim(errmsg), "'" - okay = .false. - end if - - end function constituents_in - - logical function constituents_out(num_host_fields) result(okay) - ! Copy advected constituents back to physics - use test_host_mod, only: phys_state, ncnst, index_qv - use test_host_ccpp_cap, only: test_host_ccpp_update_constituents - - ! Dummy argument - integer, intent(in) :: num_host_fields ! Packed at beginning of Q - ! Local variables - integer :: q_off - integer :: errflg - character(len=512) :: errmsg - - okay = .true. - q_off = num_host_fields + 1 - call test_host_ccpp_update_constituents(phys_state%q(:,:,q_off:), & - errflg=errflg, errmsg=errmsg) - if (errflg /= 0) then - write(6, *) "ERROR: update_constituents failed, '", trim(errmsg), "'" - okay = .false. - end if - - end function constituents_out - subroutine advect_constituents() - use test_host_mod, only: phys_state, ncnst, index_qv, ncols, pver + use test_host_mod, only: phys_state, ncnst use test_host_mod, only: twist_array ! Local variables @@ -248,17 +217,22 @@ end subroutine advect_constituents !! subroutine test_host(retval, test_suites) - use test_host_mod, only: num_time_steps, num_host_advected + use ccpp_constituent_prop_mod, only: ccpp_constituent_prop_ptr_t + use test_host_mod, only: num_time_steps use test_host_mod, only: init_data, compare_data - use test_host_mod, only: ncols, pver, pverp + use test_host_mod, only: ncols, pver use test_host_ccpp_cap, only: test_host_ccpp_register_constituents + use test_host_ccpp_cap, only: test_host_ccpp_initialize_constituents use test_host_ccpp_cap, only: test_host_ccpp_number_constituents + use test_host_ccpp_cap, only: test_host_constituents_array use test_host_ccpp_cap, only: test_host_ccpp_physics_initialize use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_initial use test_host_ccpp_cap, only: test_host_ccpp_physics_run use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_final use test_host_ccpp_cap, only: test_host_ccpp_physics_finalize use test_host_ccpp_cap, only: ccpp_physics_suite_list + use test_host_ccpp_cap, only: test_host_const_get_index + use test_host_ccpp_cap, only: test_host_model_const_properties type(suite_info), intent(in) :: test_suites(:) logical, intent(out) :: retval @@ -266,12 +240,18 @@ subroutine test_host(retval, test_suites) logical :: check integer :: col_start, col_end integer :: index, sind + integer :: index_liq, index_ice integer :: time_step integer :: num_suites integer :: num_advected ! Num advected species + logical :: const_log character(len=128), allocatable :: suite_names(:) + character(len=256) :: const_str character(len=512) :: errmsg integer :: errflg + real(kind_phys), pointer :: const_ptr(:,:,:) + type(ccpp_constituent_prop_ptr_t), pointer :: const_props(:) + character(len=*), parameter :: subname = 'test_host' ! Gather and test the inspection routines num_suites = size(test_suites) @@ -301,36 +281,127 @@ subroutine test_host(retval, test_suites) end if ! Register the constituents to find out what needs advecting - call test_host_ccpp_register_constituents(suite_names(:), & - ncols, pver, pverp, errmsg=errmsg, errflg=errflg) - if (errflg /= 0) then - write(6, '(2a)') 'ERROR register_constituents: ', trim(errmsg) - end if - num_advected = test_host_ccpp_number_constituents(errmsg=errmsg, & - errflg=errflg) - if (num_advected /= 2) then - write(6, '(a,i0)') "ERROR: num advected constituents = ", num_advected - STOP 2 - end if - - ! Initialize our 'data' - call init_data(num_advected) + call host_constituents(1)%instantiate(std_name="specific_humidity", & + long_name="Specific humidity", units="kg kg-1", & + vertical_dim="vertical_layer_dimension", advected=.true., & + errcode=errflg, errmsg=errmsg) + call check_errflg(subname//'.initialize', errflg, errmsg) + if (errflg == 0) then + call test_host_ccpp_register_constituents(suite_names(:), & + host_constituents, errmsg=errmsg, errflg=errflg) + end if + if (errflg /= 0) then + write(6, '(2a)') 'ERROR register_constituents: ', trim(errmsg) + end if + ! Check number of advected constituents + if (errflg == 0) then + call test_host_ccpp_number_constituents(num_advected, errmsg=errmsg, & + errflg=errflg) + call check_errflg(subname//".num_advected", errflg, errmsg) + end if + if (num_advected /= 3) then + write(6, '(a,i0)') "ERROR: num advected constituents = ", num_advected + STOP 2 + end if + ! Initialize constituent data + call test_host_ccpp_initialize_constituents(ncols, pver, errflg, errmsg) + + ! Initialize our 'data' + if (errflg == 0) then + const_ptr => test_host_constituents_array() + call test_host_const_get_index('specific_humidity', index, & + errflg, errmsg) + call check_errflg(subname//".index_specific_humidity", errflg, errmsg) + end if + if (errflg == 0) then + call test_host_const_get_index('cloud_liquid_dry_mixing_ratio', & + index_liq, errflg, errmsg) + call check_errflg(subname//".index_cld_liq", errflg, errmsg) + end if + if (errflg == 0) then + call test_host_const_get_index('cloud_ice_dry_mixing_ratio', & + index_ice, errflg, errmsg) + call check_errflg(subname//".index_cld_ice", errflg, errmsg) + end if + call init_data(const_ptr, index, index_liq, index_ice) + ! Check some constituent properties + if (errflg == 0) then + const_props => test_host_model_const_properties() + call const_props(index)%standard_name(const_str, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get standard_name for specific_humidity, index = ", & + index, trim(errmsg) + end if + end if + if (errflg == 0) then + if (trim(const_str) /= 'specific_humidity') then + write(6, *) "ERROR: standard name, '", trim(const_str), & + "' should be 'specific_humidity'" + errflg = -1 + end if + end if + if (errflg == 0) then + call const_props(index_liq)%long_name(const_str, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get long_name for cld_liq index = ", & + index_liq, trim(errmsg) + end if + end if + if (errflg == 0) then + if (trim(const_str) /= 'Cloud liquid dry mixing ratio') then + write(6, *) "ERROR: long name, '", trim(const_str), & + "' should be 'Cloud liquid dry mixing ratio'" + errflg = -1 + end if + end if + if (errflg == 0) then + call const_props(index_ice)%is_mass_mixing_ratio(const_log, & + errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get mass mixing ratio prop for cld_ice index = ", & + index_ice, trim(errmsg) + end if + end if + if (errflg == 0) then + if (.not. const_log) then + write(6, *) "ERROR: cloud ice is not a mass mixing_ratio" + errflg = -1 + end if + end if + if (errflg == 0) then + call const_props(index_ice)%is_dry(const_log, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get dry prop for cld_ice index = ", index_ice, trim(errmsg) + end if + end if + if (errflg == 0) then + if (.not. const_log) then + write(6, *) "ERROR: cloud ice mass_mixing_ratio is not dry" + errflg = -1 + end if + end if ! Use the suite information to setup the run - do sind = 1, num_suites - call test_host_ccpp_physics_initialize(test_suites(sind)%suite_name,& - errmsg, errflg) - if (errflg /= 0) then - write(6, '(4a)') 'ERROR in initialize of ', & - trim(test_suites(sind)%suite_name), ': ', trim(errmsg) - exit - end if + do sind = 1, num_suites + if (errflg == 0) then + call test_host_ccpp_physics_initialize( & + test_suites(sind)%suite_name, errmsg, errflg) + if (errflg /= 0) then + write(6, '(4a)') 'ERROR in initialize of ', & + trim(test_suites(sind)%suite_name), ': ', trim(errmsg) + exit + end if + end if end do ! Loop over time steps do time_step = 1, num_time_steps ! Initialize the timestep do sind = 1, num_suites - if (retval) then + if (errflg == 0) then call test_host_ccpp_physics_timestep_initial( & test_suites(sind)%suite_name, errmsg, errflg) if (errflg /= 0) then @@ -348,15 +419,17 @@ subroutine test_host(retval, test_suites) do sind = 1, num_suites do index = 1, size(test_suites(sind)%suite_parts) - call test_host_ccpp_physics_run( & - test_suites(sind)%suite_name, & - test_suites(sind)%suite_parts(index), & - col_start, col_end, errmsg, errflg) - if (errflg /= 0) then - write(6, '(5a)') trim(test_suites(sind)%suite_name), & - '/', trim(test_suites(sind)%suite_parts(index)), & - ': ', trim(errmsg) - exit + if (errflg == 0) then + call test_host_ccpp_physics_run( & + test_suites(sind)%suite_name, & + test_suites(sind)%suite_parts(index), & + col_start, col_end, errmsg, errflg) + if (errflg /= 0) then + write(6, '(5a)') trim(test_suites(sind)%suite_name), & + '/', trim(test_suites(sind)%suite_parts(index)),& + ': ', trim(errmsg) + exit + end if end if end do end do @@ -370,16 +443,13 @@ subroutine test_host(retval, test_suites) if (errflg /= 0) then write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & trim(errmsg) + exit end if end do ! Run "dycore" if (errflg == 0) then - check = constituents_in(num_host_advected) - end if - if (check) then call advect_constituents() - check = constituents_out(num_host_advected) end if end do ! End time step loop @@ -392,13 +462,14 @@ subroutine test_host(retval, test_suites) trim(errmsg) write(6,'(2a)') 'An error occurred in ccpp_timestep_final, ', & 'Exiting...' + exit end if end if end do if (errflg == 0) then ! Run finished without error, check answers - if (compare_data(num_advected + num_host_advected)) then + if (compare_data(num_advected)) then write(6, *) 'Answers are correct!' errflg = 0 else @@ -418,39 +489,42 @@ program test implicit none - character(len=cs), target :: test_parts1(1) = (/ 'physics ' /) - character(len=cm), target :: test_invars1(7) = (/ & + character(len=cs), target :: test_parts1(1) + character(len=cm), target :: test_invars1(6) + character(len=cm), target :: test_outvars1(5) + character(len=cm), target :: test_reqvars1(8) + + type(suite_info) :: test_suites(1) + logical :: run_okay + + test_parts1 = (/ 'physics '/) + test_invars1 = (/ & 'cloud_ice_dry_mixing_ratio ', & - 'cloud_liquid_dry_mixing_ratio ', & 'surface_air_pressure ', & 'temperature ', & 'time_step_for_physics ', & 'water_temperature_at_freezing ', & 'water_vapor_specific_humidity ' /) - character(len=cm), target :: test_outvars1(6) = (/ & + test_outvars1 = (/ & 'ccpp_error_message ', & 'ccpp_error_code ', & 'temperature ', & 'water_vapor_specific_humidity ', & - 'cloud_liquid_dry_mixing_ratio ', & 'cloud_ice_dry_mixing_ratio ' /) - character(len=cm), target :: test_reqvars1(9) = (/ & + test_reqvars1 = (/ & 'surface_air_pressure ', & 'temperature ', & 'time_step_for_physics ', & - 'cloud_liquid_dry_mixing_ratio ', & 'cloud_ice_dry_mixing_ratio ', & 'water_temperature_at_freezing ', & 'water_vapor_specific_humidity ', & 'ccpp_error_message ', & 'ccpp_error_code ' /) - type(suite_info) :: test_suites(1) - logical :: run_okay ! Setup expected test suite info test_suites(1)%suite_name = 'cld_suite' - test_suites(1)%suite_parts => test_parts1 + test_suites(1)%suite_parts => test_parts1 test_suites(1)%suite_input_vars => test_invars1 test_suites(1)%suite_output_vars => test_outvars1 test_suites(1)%suite_required_vars => test_reqvars1 diff --git a/test/advection_test/test_host_data.F90 b/test/advection_test/test_host_data.F90 index 10183cd6..fce25c66 100644 --- a/test/advection_test/test_host_data.F90 +++ b/test/advection_test/test_host_data.F90 @@ -5,12 +5,9 @@ module test_host_data !> \section arg_table_physics_state Argument Table !! \htmlinclude arg_table_physics_state.html type physics_state - real(kind_phys), dimension(:), allocatable :: & - ps ! surface pressure - real(kind_phys), dimension(:,:), allocatable :: & - temp ! temperature - real(kind_phys), dimension(:,:,:),allocatable :: & - q ! constituent mixing ratio (kg/kg moist or dry air depending on type) + real(kind_phys), allocatable :: ps(:) ! surface pressure + real(kind_phys), allocatable :: temp(:,:) ! temperature + real(kind_phys), pointer :: q(:,:,:) => NULL() ! constituent array end type physics_state public allocate_physics_state @@ -20,7 +17,7 @@ module test_host_data subroutine allocate_physics_state(cols, levels, constituents, state) integer, intent(in) :: cols integer, intent(in) :: levels - integer, intent(in) :: constituents + real(kind_phys), pointer :: constituents(:,:,:) type(physics_state), intent(out) :: state if (allocated(state%ps)) then @@ -31,10 +28,12 @@ subroutine allocate_physics_state(cols, levels, constituents, state) deallocate(state%temp) end if allocate(state%temp(cols, levels)) - if (allocated(state%q)) then - deallocate(state%q) + if (associated(state%q)) then + ! Do not deallocate (we do not own this array) + nullify(state%q) end if - allocate(state%q(cols, levels, constituents)) + ! Point to the advected constituents array + state%q => constituents end subroutine allocate_physics_state diff --git a/test/advection_test/test_host_mod.F90 b/test/advection_test/test_host_mod.F90 index 3e4f60a5..560b7619 100644 --- a/test/advection_test/test_host_mod.F90 +++ b/test/advection_test/test_host_mod.F90 @@ -15,9 +15,8 @@ module test_host_mod integer, parameter :: ncols = 10 integer, parameter :: pver = 5 integer, parameter :: pverP = pver + 1 - integer, parameter :: num_host_advected = 1 integer, protected :: ncnst = -1 - integer, parameter :: index_qv = 1 + integer, protected :: index_qv = -1 real(kind_phys) :: dt real(kind_phys), parameter :: tfreeze = 273.15_kind_phys type(physics_state) :: phys_state @@ -30,24 +29,35 @@ module test_host_mod real(kind_phys), private, allocatable :: check_vals(:,:,:) real(kind_phys), private :: check_temp(ncols, pver) + integer, private :: ind_liq = -1 + integer, private :: ind_ice = -1 contains - subroutine init_data(num_advected) + subroutine init_data(constituent_array, index_qv_use, index_liq, index_ice) - integer, intent(in) :: num_advected ! From suites + ! Dummy arguments + real(kind_phys), pointer :: constituent_array(:,:,:) ! From host & suites + integer, intent(in) :: index_qv_use + integer, intent(in) :: index_liq + integer, intent(in) :: index_ice - integer :: col - integer :: lev - integer :: cind - integer :: itime - real(kind_phys) :: qmax + ! Local variables + integer :: col + integer :: lev + integer :: cind + integer :: itime + real(kind_phys) :: qmax + real(kind_phys), parameter :: inc = 0.1_kind_phys ! Allocate and initialize state ! Temperature starts above freezing and decreases to -30C ! water vapor is initialized in odd columns to different amounts - ncnst = num_advected + num_host_advected - call allocate_physics_state(ncols, pver, ncnst, phys_state) + ncnst = SIZE(constituent_array, 3) + call allocate_physics_state(ncols, pver, constituent_array, phys_state) + index_qv = index_qv_use + ind_liq = index_liq + ind_ice = index_ice allocate(check_vals(ncols, pver, ncnst)) check_vals(:,:,:) = 0.0_kind_phys do lev = 1, pver @@ -66,8 +76,8 @@ subroutine init_data(num_advected) ! Do timestep 1 do col = 1, ncols, 2 check_temp(col, 1) = check_temp(col, 1) + 0.5_kind_phys - check_vals(col, 1, 1) = check_vals(col, 1, 1) - 0.1_kind_phys - check_vals(col, 1, 3) = check_vals(col, 1, 3) + 0.1_kind_phys + check_vals(col, 1, index_qv) = check_vals(col, 1, index_qv) - inc + check_vals(col, 1, ind_liq) = check_vals(col, 1, ind_liq) + inc end do do itime = 1, num_time_steps do cind = 1, ncnst @@ -82,7 +92,6 @@ subroutine twist_array(array) real(kind_phys), intent(inout) :: array(:,:) ! Local variables - integer :: q_ind ! Constituent index integer :: icol, ilev ! Field coordinates integer :: idir ! 'w' sign integer :: levb, leve ! Starting and ending level indices @@ -111,7 +120,6 @@ logical function compare_data(ncnst) integer :: col integer :: lev integer :: cind - integer :: nind logical :: need_header real(kind_phys) :: check real(kind_phys) :: denom From 6a6cc0d6d0d45b726488dfb461bcce78cd672c0a Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 21 Aug 2023 13:34:17 -0600 Subject: [PATCH 06/49] initial stab at cleanup-only PR --- doc/HelloWorld/CMakeLists.txt | 5 +- scripts/ccpp_capgen.py | 14 +- scripts/ccpp_database_obj.py | 87 +++++++++ scripts/ccpp_datafile.py | 14 +- scripts/ccpp_state_machine.py | 10 +- scripts/ccpp_suite.py | 34 ++-- scripts/common.py | 9 +- scripts/ddt_library.py | 104 +++++----- scripts/file_utils.py | 13 +- scripts/fortran_tools/fortran_write.py | 200 ++++++++++++++++---- scripts/fortran_tools/parse_fortran.py | 10 +- scripts/fortran_tools/parse_fortran_file.py | 52 +++-- scripts/framework_env.py | 4 +- scripts/metadata_table.py | 38 ++-- scripts/metavar.py | 22 +-- scripts/parse_tools/__init__.py | 3 +- scripts/parse_tools/parse_checkers.py | 2 +- scripts/parse_tools/parse_object.py | 8 +- scripts/parse_tools/parse_source.py | 83 ++++---- scripts/parse_tools/xml_tools.py | 158 +++++++--------- scripts/state_machine.py | 7 +- scripts/suite_objects.py | 8 +- 22 files changed, 541 insertions(+), 344 deletions(-) create mode 100644 scripts/ccpp_database_obj.py diff --git a/doc/HelloWorld/CMakeLists.txt b/doc/HelloWorld/CMakeLists.txt index 4b0cb208..7b480203 100644 --- a/doc/HelloWorld/CMakeLists.txt +++ b/doc/HelloWorld/CMakeLists.txt @@ -1,4 +1,4 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +CMAKE_MINIMUM_REQUIRED(VERSION 3.10) PROJECT(HelloWorld) ENABLE_LANGUAGE(Fortran) @@ -166,7 +166,8 @@ list(APPEND DTABLE_CMD "${CCPP_CAP_FILES}/datatable.xml") list(APPEND DTABLE_CMD "--ccpp-files") list(APPEND DTABLE_CMD "--separator=\\;") string(REPLACE ";" " " DTABLE_STRING "${DTABLE_CMD}") -MESSAGE(STATUS "Running: ${DTABLE_STRING}") +string(STRIP ${DTABLE_STRING} DTABLE_STRING) +MESSAGE(STATUS "Running: ${DTABLE_STRING};") EXECUTE_PROCESS(COMMAND ${DTABLE_CMD} OUTPUT_VARIABLE CCPP_CAPS RESULT_VARIABLE RES OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 7a12729a..d28bfd6a 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -15,6 +15,7 @@ import logging import re # CCPP framework imports +from ccpp_database_obj import CCPPDatabaseObj from ccpp_datafile import generate_ccpp_datatable from ccpp_suite import API from file_utils import check_for_writeable_file, remove_dir, replace_paths @@ -30,7 +31,7 @@ ## Capture the Framework root __SCRIPT_PATH = os.path.dirname(__file__) -__FRAMEWORK_ROOT = os.path.abspath(os.path.join(__SCRIPT_PATH, os.pardir)) +__FRAMEWORK_ROOT = os.path.abspath(os.path.join(_SCRIPT_PATH, os.pardir)) ## Init this now so that all Exceptions can be trapped _LOGGER = init_log(os.path.basename(__file__)) @@ -559,7 +560,7 @@ def clean_capgen(cap_output_file, logger): set_log_level(logger, log_level) ############################################################################### -def capgen(run_env): +def capgen(run_env, return_db=False): ############################################################################### """Parse indicated host, scheme, and suite files. Generate code to allow host model to run indicated CCPP suites.""" @@ -628,7 +629,8 @@ def capgen(run_env): cap_filenames = ccpp_api.write(outtemp_dir, run_env) if run_env.generate_host_cap: # Create a cap file - host_files = [write_host_cap(host_model, ccpp_api, + cap_module = host_model.ccpp_cap_name() + host_files = [write_host_cap(host_model, ccpp_api, cap_module, outtemp_dir, run_env)] else: host_files = list() @@ -650,6 +652,10 @@ def capgen(run_env): generate_ccpp_datatable(run_env, host_model, ccpp_api, scheme_headers, scheme_tdict, host_files, cap_filenames, kinds_file, src_dir) + if return_db: + return CCPPDatabaseObj(run_env, host_model=host_model, api=ccpp_api) + # end if + return None ############################################################################### def _main_func(): @@ -665,7 +671,7 @@ def _main_func(): if framework_env.clean: clean_capgen(framework_env.datatable_file, framework_env.logger) else: - capgen(framework_env) + _ = capgen(framework_env) # end if (clean) ############################################################################### diff --git a/scripts/ccpp_database_obj.py b/scripts/ccpp_database_obj.py new file mode 100644 index 00000000..31aa450c --- /dev/null +++ b/scripts/ccpp_database_obj.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +""" +Define the CCPPDatabaseObj object +Object definition and methods to provide information from a run of capgen. +""" + +from host_model import HostModel +from ccpp_suite import API + +class CCPPDatabaseObjError(ValueError): + """Error class specific to CCPPDatabaseObj. + All uses of this error should be internal (i.e., programmer error, + not user error).""" + + def __init__(self, message): + """Initialize this exception""" + super().__init__(message) + +class CCPPDatabaseObj: + """Ojbect with data and methods to provide information from a run of capgen. + """ + + def __init__(self, run_env, host_model=None, api=None, database_file=None): + """Initialize this CCPPDatabaseObj. + If is not None, all other inputs MUST be None and + the object is created from the database table created by capgen. + To initialize the object from an in-memory capgen run, ALL other + inputs MUST be passed (i.e., not None) and it is an error to pass + a value for . + """ + + runtime_obj = all([host_model is not None, api is not None]) + self.__host_model = None + self.__api = None + self.__database_file = None + if runtime_obj and database_file: + emsg = "Cannot provide both runtime arguments and database_file." + elif (not runtime_obj) and (not database_file): + emsg = "Must provide either database_file or all runtime arguments." + else: + emsg = "" + # end if + if emsg: + raise CCPPDatabaseObjError(f"ERROR: {emsg}") + # end if + if runtime_obj: + self.__host_model = host_model + self.__api = api + else: + self.db_from_file(run_env, database_file) + # end if + + def db_from_file(self, run_env, database_file): + """Create the necessary internal data structures from a CCPP + datatable.xml file created by capgen. + """ + metadata_tables = {} + host_name = "cam" + self.__host_model = HostModel(metadata_tables, host_name, run_env) + self.__api = API(sdfs, host_model, scheme_headers, run_env) + raise CCPPDatabaseObjError("ERROR: not supported") + + def host_model_dict(self): + """Return the host model dictionary for this CCPP DB object""" + if self.__host_model is not None: + return self.__host_model + # end if + raise CCPPDatabaseObjError("ERROR: not supported") + + def suite_list(self): + """Return a list of suites built into the API""" + if self.__api is not None: + return list(self.__api.suites) + # end if + raise CCPPDatabaseObjError("ERROR: not supported") + + def constituent_dictionary(self, suite): + """Return the constituent dictionary for """ + return suite.constituent_dictionary() + + def call_list(self, phase): + """Return the API call list for """ + if self.__api is not None: + return self.__api.call_list(phase) + # end if + raise CCPPDatabaseObjError("ERROR: not supported") diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 158d9dec..562a8f4b 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -27,10 +27,6 @@ from parse_tools import read_xml_file, PrettyElementTree from suite_objects import VerticalLoop, Subcycle -# Find python version -PY3 = sys.version_info[0] > 2 -PYSUBVER = sys.version_info[1] - # Global data _INDENT_STR = " " @@ -652,7 +648,7 @@ def _new_var_entry(parent, var, full_entry=True): """Create a variable sub-element of with information from . If is False, only include standard name and intent. """ - prop_list = ["intent"] + prop_list = ["intent", "local_name"] if full_entry: prop_list.extend(["allocatable", "active", "default_value", "diagnostic_name", "diagnostic_name_fixed", @@ -671,9 +667,13 @@ def _new_var_entry(parent, var, full_entry=True): if full_entry: dims = var.get_dimensions() if dims: - dim_entry = ET.SubElement(ventry, "dimensions") - dim_entry.text = " ".join(dims) + v_entry = ET.SubElement(ventry, "dimensions") + v_entry.text = " ".join(dims) # end if + v_entry = ET.SubElement(ventry, "source_type") + v_entry.text = var.source.ptype.lower() + v_entry = ET.SubElement(ventry, "source_name") + v_entry.text = var.source.name.lower() # end if ############################################################################### diff --git a/scripts/ccpp_state_machine.py b/scripts/ccpp_state_machine.py index 30540fc9..0de2c7bd 100644 --- a/scripts/ccpp_state_machine.py +++ b/scripts/ccpp_state_machine.py @@ -3,11 +3,11 @@ # CCPP framework imports from state_machine import StateMachine -_INIT_ST = r"(?:(?i)init(?:ial(?:ize)?)?)" -_FINAL_ST = r"(?:(?i)final(?:ize)?)" -_RUN_ST = r"(?:(?i)run)" -_TS_INIT_ST = r"(?:(?i)timestep_init(?:ial(?:ize)?)?)" -_TS_FINAL_ST = r"(?:(?i)timestep_final(?:ize)?)" +_INIT_ST = r"(?:init(?:ial(?:ize)?)?)" +_FINAL_ST = r"(?:final(?:ize)?)" +_RUN_ST = r"(?:run)" +_TS_INIT_ST = r"(?:timestep_init(?:ial(?:ize)?)?)" +_TS_FINAL_ST = r"(?:timestep_final(?:ize)?)" # Allowed CCPP transitions # pylint: disable=bad-whitespace diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 8b9a1aeb..ba8f8609 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -17,8 +17,7 @@ from fortran_tools import FortranWriter from framework_env import CCPPFrameworkEnv from metavar import Var, VarDictionary, ccpp_standard_var -from metavar import CCPP_CONSTANT_VARS, CCPP_LOOP_VAR_STDNAMES -from parse_tools import ParseContext, ParseSource, context_string +from parse_tools import ParseContext, ParseSource from parse_tools import ParseInternalError, CCPPError from parse_tools import read_xml_file, validate_xml_file, find_schema_version from parse_tools import init_log, set_log_to_null @@ -31,12 +30,12 @@ ############################################################################### # Source for internally generated variables. -_API_SOURCE_NAME = "CCPP_API" +API_SOURCE_NAME = "CCPP_API" # Use the constituent source type for consistency _API_SUITE_VAR_NAME = ConstituentVarDict.constitutent_source_type() _API_SCHEME_VAR_NAME = "scheme" _API_CONTEXT = ParseContext(filename="ccpp_suite.py") -_API_SOURCE = ParseSource(_API_SOURCE_NAME, _API_SCHEME_VAR_NAME, _API_CONTEXT) +_API_SOURCE = ParseSource(API_SOURCE_NAME, _API_SCHEME_VAR_NAME, _API_CONTEXT) _API_LOGGING = init_log('ccpp_suite') set_log_to_null(_API_LOGGING) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(_API_LOGGING, @@ -299,7 +298,7 @@ def find_variable(self, standard_name=None, source_var=None, # Remove this entry to avoid looping back here del self.__gvar_stdnames[standard_name] # Let everyone know this is now a Suite variable - var.source = ParseSource(_API_SOURCE_NAME, + var.source = ParseSource(API_SOURCE_NAME, _API_SUITE_VAR_NAME, var.context) self.add_variable(var, self.__run_env) @@ -730,9 +729,9 @@ def write_var_set_loop(ofile, varlist_name, var_list, indent, def write_suite_part_list_sub(self, ofile, errmsg_name, errcode_name): """Write the suite-part list subroutine""" - oline = "suite_name, part_list, {errmsg}, {errcode}" - inargs = oline.format(errmsg=errmsg_name, errcode=errcode_name) - ofile.write("\nsubroutine {}({})".format(API.__part_fname, inargs), 1) + ofile.blank_line() + inargs = f"suite_name, part_list, {errmsg_name}, {errcode_name}" + ofile.write(f"subroutine {API.__part_fname}({inargs})", 1) oline = "character(len=*), intent(in) :: suite_name" ofile.write(oline, 2) oline = "character(len=*), allocatable, intent(out) :: part_list(:)" @@ -741,9 +740,9 @@ def write_suite_part_list_sub(self, ofile, errmsg_name, errcode_name): self._errcode_var.write_def(ofile, 2, self) else_str = '' ename = self._errcode_var.get_prop_value('local_name') - ofile.write("{} = 0".format(ename), 2) + ofile.write(f"{ename} = ''", 2) ename = self._errmsg_var.get_prop_value('local_name') - ofile.write("{} = ''".format(ename), 2) + ofile.write(f"{ename} = ''", 2) for suite in self.suites: oline = "{}if(trim(suite_name) == '{}') then" ofile.write(oline.format(else_str, suite.name), 2) @@ -751,12 +750,12 @@ def write_suite_part_list_sub(self, ofile, errmsg_name, errcode_name): else_str = 'else ' # end for ofile.write("else", 2) - emsg = "write({errmsg}, '(3a)')".format(errmsg=errmsg_name) + emsg = f"write({errmsg_name}, '(3a)')" emsg += "'No suite named ', trim(suite_name), ' found'" ofile.write(emsg, 3) - ofile.write("{errcode} = 1".format(errcode=errcode_name), 3) + ofile.write(f"{errcode_name} = 1", 3) ofile.write("end if", 2) - ofile.write("end subroutine {}".format(API.__part_fname), 1) + ofile.write(f"end subroutine {API.__part_fname}", 1) def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): """Write the required variables subroutine""" @@ -807,9 +806,9 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): parent = suite.parent # Collect all the suite variables oline = "{}if(trim(suite_name) == '{}') then" - input_vars = [set(), set(), set()] # leaves, arrrays, leaf elements - inout_vars = [set(), set(), set()] # leaves, arrrays, leaf elements - output_vars = [set(), set(), set()] # leaves, arrrays, leaf elements + input_vars = [set(), set(), set()] # leaves, arrays, leaf elements + inout_vars = [set(), set(), set()] # leaves, arrays, leaf elements + output_vars = [set(), set(), set()] # leaves, arrays, leaf elements for part in suite.groups: for var in part.call_list.variable_list(): stdname = var.get_prop_value("standard_name") @@ -821,7 +820,8 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): protected = pvar.get_prop_value("protected") # end if # end if - elements = var.intrinsic_elements(check_dict=self.parent) + elements = var.intrinsic_elements(check_dict=self.parent, + ddt_lib=self.__ddt_lib) if (intent == 'in') and (not protected): if isinstance(elements, list): input_vars[1].add(stdname) diff --git a/scripts/common.py b/scripts/common.py index 0ac2c74d..7f4e2f43 100755 --- a/scripts/common.py +++ b/scripts/common.py @@ -165,14 +165,7 @@ def escape_tex(text): def isstring(s): """Return true if a variable is a string""" - # We use Python 3 - if (sys.version_info.major == 3): - return isinstance(s, str) - # We use Python 2 - elif (sys.version_info.major == 2): - return isinstance(s, basestring) - else: - raise Exception('Unknown Python version') + return isinstance(s, str) def string_to_python_identifier(string): """Replaces forbidden characters in strings with standard substitutions diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 30614226..4be53963 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -44,11 +44,11 @@ def __init__(self, new_field, var_ref, run_env, recur=False): else: # Recurse to find correct (tail) location for self.__field = VarDDT(new_field, var_ref.field, run_env, recur=True) - # End if + # end if if ((not recur) and run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG)): run_env.logger.debug('Adding DDT field, {}'.format(self)) - # End if + # end if def is_ddt(self): """Return True iff is a DDT type.""" @@ -66,18 +66,18 @@ def get_prop_value(self, name): pvalue = super().get_prop_value(name) else: pvalue = self.field.get_prop_value(name) - # End if + # end if return pvalue def intrinsic_elements(self, check_dict=None): """Return the Var intrinsic elements for the leaf Var object. - See Var.intrinsic_elem for details + See Var.intrinsic_elements for details """ if self.field is None: pvalue = super().intrinsic_elements(check_dict=check_dict) else: pvalue = self.field.intrinsic_elements(check_dict=check_dict) - # End if + # end if return pvalue def clone(self, subst_dict, source_name=None, source_type=None, @@ -98,7 +98,7 @@ def clone(self, subst_dict, source_name=None, source_type=None, source_name=source_name, source_type=source_type, context=context) - # End if + # end if return clone_var def call_string(self, var_dict, loop_vars=None): @@ -109,7 +109,7 @@ def call_string(self, var_dict, loop_vars=None): if self.field is not None: call_str += '%' + self.field.call_string(var_dict, loop_vars=loop_vars) - # End if + # end if return call_str def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): @@ -122,7 +122,7 @@ def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): else: self.field.write_def(outfile, indent, ddict, allocatable=allocatable, dummy=dummy) - # End if + # end if @staticmethod def __var_rep(var, prefix=""): @@ -137,14 +137,14 @@ def __var_rep(var, prefix=""): lstr = '{}%{}({})'.format(prefix, lname, ', '.join(ldims)) else: lstr = '{}({})'.format(lname, ', '.join(ldims)) - # End if + # end if else: if prefix: lstr = '{}%{}'.format(prefix, lname) else: lstr = '{}'.format(lname) - # End if - # End if + # end if + # end if return lstr def __repr__(self): @@ -160,9 +160,9 @@ def __repr__(self): elif isinstance(field, Var): lstr = self.__var_rep(field, prefix=lstr) field = None - # End if + # end if sep = '%' - # End while + # end while return "".format(lstr) def __str__(self): @@ -201,33 +201,33 @@ def __init__(self, name, run_env, ddts=None, logger=None): ddts = list() elif not isinstance(ddts, list): ddts = [ddts] - # End if + # end if # Add all the DDT headers, then process for ddt in ddts: if not isinstance(ddt, MetadataSection): errmsg = 'Invalid DDT metadata type, {}' - raise ParseInternalError(errmsg.format(type(ddt))) - # End if + raise ParseInternalError(errmsg.format(type(ddt).__name__)) + # end if if not ddt.header_type == 'ddt': errmsg = 'Metadata table header is for a {}, should be DDT' raise ParseInternalError(errmsg.format(ddt.header_type)) - # End if + # end if if ddt.title in self: errmsg = "Duplicate DDT, {}, found{}, original{}" ctx = context_string(ddt.source.context) octx = context_string(self[ddt.title].source.context) raise CCPPError(errmsg.format(ddt.title, ctx, octx)) - # End if - if logger is not None: - lmsg = 'Adding DDT {} to {}' - logger.debug(lmsg.format(ddt.title, self.name)) - # End if + # end if + if logger and logger.isEnabledFor(logging.DEBUG): + lmsg = f"Adding DDT {ddt.title} to {self.name}" + logger.debug(lmsg) + # end if self[ddt.title] = ddt dlen = len(ddt.module) if dlen > self.__max_mod_name_len: self.__max_mod_name_len = dlen - # End if - # End for + # end if + # end for def check_ddt_type(self, var, header, lname=None): """If is a DDT, check to make sure it is in this DDT library. @@ -239,16 +239,21 @@ def check_ddt_type(self, var, header, lname=None): if vtype not in self: if lname is None: lname = var.get_prop_value('local_name') - # End if + # end if errmsg = 'Variable {} is of unknown type ({}) in {}' ctx = context_string(var.context) raise CCPPError(errmsg.format(lname, vtype, header.title, ctx)) - # End if - # End if (no else needed) + # end if + # end if (no else needed) - def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): + def collect_ddt_fields(self, var_dict, var, run_env, + ddt=None, skip_duplicates=False): """Add all the reachable fields from DDT variable of type, to . Each field is added as a VarDDT. + Note: By default, it is an error to try to add a duplicate + field to (i.e., the field already exists in + or one of its parents). To simply skip duplicate + fields, set to True. """ if ddt is None: vtype = var.get_prop_value('type') @@ -259,8 +264,8 @@ def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): ctx = context_string(var.context) errmsg = "Variable, {}, is not a known DDT{}" raise ParseInternalError(errmsg.format(lname, ctx)) - # End if - # End if + # end if + # end if for dvar in ddt.variable_list(): subvar = VarDDT(dvar, var, self.run_env) dvtype = dvar.get_prop_value('type') @@ -268,22 +273,25 @@ def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): # If DDT in our library, we need to add sub-fields recursively. subddt = self[dvtype] self.collect_ddt_fields(var_dict, subvar, run_env, ddt=subddt) - else: - # add_variable only checks the current dictionary. For a - # DDT, the variable also cannot be in our parent dictionaries. - stdname = dvar.get_prop_value('standard_name') - pvar = var_dict.find_variable(standard_name=stdname, + # end if + # add_variable only checks the current dictionary. By default, + # for a DDT, the variable also cannot be in our parent + # dictionaries. + stdname = dvar.get_prop_value('standard_name') + pvar = var_dict.find_variable(standard_name=stdname, any_scope=True) - if pvar: - emsg = "Attempt to add duplicate DDT sub-variable, {}{}." - emsg += "\nVariable originally defined{}" - ntx = context_string(dvar.context) - ctx = context_string(pvar.context) - raise CCPPError(emsg.format(stdname, ntx, ctx)) - # end if - # Add this intrinsic to + if pvar and (not skip_duplicates): + emsg = "Attempt to add duplicate DDT sub-variable, {}{}." + emsg += "\nVariable originally defined{}" + ntx = context_string(dvar.context) + ctx = context_string(pvar.context) + raise CCPPError(emsg.format(stdname, ntx, ctx)) + # end if + # Add this intrinsic to + if not pvar: var_dict.add_variable(subvar, run_env) - # End for + # end if + # end for def ddt_modules(self, variable_list, ddt_mods=None): """Collect information for module use statements. @@ -292,14 +300,14 @@ def ddt_modules(self, variable_list, ddt_mods=None): """ if ddt_mods is None: ddt_mods = set() # Need a new set for every call - # End if + # end if for var in variable_list: vtype = var.get_prop_value('type') if vtype in self: module = self[vtype].module ddt_mods.add((module, vtype)) - # End if - # End for + # end if + # end for return ddt_mods def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): @@ -313,7 +321,7 @@ def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): slen = ' '*(pad - len(dmod)) ustring = 'use {},{} only: {}' outfile.write(ustring.format(dmod, slen, dtype), indent) - # End for + # end for @property def name(self): diff --git a/scripts/file_utils.py b/scripts/file_utils.py index 8cdfd023..669922b9 100644 --- a/scripts/file_utils.py +++ b/scripts/file_utils.py @@ -13,11 +13,6 @@ import os # CCPP framework imports from parse_tools import CCPPError, ParseInternalError -#XXgoldyXX: v Crap required to support python 2 -import sys -# Find python version -PY3 = sys.version_info[0] > 2 -#XXgoldyXX: ^ Crap required to support python 2 # Standardize name of generated kinds file and module KINDS_MODULE = 'ccpp_kinds' @@ -300,13 +295,7 @@ def move_modified_files(src_dir, dest_dir, overwrite=False, remove_src=False): fmove = True # end if if fmove: -#XXgoldyXX: v Crap required to support python 2 - if PY3: - os.replace(src_path, dest_path) - else: - os.rename(src_path, dest_path) - # end if -#XXgoldyXX: ^ Crap required to support python 2 + os.replace(src_path, dest_path) else: os.remove(src_path) # end if diff --git a/scripts/fortran_tools/fortran_write.py b/scripts/fortran_tools/fortran_write.py index a238dcc5..f9bcfa3f 100644 --- a/scripts/fortran_tools/fortran_write.py +++ b/scripts/fortran_tools/fortran_write.py @@ -4,7 +4,9 @@ """Code to write Fortran code """ -class FortranWriter(object): +import math + +class FortranWriter: """Class to turn output into properly continued and indented Fortran code >>> FortranWriter("foo.F90", 'r', 'test', 'mod_name') #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): @@ -59,9 +61,9 @@ class FortranWriter(object): def indent(self, level=0, continue_line=False): 'Return an indent string for any level' - indent = self._indent * level + indent = self.indent_size * level if continue_line: - indent = indent + self._continue_indent + indent = indent + self.__continue_indent # End if return indent*' ' @@ -71,22 +73,55 @@ def find_best_break(self, choices, last=None): """Find the best line break point given . If is present, use it as a target line length.""" if last is None: - last = self._line_fill + last = self.__line_fill # End if # Find largest good break - possible = [x for x in choices if x < last] + possible = [x for x in choices if 0 < x < last] if not possible: - best = self._line_max + 1 + best = self.__line_max + 1 else: best = max(possible) # End if - if (best > self._line_max) and (last < self._line_max): - best = self.find_best_break(choices, last=self._line_max) + if (best > self.__line_max) and (last < self.__line_max): + best = self.find_best_break(choices, last=self.__line_max) # End if return best ########################################################################### + @staticmethod + def _in_quote(test_str): + """Return True if ends in a character context. + >>> FortranWriter._in_quote("hi'mom") + True + >>> FortranWriter._in_quote("hi mom") + False + >>> FortranWriter._in_quote("'hi mom'") + False + >>> FortranWriter._in_quote("'hi"" mom'") + False + """ + in_single_char = False + in_double_char = False + for char in test_str: + if in_single_char: + if char == "'": + in_single_char = False + # end if + elif in_double_char: + if char == '"': + in_double_char = False + # end if + elif char == "'": + in_single_char = True + elif char == '"': + in_double_char = True + # end if + # end for + return in_single_char or in_double_char + + ########################################################################### + def write(self, statement, indent_level, continue_line=False): """Write to the open file, indenting to (see self.indent). @@ -102,9 +137,18 @@ def write(self, statement, indent_level, continue_line=False): # End for else: istr = self.indent(indent_level, continue_line) - outstr = istr + statement.strip() + ostmt = statement.strip() + is_comment_stmt = ostmt and (ostmt[0] == '!') + in_comment = "" + if ostmt and (ostmt[0] != '&'): + # Skip indent for continue that is in the middle of a + # token or a quoted region + outstr = istr + ostmt + else: + outstr = ostmt + # end if line_len = len(outstr) - if line_len > self._line_fill: + if line_len > self.__line_fill: # Collect pretty break points spaces = list() commas = list() @@ -125,9 +169,14 @@ def write(self, statement, indent_level, continue_line=False): elif outstr[sptr] == '"': in_double_char = True elif outstr[sptr] == '!': - # Comment in non-character context, suck in rest of line + # Comment in non-character context spaces.append(sptr-1) - sptr = line_len - 1 + in_comment = "! " # No continue for comment + if ((not is_comment_stmt) and + (sptr >= self.__max_comment_start)): + # suck in rest of line + sptr = line_len - 1 + # end if elif outstr[sptr] == ' ': # Non-quote spaces are where we can break spaces.append(sptr) @@ -140,31 +189,62 @@ def write(self, statement, indent_level, continue_line=False): # End if (no else, other characters will be ignored) sptr = sptr + 1 # End while + # Before looking for best space, reject any that are on a + # comment line but before any significant characters + if outstr.lstrip()[0] == '!': + first_space = outstr.index('!') + 1 + while ((outstr[first_space] == '!' or + outstr[first_space] == ' ') and + (first_space < line_len)): + first_space += 1 + # end while + if min(spaces) < first_space: + spaces = [x for x in spaces if x >= first_space] + # end if best = self.find_best_break(spaces) - if best >= self._line_fill: - best = self.find_best_break(commas) + if best >= self.__line_fill: + best = min(best, self.find_best_break(commas)) # End if - if best > self._line_max: - # This is probably a bad situation that might not - # compile, just write the line and hope for the best. - line_continue = False - elif len(outstr) > best: - # If next line is just comment, do not use continue - # NB: Is this a Fortran issue or just a gfortran issue? - line_continue = outstr[best+1:].lstrip()[0] != '!' - else: - line_continue = True + line_continue = False + if best >= self.__line_max: + # This is probably a bad situation so we have to break + # in an ugly spot + best = self.__line_max - 1 + if len(outstr) > best: + line_continue = '&' + # end if + # end if + if len(outstr) > best: + if self._in_quote(outstr[0:best+1]): + line_continue = '&' + else: + # If next line is just comment, do not use continue + line_continue = outstr[best+1:].lstrip()[0] != '!' + # end if + elif not line_continue: + line_continue = len(outstr) > best # End if + if in_comment or is_comment_stmt: + line_continue = False + # end if if line_continue: - fill = "{}&".format((self._line_fill - best)*' ') + fill = "{}&".format((self.__line_fill - best)*' ') else: - fill = '' + fill = "" # End if - self._file.write("{}{}\n".format(outstr[0:best+1], fill)) - statement = outstr[best+1:] + outline = f"{outstr[0:best+1]}{fill}".rstrip() + self.__file.write(f"{outline}\n") + if best <= 0: + imsg = "Internal ERROR: Unable to break line" + raise ValueError(f"{imsg}, '{statement}'") + # end if + statement = in_comment + outstr[best+1:] + if isinstance(line_continue, str) and statement: + statement = line_continue + statement + # end if self.write(statement, indent_level, continue_line=line_continue) else: - self._file.write("{}\n".format(outstr)) + self.__file.write("{}\n".format(outstr)) # End if # End if @@ -175,7 +255,7 @@ def __init__(self, filename, mode, file_description, module_name, line_fill=None, line_max=None): """Initialize thie FortranWriter object. Some boilerplate is written automatically.""" - self.__file_desc = file_description + self.__file_desc = file_description.replace('\n', '\n!! ') self.__module = module_name # We only handle writing situations (for now) and only text if 'r' in mode: @@ -184,26 +264,27 @@ def __init__(self, filename, mode, file_description, module_name, if 'b' in mode: raise ValueError('Binary mode not allowed in FortranWriter object') # End if - self._file = open(filename, mode) + self.__file = open(filename, mode) if indent is None: - self._indent = FortranWriter.__INDENT + self.__indent = FortranWriter.__INDENT else: - self._indent = indent + self.__indent = indent # End if if continue_indent is None: - self._continue_indent = FortranWriter.__CONTINUE_INDENT + self.__continue_indent = FortranWriter.__CONTINUE_INDENT else: - self._continue_indent = continue_indent + self.__continue_indent = continue_indent # End if if line_fill is None: - self._line_fill = FortranWriter.__LINE_FILL + self.__line_fill = FortranWriter.__LINE_FILL else: - self._line_fill = line_fill + self.__line_fill = line_fill # End if + self.__max_comment_start = math.ceil(self.__line_fill * 3 / 4) if line_max is None: - self._line_max = FortranWriter.__LINE_MAX + self.__line_max = FortranWriter.__LINE_MAX else: - self._line_max = line_max + self.__line_max = line_max # End if ########################################################################### @@ -234,7 +315,7 @@ def __enter__(self, *args): def __exit__(self, *args): self.write(FortranWriter.__MOD_FOOTER.format(module=self.__module), 0) - self._file.close() + self.__file.close() return False ########################################################################### @@ -247,6 +328,45 @@ def module_header(self): ########################################################################### + def comment(self, comment, indent): + """Write a Fortran comment with contents, """ + mlcomment = comment.replace('\n', '\n! ') # No backslash in f string + self.write(f"! {mlcomment}", indent) + + ########################################################################### + + def blank_line(self): + """Write a blank line""" + self.write("", 0) + + ########################################################################### + + def include(self, filename): + """Insert the contents of verbatim.""" + with open(filename, 'r') as infile: + for line in infile: + self.__file.write(line) + # end for + # end with + + ########################################################################### + + @property + def line_fill(self): + """Return the target line length for this Fortran file""" + return self.__line_fill + + ########################################################################### + + @property + def indent_size(self): + """Return the number of spaces for each indent level for this + Fortran file + """ + return self.__indent + + ########################################################################### + @classmethod def copyright(cls): """Return the standard Fortran file copyright string""" diff --git a/scripts/fortran_tools/parse_fortran.py b/scripts/fortran_tools/parse_fortran.py index 624f02cb..2310e13f 100644 --- a/scripts/fortran_tools/parse_fortran.py +++ b/scripts/fortran_tools/parse_fortran.py @@ -532,7 +532,8 @@ def class_match(cls, line): @classmethod def type_def_line(cls, line): """Return a type information if represents the start - of a type definition""" + of a type definition. + Otherwise, return None""" type_def = None if not cls.type_match(line): if '!' in line: @@ -629,7 +630,8 @@ def ftype_factory(line, context): def fortran_type_definition(line): ######################################################################## """Return a type information if represents the start - of a type definition""" + of a type definition. + Otherwise, return None.""" return FtypeTypeDecl.type_def_line(line) ######################################################################## @@ -720,8 +722,8 @@ def parse_fortran_var_decl(line, source, run_env): varprops = Ftype.parse_attr_specs(elements[0].strip(), context) for prop in varprops: if prop[0:6] == 'intent': - if source.type != 'scheme': - typ = source.type + if source.ptype != 'scheme': + typ = source.ptype errmsg = 'Invalid variable declaration, {}, intent' errmsg = errmsg + ' not allowed in {} variable' if run_env.logger is not None: diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index f37b3377..7c6493e7 100755 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,29 +563,39 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): module=spec_name, var_dict=var_dict) mheaders.append(mheader) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if (run_env.logger and + run_env.logger.isEnabledFor(logging.DEBUG)): ctx = context_string(pobj, nodir=True) msg = 'Adding header {}{}' run_env.logger.debug(msg.format(mheader.table_name, ctx)) + # end if break - elif ((type_def is not None) and (active_table is not None) and - (type_def[0].lower() == active_table.lower())): + elif type_def is not None: # Put statement back so caller knows where we are statements.insert(0, statement) - statements, ddt = parse_type_def(statements, type_def, - spec_name, pobj, run_env) - if ddt is None: - ctx = context_string(pobj, nodir=True) - msg = "No DDT found at '{}'{}" - raise CCPPError(msg.format(statement, ctx)) - # End if - mheaders.append(ddt) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): - ctx = context_string(pobj, nodir=True) - msg = 'Adding DDT {}{}' - run_env.logger.debug(msg.format(ddt.table_name, ctx)) - # End if - active_table = None + if ((active_table is not None) and + (type_def[0].lower() == active_table.lower())): + statements, ddt = parse_type_def(statements, type_def, + spec_name, pobj, run_env) + if ddt is None: + ctx = context_string(pobj, nodir=True) + msg = "No DDT found at '{}'{}" + raise CCPPError(msg.format(statement, ctx)) + # End if + mheaders.append(ddt) + if (run_env.logger and + run_env.logger.isEnabledFor(logging.DEBUG)): + ctx = context_string(pobj, nodir=True) + msg = 'Adding DDT {}{}' + run_env.logger.debug(msg.format(ddt.table_name, ctx)) + # End if + active_table = None + else: + # We found a type definition but it is not one with + # metadata. Just parse it and throw away what is found. + _ = parse_type_def(statements, type_def, + spec_name, pobj, run_env) + # end if elif active_table is not None: # We should have a variable definition to add if ((not is_comment_statement(statement)) and @@ -786,6 +796,7 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End program or module pmatch = endmatch.match(statement) asmatch = _ARG_TABLE_START_RE.match(statement) + type_def = fortran_type_definition(statement) if pmatch is not None: # We never found a contains statement inspec = False @@ -812,6 +823,13 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End if inspec = pobj.in_region('MODULE', region_name=mod_name) break + elif type_def: + # We have a type definition without metadata + # Just parse it and throw away what is found. + # Put statement back so caller knows where we are + statements.insert(0, statement) + _ = parse_type_def(statements, type_def, + spec_name, pobj, run_env) elif is_contains_statement(statement, inmod): inspec = False break diff --git a/scripts/framework_env.py b/scripts/framework_env.py index 8ee553f4..b5550ca6 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -103,8 +103,8 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, # String of definitions, separated by spaces preproc_list = [x.strip() for x in preproc_defs.split(' ') if x] else: - wmsg = "Error: Bad preproc list type, '{}'" - emsg += esep + wmsg.format(type(preproc_defs)) + wmsg = f"Error: Bad preproc list type, '{type_name{preproc_defs)}'" + emsg += esep + wmsg esep = '\n' # end if # Turn the list into a dictionary diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 17e8dfdb..3501e40e 100644 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -158,7 +158,7 @@ def blank_metadata_line(line): def _parse_config_line(line, context): """Parse a config line and return a list of keyword value pairs.""" - parse_items = list() + parse_items = [] if line is None: pass # No properties on this line elif blank_metadata_line(line): @@ -182,8 +182,8 @@ def _parse_config_line(line, context): def parse_metadata_file(filename, known_ddts, run_env): """Parse and return list of parsed metadata tables""" # Read all lines of the file at once - meta_tables = list() - table_titles = list() # Keep track of names in file + meta_tables = [] + table_titles = [] # Keep track of names in file with open(filename, 'r') as infile: fin_lines = infile.readlines() for index, fin_line in enumerate(fin_lines): @@ -225,7 +225,7 @@ def find_scheme_names(filename): """Find and return a list of all the physics scheme names in . A scheme is identified by its ccpp-table-properties name. """ - scheme_names = list() + scheme_names = [] with open(filename, 'r') as infile: fin_lines = infile.readlines() # end with @@ -283,7 +283,7 @@ def __init__(self, run_env, table_name_in=None, table_type_in=None, self.__pobj = parse_object self.__dependencies = dependencies self.__relative_path = relative_path - self.__sections = list() + self.__sections = [] self.__run_env = run_env if parse_object is None: if table_name_in is not None: @@ -339,7 +339,7 @@ def __init__(self, run_env, table_name_in=None, table_type_in=None, raise ParseInternalError(perr) # end if if known_ddts is None: - known_ddts = list() + known_ddts = [] # end if self.__start_context = ParseContext(context=self.__pobj) self.__init_from_file(known_ddts, self.__run_env) @@ -351,7 +351,7 @@ def __init_from_file(self, known_ddts, run_env): curr_line, _ = self.__pobj.next_line() in_properties_header = True skip_rest_of_section = False - self.__dependencies = list() # Default is no dependencies + self.__dependencies = [] # Default is no dependencies # Process lines until the end of the file or start of the next table. while ((curr_line is not None) and (not MetadataTable.table_start(curr_line))): @@ -440,7 +440,7 @@ def __init_from_file(self, known_ddts, run_env): known_ddts.append(self.table_name) # end if if self.__dependencies is None: - self.__dependencies = list() + self.__dependencies = [] # end if def start_context(self, with_comma=True, nodir=True): @@ -673,7 +673,7 @@ def __init__(self, table_name, table_type, run_env, parse_object=None, self.__start_context = None else: if known_ddts is None: - known_ddts = list() + known_ddts = [] # end if self.__start_context = ParseContext(context=self.__pobj) self.__init_from_file(table_name, table_type, known_ddts, run_env) @@ -683,7 +683,7 @@ def __init__(self, table_name, table_type, run_env, parse_object=None, register_fortran_ddt_name(self.title) # end if # Categorize the variables - self._var_intents = {'in' : list(), 'out' : list(), 'inout' : list()} + self._var_intents = {'in' : [], 'out' : [], 'inout' : [])} for var in self.variable_list(): intent = var.get_prop_value('intent') if intent is not None: @@ -693,7 +693,7 @@ def __init__(self, table_name, table_type, run_env, parse_object=None, def _default_module(self): """Set a default module for this header""" - mfile = self.__pobj.file_name + mfile = self.__pobj.filename if mfile[-5:] == '.meta': # Default value is a Fortran module that matches the filename def_mod = os.path.basename(mfile)[:-5] @@ -888,7 +888,7 @@ def parse_variable(self, curr_line, known_ddts): # Special case for dimensions, turn them into ranges if pname == 'dimensions': porig = pval - pval = list() + pval = [] for dim in porig: if ':' in dim: pval.append(dim) @@ -975,7 +975,7 @@ def check_array_reference(local_name, var_dict, context): local_name, colon_rank, ctx)) # end if - sub_dims = list() + sub_dims = [] sindex = 0 for rind in rdims: if rind == ':': @@ -1010,7 +1010,7 @@ def convert_dims_to_standard_names(self, var, logger=None, context=None): """Convert the dimension elements in to standard names by by using other variables in this header. """ - std_dims = list() + std_dims = [] vdims = var.get_dimensions() # Check for bad dimensions if vdims is None: @@ -1036,7 +1036,7 @@ def convert_dims_to_standard_names(self, var, logger=None, context=None): raise CCPPError("{}".format(errmsg)) # end if for dim in vdims: - std_dim = list() + std_dim = [] if ':' not in dim: # Metadata dimensions always have an explicit start var_one = CCPP_CONSTANT_VARS.find_local_name('1') @@ -1056,7 +1056,7 @@ def convert_dims_to_standard_names(self, var, logger=None, context=None): # Some non-standard integer value dname = item # end if - except ValueError: + except ValueError as verr: # Not an integer, try to find the standard_name if not item: # Naked colons are okay @@ -1070,15 +1070,15 @@ def convert_dims_to_standard_names(self, var, logger=None, context=None): # end if # end if if dname is None: - errmsg = "Unknown dimension element, {}, in {}{}" std = var.get_prop_value('local_name') - ctx = context_string(context) + errmsg = f"Unknown dimension element, {item}, in {std}" + errmsg += context_string(context) if logger is not None: errmsg = "ERROR: " + errmsg logger.error(errmsg.format(item, std, ctx)) dname = unique_standard_name() else: - raise CCPPError(errmsg.format(item, std, ctx)) + raise CCPPError(errmsg) from verr # end if # end if # end try diff --git a/scripts/metavar.py b/scripts/metavar.py index 71609580..7f852c4c 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -20,7 +20,7 @@ from parse_tools import check_units, check_dimensions, check_cf_standard_name from parse_tools import check_diagnostic_id, check_diagnostic_fixed from parse_tools import check_default_value, check_valid_values -from parse_tools import ParseContext, ParseSource +from parse_tools import ParseContext, ParseSource, type_name from parse_tools import ParseInternalError, ParseSyntaxError, CCPPError from var_props import CCPP_LOOP_DIM_SUBSTS, VariableProperty, VarCompatObj from var_props import find_horizontal_dimension, find_vertical_dimension @@ -261,7 +261,7 @@ def __init__(self, prop_dict, source, run_env, context=None, if isinstance(prop_dict, Var): prop_dict = prop_dict.copy_prop_dict() # end if - if source.type == 'scheme': + if source.ptype == 'scheme': self.__required_props = Var.__required_var_props # XXgoldyXX: v don't fill in default properties? # mstr_propdict = Var.__var_propdict @@ -272,7 +272,7 @@ def __init__(self, prop_dict, source, run_env, context=None, mstr_propdict = Var.__spec_propdict # XXgoldyXX: ^ don't fill in default properties? # end if - self._source = source + self.__source = source # Grab a frozen copy of the context if context is None: self._context = ParseContext(context=source.context) @@ -476,7 +476,7 @@ def clone(self, subst_dict=None, remove_intent=False, source_name = self.source.name # end if if source_type is None: - source_type = self.source.type + source_type = self.source.ptype # end if if context is None: context = self._context @@ -822,7 +822,7 @@ def parent(self, parent_var): else: emsg = 'Attempting to set parent for {}, bad parent type, {}' lname = self.get_prop_value('local_name') - raise ParseInternalError(emsg.format(lname, type(parent_var))) + raise ParseInternalError(emsg.format(lname, type_name(parent_var))) # end if def add_child(self, cvar): @@ -852,13 +852,13 @@ def context(self): @property def source(self): """Return the source object for this variable""" - return self._source + return self.__source @source.setter def source(self, new_source): """Reset this Var's source if seems legit""" if isinstance(new_source, ParseSource): - self._source = new_source + self.__source = new_source else: errmsg = 'Attemping to set source of {} ({}) to "{}"' stdname = self.get_prop_value('standard_name') @@ -874,7 +874,7 @@ def clone_source(self): @property def host_interface_var(self): """True iff self is included in the host model interface calls""" - return self.source.type == 'host' + return self.source.ptype == 'host' @property def run_env(self): @@ -1427,7 +1427,7 @@ def __init__(self, name, run_env, variables=None, self[stdname] = variables[key] # end for elif variables is not None: - raise ParseInternalError('Illegal type for variables, {} in {}'.format(type(variables), self.name)) + raise ParseInternalError('Illegal type for variables, {} in {}'.format(type_name(variables), self.name)) # end if @property @@ -1608,7 +1608,7 @@ def add_variable_dimensions(self, var, ignore_sources, to_dict=None, # end if if not present: dvar = self.find_variable(standard_name=dimname, any_scope=True) - if dvar and (dvar.source.type not in ignore_sources): + if dvar and (dvar.source.ptype not in ignore_sources): if to_dict: to_dict.add_variable(dvar, self.__run_env, exists_ok=True, @@ -1631,7 +1631,7 @@ def add_variable_dimensions(self, var, ignore_sources, to_dict=None, err_ret += "\nFound {} from excluded source, '{}'{}" lname = dvar.get_prop_value('local_name') dctx = context_string(dvar.context) - err_ret = err_ret.format(lname, dvar.source.type, dctx) + err_ret = err_ret.format(lname, dvar.source.ptype, dctx) # end if # end if # end if diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 03dd0429..4f888fb1 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -8,7 +8,7 @@ # pylint: disable=wrong-import-position from parse_source import ParseContext, ParseSource from parse_source import ParseSyntaxError, ParseInternalError -from parse_source import CCPPError, context_string +from parse_source import CCPPError, context_string, type_name from parse_source import unique_standard_name, reset_standard_name_counter from parse_object import ParseObject from parse_checkers import check_fortran_id, FORTRAN_ID @@ -71,6 +71,7 @@ 'set_log_to_file', 'set_log_to_null', 'set_log_to_stdout', + 'type_name', 'unique_standard_name', 'validate_xml_file' ] diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index 487478e6..ca5f1f51 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -12,7 +12,7 @@ ######################################################################## -_UNITS_RE = re.compile(r"^[^/@#$%^&*()\|<>\[\]{}?,.]+$") +_UNITS_RE = re.compile(r"^[^/!@#$%^&*=()\|<>\[\]{}?,.]+$") def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None diff --git a/scripts/parse_tools/parse_object.py b/scripts/parse_tools/parse_object.py index 4518d75c..f141b298 100644 --- a/scripts/parse_tools/parse_object.py +++ b/scripts/parse_tools/parse_object.py @@ -35,7 +35,6 @@ class ParseObject(ParseContext): def __init__(self, filename, lines_in, line_start=0): """Initialize this ParseObject""" - self.__filename = filename self.__lines = lines_in self.__line_start = line_start self.__line_end = line_start @@ -43,7 +42,7 @@ def __init__(self, filename, lines_in, line_start=0): self.__num_lines = len(self.__lines) self.__error_message = "" self.__num_errors = 0 - super(ParseObject, self).__init__(linenum=line_start, filename=filename) + super().__init__(linenum=line_start, filename=filename) @property def first_line_num(self): @@ -59,11 +58,6 @@ def valid_line(self): """Return True if the current line is valid""" return (self.line_num >= 0) and (self.line_num < self.__num_lines) - @property - def file_name(self): - """Return this object's filename""" - return self.__filename - @property def error_message(self): """Return this object's error message""" diff --git a/scripts/parse_tools/parse_source.py b/scripts/parse_tools/parse_source.py index 6d28b694..f96bbb2d 100644 --- a/scripts/parse_tools/parse_source.py +++ b/scripts/parse_tools/parse_source.py @@ -2,24 +2,18 @@ """Classes to aid the parsing process""" -import sys -# Find python version -PY3 = sys.version_info[0] > 2 -# pylint: disable=wrong-import-position # Python library imports -if PY3: - from collections.abc import Iterable -else: - from collections import Iterable +from collections.abc import Iterable # end if import copy +import sys import os.path import logging # CCPP framework imports # pylint: enable=wrong-import-position -class _StdNameCounter(object): +class _StdNameCounter(): """Class to hold a global counter to avoid using global keyword""" __SNAME_NUM = 0 # Counter for unique standard names @@ -117,6 +111,12 @@ def context_string(context=None, with_comma=True, nodir=False): # End if return cstr.format(comma=comma, where_str=where_str, ctx=context) +############################################################################### +def type_name(obj): +############################################################################### + """Return the name of the type of """ + return type(obj).__name__ + ############################################################################### class CCPPError(ValueError): """Class so programs can log user errors without backtrace""" @@ -198,7 +198,7 @@ def __getitem__(self, index): ######################################################################## -class ParseContext(object): +class ParseContext(): """A class for keeping track of a parsing position >>> ParseContext(32, "source.F90") #doctest: +ELLIPSIS <__main__.ParseContext object at 0x...> @@ -216,13 +216,6 @@ class ParseContext(object): """ - # python 2/3 difference - try: - __pstr_types = (str, unicode) - except NameError: - __pstr_types = (str,) - # End try - def __init__(self, linenum=None, filename=None, context=None): """Initialize this ParseContext""" # Set regions first in case of exception @@ -245,27 +238,27 @@ def __init__(self, linenum=None, filename=None, context=None): filename = context.filename elif filename is None: filename = "" - elif not isinstance(filename, ParseContext.__pstr_types): + elif not isinstance(filename, str): raise CCPPError('ParseContext filename must be a string') # No else, everything is okay # End if - self._linenum = linenum - self._filename = filename + self.__linenum = linenum + self.__filename = filename @property def line_num(self): """Return the current line""" - return self._linenum + return self.__linenum @line_num.setter def line_num(self, newnum): """Set a new line number for this context""" - self._linenum = newnum + self.__linenum = newnum @property def filename(self): """Return the object's filename""" - return self._filename + return self.__filename @property def regions(self): @@ -274,19 +267,19 @@ def regions(self): def __format__(self, spec): """Return a string representing the location in a file - Note that self._linenum is zero based. + Note that self.__linenum is zero based. can be 'dir' (show filename directory) or 'nodir' filename only. Any other spec entry is ignored. """ if spec == 'dir': - fname = self._filename + fname = self.__filename elif spec == 'nodir': - fname = os.path.basename(self._filename) + fname = os.path.basename(self.__filename) else: - fname = self._filename + fname = self.__filename # End if - if self._linenum >= 0: - fmt_str = "{}:{}".format(fname, self._linenum+1) + if self.__linenum >= 0: + fmt_str = "{}:{}".format(fname, self.__linenum+1) else: fmt_str = "{}".format(fname) # End if @@ -294,21 +287,21 @@ def __format__(self, spec): def __str__(self): """Return a string representing the location in a file - Note that self._linenum is zero based. + Note that self.__linenum is zero based. """ - if self._linenum >= 0: - retstr = "{}:{}".format(self._filename, self._linenum+1) + if self.__linenum >= 0: + retstr = "{}:{}".format(self.__filename, self.__linenum+1) else: - retstr = "{}".format(self._filename) + retstr = "{}".format(self.__filename) # End if return retstr def increment(self, inc=1): """Increment the location within a file""" - if self._linenum < 0: - self._linenum = 0 + if self.__linenum < 0: + self.__linenum = 0 # End if - self._linenum = self._linenum + inc + self.__linenum = self.__linenum + inc def enter_region(self, region_type, region_name=None, nested_ok=True): """Mark the entry of a region (e.g., DDT, module, function). @@ -378,12 +371,12 @@ def region_str(self): ######################################################################## -class ParseSource(object): +class ParseSource(): """ A simple object for providing source information >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")) #doctest: +ELLIPSIS <__main__.ParseSource object at 0x...> - >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).type + >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).ptype 'mytype' >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).name 'myname' @@ -393,24 +386,24 @@ class ParseSource(object): def __init__(self, name_in, type_in, context_in): """Initialize this ParseSource object.""" - self._name = name_in - self._type = type_in - self._context = context_in + self.__name = name_in + self.__type = type_in + self.__context = context_in @property - def type(self): + def ptype(self): """Return this source's type""" - return self._type + return self.__type @property def name(self): """Return this source's name""" - return self._name + return self.__name @property def context(self): """Return this source's context""" - return self._context + return self.__context ######################################################################## diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 414ffc5a..0886356d 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -7,26 +7,19 @@ # Python library imports from __future__ import print_function import os -import os.path import re +import shutil import subprocess import sys import xml.etree.ElementTree as ET sys.path.insert(0, os.path.dirname(__file__)) -# pylint: disable=wrong-import-position -try: - from distutils.spawn import find_executable - _XMLLINT = find_executable('xmllint') -except ImportError: - _XMLLINT = None -# End try # CCPP framework imports from parse_source import CCPPError from parse_log import init_log, set_log_to_null -# pylint: enable=wrong-import-position # Global data _INDENT_STR = " " +_XMLLINT = shutil.which('xmllint') # Blank if not installed beg_tag_re = re.compile(r"([<][^/][^<>]*[^/][>])") end_tag_re = re.compile(r"([<][/][^<>/]+[>])") simple_tag_re = re.compile(r"([<][^/][^<>/]+[/][>])") @@ -36,6 +29,14 @@ PYSUBVER = sys.version_info[1] _LOGGER = None +############################################################################### +class XMLToolsInternalError(ValueError): +############################################################################### + """Error class for reporting internal errors""" + def __init__(self, message): + """Initialize this exception""" + super().__init__(message) + ############################################################################### def call_command(commands, logger, silent=False): ############################################################################### @@ -53,35 +54,12 @@ def call_command(commands, logger, silent=False): result = False outstr = '' try: - if PY3: - if PYSUBVER > 6: - cproc = subprocess.run(commands, check=True, - capture_output=True) - if not silent: - logger.debug(cproc.stdout) - # End if - result = cproc.returncode == 0 - elif PYSUBVER >= 5: - cproc = subprocess.run(commands, check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if not silent: - logger.debug(cproc.stdout) - # End if - result = cproc.returncode == 0 - else: - raise ValueError("Python 3 must be at least version 3.5") - # End if - else: - pproc = subprocess.Popen(commands, stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, _ = pproc.communicate() - if not silent: - logger.debug(output) - # End if - result = pproc.returncode == 0 - # End if + cproc = subprocess.run(commands, check=True, + capture_output=True) + if not silent: + logger.debug(cproc.stdout) + # end if + result = cproc.returncode == 0 except (OSError, CCPPError, subprocess.CalledProcessError) as err: if silent: result = False @@ -90,9 +68,9 @@ def call_command(commands, logger, silent=False): emsg = "Execution of '{}' failed with code:\n" outstr = emsg.format(cmd, err.returncode) outstr += "{}".format(err.output) - raise CCPPError(outstr) - # End if - # End of try + raise CCPPError(outstr) from err + # end if + # end of try return result ############################################################################### @@ -102,6 +80,8 @@ def find_schema_version(root): Find the version of the host registry file represented by root >>> find_schema_version(ET.fromstring('')) [1, 0] + >>> find_schema_version(ET.fromstring('')) + [2, 0] >>> find_schema_version(ET.fromstring('')) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): CCPPError: Illegal version string, '1.a' @@ -118,33 +98,33 @@ def find_schema_version(root): verbits = None if 'version' not in root.attrib: raise CCPPError("version attribute required") - # End if + # end if version = root.attrib['version'] versplit = version.split('.') try: if len(versplit) != 2: raise CCPPError('oops') - # End if (no else needed) + # end if (no else needed) try: verbits = [int(x) for x in versplit] except ValueError as verr: - raise CCPPError(verr) - # End try + raise CCPPError(verr) from verr + # end try if verbits[0] < 1: raise CCPPError('Major version must be at least 1') - # End if + # end if if verbits[1] < 0: raise CCPPError('Minor version must be non-negative') - # End if + # end if except CCPPError as verr: errstr = """Illegal version string, '{}' Format must be .""" ve_str = str(verr) if ve_str: errstr = ve_str + '\n' + errstr - # End if - raise CCPPError(errstr.format(version)) - # End try + # end if + raise CCPPError(errstr.format(version)) from verr + # end try return verbits ############################################################################### @@ -161,10 +141,10 @@ def find_schema_file(schema_root, version, schema_path=None): schema_file = os.path.join(schema_path, schema_filename) else: schema_file = schema_filename - # End if + # end if if os.path.exists(schema_file): return schema_file - # End if + # end if return None ############################################################################### @@ -178,38 +158,42 @@ def validate_xml_file(filename, schema_root, version, logger, # Check the filename if not os.path.isfile(filename): raise CCPPError("validate_xml_file: Filename, '{}', does not exist".format(filename)) - # End if + # end if if not os.access(filename, os.R_OK): raise CCPPError("validate_xml_file: Cannot open '{}'".format(filename)) - # End if - if not schema_path: - # Find the schema, based on the model version - thispath = os.path.abspath(__file__) - pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) - schema_path = os.path.join(pdir, 'schema') - # End if - schema_file = find_schema_file(schema_root, version, schema_path) - if not (schema_file and os.path.isfile(schema_file)): - verstring = '.'.join([str(x) for x in version]) - emsg = """validate_xml_file: Cannot find schema for version {}, - {} does not exist""" - raise CCPPError(emsg.format(verstring, schema_file)) - # End if + # end if + if os.path.isfile(schema_root): + # We already have a file, just use it + schema_file = schema_root + else: + if not schema_path: + # Find the schema, based on the model version + thispath = os.path.abspath(__file__) + pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) + schema_path = os.path.join(pdir, 'schema') + # end if + schema_file = find_schema_file(schema_root, version, schema_path) + if not (schema_file and os.path.isfile(schema_file)): + verstring = '.'.join([str(x) for x in version]) + emsg = """validate_xml_file: Cannot find schema for version {}, {} does not exist""" + raise CCPPError(emsg.format(verstring, schema_file)) + # end if + # end if if not os.access(schema_file, os.R_OK): emsg = "validate_xml_file: Cannot open schema, '{}'" raise CCPPError(emsg.format(schema_file)) - # End if - if _XMLLINT is not None: + # end if + if _XMLLINT: logger.debug("Checking file {} against schema {}".format(filename, schema_file)) cmd = [_XMLLINT, '--noout', '--schema', schema_file, filename] result = call_command(cmd, logger) return result - # End if + # end if lmsg = "xmllint not found, could not validate file {}" if error_on_noxmllint: raise CCPPError("validate_xml_file: " + lmsg.format(filename)) - # End if + # end if logger.warning(lmsg.format(filename)) return True # We could not check but still need to proceed @@ -229,13 +213,13 @@ def read_xml_file(filename, logger=None): root = tree.getroot() except ET.ParseError as perr: emsg = "read_xml_file: Cannot read {}, {}" - raise CCPPError(emsg.format(filename, perr)) + raise CCPPError(emsg.format(filename, perr)) from perr elif not os.access(filename, os.R_OK): raise CCPPError("read_xml_file: Cannot open '{}'".format(filename)) else: emsg = "read_xml_file: Filename, '{}', does not exist" raise CCPPError(emsg.format(filename)) - # End if + # end if if logger: logger.debug("Read XML file, '{}'".format(filename)) # End if @@ -248,7 +232,7 @@ class PrettyElementTree(ET.ElementTree): def __init__(self, element=None, file=None): """Initialize a PrettyElementTree object""" - super(PrettyElementTree, self).__init__(element, file) + super().__init__(element, file) def _write(self, outfile, line, indent, eol=os.linesep): """Write as an ASCII string to """ @@ -268,7 +252,7 @@ def _inc_pos(outstr, text, txt_beg): # end if emsg = "No output at {} of {}\n{}".format(txt_beg, len(text), text[txt_beg:txt_end]) - raise DatatableInternalError(emsg) + raise XMLToolsInternalError(emsg) def write(self, file, encoding="us-ascii", xml_declaration=None, default_namespace=None, method="xml", @@ -276,26 +260,26 @@ def write(self, file, encoding="us-ascii", xml_declaration=None, """Subclassed write method to format output.""" if PY3 and (PYSUBVER >= 4): if PYSUBVER >= 8: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method, - xml_declaration=xml_declaration, - default_namespace=default_namespace, - short_empty_elements=short_empty_elements) + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method, + xml_declaration=xml_declaration, + default_namespace=default_namespace, + short_empty_elements=short_empty_elements) else: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method, - short_empty_elements=short_empty_elements) + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method, + short_empty_elements=short_empty_elements) # end if else: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method) + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method) # end if if PY3: fmode = 'wt' - root = str(input, encoding="utf-8") + root = str(et_str, encoding="utf-8") else: fmode = 'w' - root = input + root = et_str # end if indent = 0 last_write_text = False diff --git a/scripts/state_machine.py b/scripts/state_machine.py index 966ad04f..393e423d 100644 --- a/scripts/state_machine.py +++ b/scripts/state_machine.py @@ -30,7 +30,7 @@ class StateMachine: >>> StateMachine([('ab','a','b','a')]).final_state('ab') 'b' >>> StateMachine([('ab','a','b','a')]).transition_regex('ab') - re.compile('a$') + re.compile('a$, re.IGNORECASE') >>> StateMachine([('ab','a','b','a')]).function_match('foo_a', transition='ab') ('foo', 'a', 'ab') >>> StateMachine([('ab','a','b',r'ax?')]).function_match('foo_a', transition='ab') @@ -162,8 +162,9 @@ def __setitem__(self, key, value): if len(value) != 3: raise ValueError("Invalid transition ({}), should be of the form (inital_state, final_state, regex).".format(value)) # end if - regex = re.compile(value[2] + r"$") - function = re.compile(FORTRAN_ID + r"_(" + value[2] + r")$") + regex = re.compile(value[2] + r"$", re.IGNORECASE) + function = re.compile(FORTRAN_ID + r"_(" + value[2] + r")$", + re.IGNORECASE) self.__stt__[key] = (value[0], value[1], regex, function) def __delitem__(self, key): diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 472556cb..cc34a750 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -361,7 +361,7 @@ def register_action(self, vaction): @classmethod def is_suite_variable(cls, var): """Return True iff belongs to our Suite""" - return var and (var.source.type == _API_SUITE_VAR_NAME) + return var and (var.source.ptype == _API_SUITE_VAR_NAME) def is_local_variable(self, var): """Return the local variable matching if one is found belonging @@ -433,7 +433,7 @@ def add_call_list_variable(self, newvar, exists_ok=False, if dvar is None: emsg = "{}: Could not find dimension {} in {}" raise ParseInternalError(emsg.format(self.name, - stdname, vardim)) + vardim, stdname)) # end if elif self.parent is None: errmsg = 'No call_list found for {}'.format(newvar) @@ -831,7 +831,7 @@ def match_variable(self, var, vstdname=None, vdims=None): found_var = self.parent.add_variable_to_call_tree(dict_var, vmatch=vmatch) new_vdims = vdims - elif dict_var.source.type in _API_LOCAL_VAR_TYPES: + elif dict_var.source.ptype in _API_LOCAL_VAR_TYPES: # We cannot change the dimensions of locally-declared variables # Using a loop substitution is invalid because the loop variable # value has not yet been set. @@ -1716,7 +1716,7 @@ def find_variable(self, standard_name=None, source_var=None, search_call_list=search_call_list, loop_subst=loop_subst) if fvar and fvar.is_constituent(): - if fvar.source.type == ConstituentVarDict.constitutent_source_type(): + if fvar.source.ptype == ConstituentVarDict.constitutent_source_type(): # We found this variable in the constituent dictionary, # add it to our call list self.add_call_list_variable(fvar, exists_ok=True) From 43031fe8d7cb30ae9bdb8297507afc64c2ee9e80 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 21 Aug 2023 13:35:15 -0600 Subject: [PATCH 07/49] initial stab at testing-only PR --- .github/workflows/capgen_unit_tests.yaml | 22 ++ test/advection_test/cld_ice.F90 | 12 +- test/advection_test/cld_ice.meta | 4 +- test/advection_test/cld_liq.F90 | 12 +- test/advection_test/cld_liq.meta | 4 +- test/run_tests.sh | 15 +- .../fortran_files/comments_test.F90 | 33 ++ .../fortran_files/linebreak_test.F90 | 39 +++ .../sample_host_files/data1_mod.F90 | 11 + .../sample_host_files/data1_mod.meta | 25 ++ test/unit_tests/test_fortran_write.py | 126 +++++++ test/unit_tests/test_metadata_host_file.py | 260 ++++++++++++++ test/unit_tests/test_metadata_scheme_file.py | 329 +++++++++++------- test/unit_tests/test_metadata_table.py | 2 +- 14 files changed, 737 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/capgen_unit_tests.yaml create mode 100644 test/unit_tests/sample_files/fortran_files/comments_test.F90 create mode 100644 test/unit_tests/sample_files/fortran_files/linebreak_test.F90 create mode 100644 test/unit_tests/sample_host_files/data1_mod.F90 create mode 100644 test/unit_tests/sample_host_files/data1_mod.meta create mode 100644 test/unit_tests/test_fortran_write.py create mode 100644 test/unit_tests/test_metadata_host_file.py diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml new file mode 100644 index 00000000..eb574f2d --- /dev/null +++ b/.github/workflows/capgen_unit_tests.yaml @@ -0,0 +1,22 @@ +name: Capgen Unit Tests + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + #Trigger workflow on push to any branch or branch heirarchy: + - '**' + +jobs: + unit_tests: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: update repos and install dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential gfortran cmake python3 git + - name: Run unit tests + run: cd test && ./run_tests.sh + diff --git a/test/advection_test/cld_ice.F90 b/test/advection_test/cld_ice.F90 index 5a91282f..9c1e769a 100644 --- a/test/advection_test/cld_ice.F90 +++ b/test/advection_test/cld_ice.F90 @@ -19,7 +19,7 @@ MODULE cld_ice !> \section arg_table_cld_ice_run Argument Table !! \htmlinclude arg_table_cld_ice_run.html !! - subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & + subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -27,7 +27,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_ice(:,:) + REAL(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -44,7 +44,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & do ilev = 1, size(temp, 2) if (temp(icol, ilev) < tcld) then frz = MAX(qv(icol, ilev) - 0.5_kind_phys, 0.0_kind_phys) - cld_ice(icol, ilev) = cld_ice(icol, ilev) + frz + cld_ice_array(icol, ilev) = cld_ice_array(icol, ilev) + frz qv(icol, ilev) = qv(icol, ilev) - frz if (frz > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + 1.0_kind_phys @@ -58,16 +58,16 @@ END SUBROUTINE cld_ice_run !> \section arg_table_cld_ice_init Argument Table !! \htmlinclude arg_table_cld_ice_init.html !! - subroutine cld_ice_init(tfreeze, cld_ice, errmsg, errflg) + subroutine cld_ice_init(tfreeze, cld_ice_array, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(inout) :: cld_ice(:,:) + real(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg errmsg = '' errflg = 0 - cld_ice = 0.0_kind_phys + cld_ice_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_ice_init diff --git a/test/advection_test/cld_ice.meta b/test/advection_test/cld_ice.meta index e4a961ab..f66888e0 100644 --- a/test/advection_test/cld_ice.meta +++ b/test/advection_test/cld_ice.meta @@ -41,7 +41,7 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. units = kg kg-1 @@ -73,7 +73,7 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. units = kg kg-1 diff --git a/test/advection_test/cld_liq.F90 b/test/advection_test/cld_liq.F90 index 2e1e5a57..1e6940e9 100644 --- a/test/advection_test/cld_liq.F90 +++ b/test/advection_test/cld_liq.F90 @@ -16,7 +16,7 @@ MODULE cld_liq !> \section arg_table_cld_liq_run Argument Table !! \htmlinclude arg_table_cld_liq_run.html !! - subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & + subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -25,7 +25,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_liq(:,:) + REAL(kind_phys), intent(inout) :: cld_liq_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -43,7 +43,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & if ( (qv(icol, ilev) > 0.0_kind_phys) .and. & (temp(icol, ilev) <= tcld)) then cond = MIN(qv(icol, ilev), 0.1_kind_phys) - cld_liq(icol, ilev) = cld_liq(icol, ilev) + cond + cld_liq_array(icol, ilev) = cld_liq_array(icol, ilev) + cond qv(icol, ilev) = qv(icol, ilev) - cond if (cond > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + (cond * 5.0_kind_phys) @@ -57,10 +57,10 @@ END SUBROUTINE cld_liq_run !> \section arg_table_cld_liq_init Argument Table !! \htmlinclude arg_table_cld_liq_init.html !! - subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) + subroutine cld_liq_init(tfreeze, cld_liq_array, tcld, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(out) :: cld_liq(:,:) + real(kind_phys), intent(out) :: cld_liq_array(:,:) real(kind_phys), intent(out) :: tcld character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg @@ -69,7 +69,7 @@ subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) errmsg = '' errflg = 0 - cld_liq = 0.0_kind_phys + cld_liq_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_liq_init diff --git a/test/advection_test/cld_liq.meta b/test/advection_test/cld_liq.meta index 1186d741..4c87f4b7 100644 --- a/test/advection_test/cld_liq.meta +++ b/test/advection_test/cld_liq.meta @@ -47,7 +47,7 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 @@ -79,7 +79,7 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 diff --git a/test/run_tests.sh b/test/run_tests.sh index 97001d30..04ce5e8c 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -38,12 +38,14 @@ if [ $res -ne 0 ]; then fi # Run var_action test -./var_action_test/run_test -res=$? -errcnt=$((errcnt + res)) -if [ $res -ne 0 ]; then - echo "Failure running var_action test" -fi +# TODO: Re-enable after feature fully implemented. +# ./var_action_test/run_test +# res=$? +# errcnt=$((errcnt + res)) +# if [ $res -ne 0 ]; then +# echo "Failure running var_action test" +# fi +echo "Skipping var_action_test/run_test until feature is fully implemented" # Run doctests ./run_doctest.sh @@ -67,4 +69,5 @@ if [ $errcnt -eq 0 ]; then echo "All tests PASSed!" else echo "${errcnt} tests FAILed" + return 1 fi diff --git a/test/unit_tests/sample_files/fortran_files/comments_test.F90 b/test/unit_tests/sample_files/fortran_files/comments_test.F90 new file mode 100644 index 00000000..d4820a36 --- /dev/null +++ b/test/unit_tests/sample_files/fortran_files/comments_test.F90 @@ -0,0 +1,33 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated Test of comment writing for FortranWriter +!! +! +module comments_test + +! We can write comments in the module header + ! We can write indented comments in the header + integer :: foo ! Comment at end of line works + integer :: bar ! + ! xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ! + integer :: baz ! + ! yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + ! yyyyy + +CONTAINS + ! We can write comments in the module body + +end module comments_test diff --git a/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 b/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 new file mode 100644 index 00000000..4f89441f --- /dev/null +++ b/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 @@ -0,0 +1,39 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated Test of line breaking for FortranWriter +!! +! +module linebreak_test + + character(len=7) :: data = (/ name000, name001, name002, name003, name004, name005, name006, & + name007, name008, name009, name010, name011, name012, name013, name014, name015, & + name016, name017, name018, name019, name020, name021, name022, name023, name024, & + name025, name026, name027, name028, name029, name030, name031, name032, name033, & + name034, name035, name036, name037, name038, name039, name040, name041, name042, & + name043, name044, name045, name046, name047, name048, name049, name050, name051, & + name052, name053, name054, name055, name056, name057, name058, name059, name060, & + name061, name062, name063, name064, name065, name066, name067, name068, name069, & + name070, name071, name072, name073, name074, name075, name076, name077, name078, & + name079, name080, name081, name082, name083, name084, name085, name086, name087, & + name088, name089, name090, name091, name092, name093, name094, name095, name096, & + name097, name098, name099 /) + +CONTAINS + call & + endrun('Cannot read columns_on_task from file'// & + ', columns_on_task has no horizontal dimension; columns_on_task is a protected variable') + + +end module linebreak_test diff --git a/test/unit_tests/sample_host_files/data1_mod.F90 b/test/unit_tests/sample_host_files/data1_mod.F90 new file mode 100644 index 00000000..b85db315 --- /dev/null +++ b/test/unit_tests/sample_host_files/data1_mod.F90 @@ -0,0 +1,11 @@ +module data1_mod + + use ccpp_kinds, only: kind_phys + + !> \section arg_table_data1_mod Argument Table + !! \htmlinclude arg_table_data1_mod.html + real(kind_phys) :: ps1 + real(kind_phys), allocatable :: xbox(:,:) + real(kind_phys), allocatable :: switch(:,:) + +end module data1_mod diff --git a/test/unit_tests/sample_host_files/data1_mod.meta b/test/unit_tests/sample_host_files/data1_mod.meta new file mode 100644 index 00000000..37e2de96 --- /dev/null +++ b/test/unit_tests/sample_host_files/data1_mod.meta @@ -0,0 +1,25 @@ +[ccpp-table-properties] + name = data1_mod + type = module +[ccpp-arg-table] + name = data1_mod + type = module +[ ps1 ] + standard_name = play_station + state_variable = true + type = real | kind = kind_phys + units = Pa + dimensions = () +[ xbox ] + standard_name = xbox + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) +[ switch ] + standard_name = nintendo_switch + long_name = Incompatible junk + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) diff --git a/test/unit_tests/test_fortran_write.py b/test/unit_tests/test_fortran_write.py new file mode 100644 index 00000000..fdc00085 --- /dev/null +++ b/test/unit_tests/test_fortran_write.py @@ -0,0 +1,126 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Contains unit tests for FortranWriter + in scripts file fortran/fortran_write.py + + Assumptions: + + Command line arguments: none + + Usage: python3 test_fortran_write.py # run the unit tests +----------------------------------------------------------------------- +""" + +import filecmp +import glob +import os +import sys +import unittest + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +_SAMPLE_FILES_DIR = os.path.join(_TEST_DIR, "sample_files", "fortran_files") +_PRE_TMP_DIR = os.path.join(_TEST_DIR, "tmp") +_TMP_DIR = os.path.join(_PRE_TMP_DIR, "fortran_files") + +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError(f"Cannot find scripts directory, {_SCRIPTS_DIR}") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from fortran_tools import FortranWriter +# pylint: enable=wrong-import-position + +############################################################################### +def remove_files(file_list): +############################################################################### + """Remove files in if they exist""" + if isinstance(file_list, str): + file_list = [file_list] + # end if + for fpath in file_list: + if os.path.exists(fpath): + os.remove(fpath) + # End if + # End for + +class MetadataTableTestCase(unittest.TestCase): + + """Tests for `FortranWriter`.""" + + @classmethod + def setUpClass(cls): + """Clean output directory (tmp) before running tests""" + #Does "tmp" directory exist? If not then create it: + if not os.path.exists(_PRE_TMP_DIR): + os.mkdir(_PRE_TMP_DIR) + # Ensure the "sample_files/fortran_files" directory exists and is empty + if os.path.exists(_TMP_DIR): + # Clear out all files: + remove_files(glob.iglob(os.path.join(_TMP_DIR, '*.*'))) + else: + os.makedirs(_TMP_DIR) + # end if + + #Run inherited setup method: + super().setUpClass() + + def test_line_breaking(self): + """Test that FortranWriter correctly breaks long lines""" + # Setup + testname = "linebreak_test" + compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") + generate = os.path.join(_TMP_DIR, f"{testname}.F90") + # Exercise + header = "Test of line breaking for FortranWriter" + with FortranWriter(generate, 'w', header, f"{testname}") as gen: + # Test long declaration + data_items = ', '.join([f"name{x:03}" for x in range(100)]) + gen.write(f"character(len=7) :: data = (/ {data_items} /)", 1) + gen.end_module_header() + # Test long code lines + line_items = ["call endrun('Cannot read columns_on_task from ", + "file'//', columns_on_task has no horizontal ", + "dimension; columns_on_task is a ", + "protected variable')"] + gen.write(f"{''.join(line_items)}", 2) + # end with + + # Check that file was generated + amsg = f"{generate} does not exist" + self.assertTrue(os.path.exists(generate), msg=amsg) + amsg = f"{generate} does not match {compare}" + self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) + + def test_good_comments(self): + """Test that comments are written and broken correctly.""" + # Setup + testname = "comments_test" + compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") + generate = os.path.join(_TMP_DIR, f"{testname}.F90") + # Exercise + header = "Test of comment writing for FortranWriter" + with FortranWriter(generate, 'w', header, f"{testname}") as gen: + gen.comment("We can write comments in the module header", 0) + gen.comment("We can write indented comments in the header", 1) + gen.write("integer :: foo ! Comment at end of line works", 1) + # Test long comments at end of line + gen.write(f"integer :: bar ! {'x'*100}", 1) + gen.write(f"integer :: baz ! {'y'*130}", 1) + gen.end_module_header() + # Test comment line in body + gen.comment("We can write comments in the module body", 1) + + # end with + + # Check that file was generated + amsg = f"{generate} does not exist" + self.assertTrue(os.path.exists(generate), msg=amsg) + amsg = f"{generate} does not match {compare}" + self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit_tests/test_metadata_host_file.py b/test/unit_tests/test_metadata_host_file.py new file mode 100644 index 00000000..cc5ba7b5 --- /dev/null +++ b/test/unit_tests/test_metadata_host_file.py @@ -0,0 +1,260 @@ +#! /usr/bin/env python3 + +""" +----------------------------------------------------------------------- + Description: capgen needs to compare a metadata header against the + associated CCPP Fortran interface routine. This set of + tests is testing the parse_host_model_files function in + ccpp_capgen.py which performs the operations in the first + bullet below. Each test calls this function. + + * This script contains unit tests that do the following: + 1) Read one or more metadata files (to collect + the metadata headers) + 2) Read the associated CCPP Fortran host file(s) (to + collect Fortran interfaces) + 3) Create a CCPP host model object + 3) Test the properties of the CCPP host model object + + * Tests include: + - Correctly parse and match a simple module file with + data fields (a data block) + - Correctly parse and match a simple module file with + a DDT definition + - Correctly parse and match a simple module file with + two DDT definitions + - Correctly parse and match a simple module file with + two DDT definitions and a data block + + Assumptions: + + Command line arguments: none + + Usage: python3 test_metadata_host_file.py # run the unit tests +----------------------------------------------------------------------- +""" +import sys +import os +import logging +import unittest + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError("Cannot find scripts directory") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from ccpp_capgen import parse_host_model_files +from framework_env import CCPPFrameworkEnv +from parse_tools import CCPPError +# pylint: enable=wrong-import-position + +class MetadataHeaderTestCase(unittest.TestCase): + """Unit tests for parse_host_model_files""" + + def setUp(self): + """Setup important directories and logging""" + self._sample_files_dir = os.path.join(_TEST_DIR, "sample_host_files") + logger = logging.getLogger(self.__class__.__name__) + self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', + 'scheme_files':'', + 'suites':''}) + + def test_module_with_data(self): + """Test that a module containing a data block is parsed and matched + correctly.""" + # Setup + module_files = [os.path.join(self._sample_files_dir, "data1_mod.meta")] + # Exercise + hname = 'host_name_data1' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue('data1_mod' in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('play_station' in std_names) + self.assertTrue('xbox' in std_names) + self.assertTrue('nintendo_switch' in std_names) + + def test_module_with_one_ddt(self): + """Test that a module containing a DDT definition is parsed and matched + correctly.""" + # Setup + ddt_name = 'ddt1_t' + module_files = [os.path.join(self._sample_files_dir, "ddt1.meta")] + # Exercise + hname = 'host_name_ddt1' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue(ddt_name in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that the DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), 2) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('ddt_var_array_dimension' in std_names) + self.assertTrue('vars_array' in std_names) + + def test_module_with_two_ddts(self): + """Test that a module containing two DDT definitions is parsed and + matched correctly.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, "ddt2.meta")] + # Exercise + hname = 'host_name_ddt2' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), len(ddt_names)) + # Verify header titles + for ddt_name in ddt_names: + self.assertTrue(ddt_name in module_headers) + # end for + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that each DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + for index, ddt_name in enumerate(ddt_names): + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), len(ddt_vars[index])) + std_names = [x.get_prop_value('standard_name') for x in vlist] + for sname in ddt_vars[index]: + self.assertTrue(sname in std_names) + # end for + # end for + + def test_module_with_two_ddts_and_data(self): + """Test that a module containing two DDT definitions and a block of + module data is parsed and matched correctly.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, + "ddt_data1_mod.meta")] + # Exercise + hname = 'host_name_ddt_data' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), len(ddt_names) + 1) + # Verify header titles + for ddt_name in ddt_names: + self.assertTrue(ddt_name in module_headers) + # end for + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + # Verify that each DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + for index, ddt_name in enumerate(ddt_names): + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), len(ddt_vars[index])) + std_names = [x.get_prop_value('standard_name') for x in vlist] + for sname in ddt_vars[index]: + self.assertTrue(sname in std_names) + # end for + # end for + # Verify header titles + self.assertTrue('ddt_data1_mod' in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('play_station' in std_names) + self.assertTrue('xbox' in std_names) + self.assertTrue('nintendo_switch' in std_names) + + def test_module_with_one_ddt_plus_undoc(self): + """Test that a module containing a one documented DDT definition + (i.e., with metadata) and one DDT without (i.e., no metadata) + is parsed and matched correctly.""" + # Setup + ddt_name = 'ddt2_t' + module_files = [os.path.join(self._sample_files_dir, "ddt1_plus.meta")] + # Exercise + hname = 'host_name_ddt1_plus' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue(ddt_name in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that the DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), 2) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('ddt_var_array_dimension' in std_names) + self.assertTrue('vars_array' in std_names) + + def test_module_with_two_ddts_and_extra_var(self): + """Test that a module containing two DDT definitions is parsed and + a useful error message is produced if the DDT metadata has an + extra variable.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, + "ddt2_extra_var.meta")] + # Exercise + hname = 'host_name_ddt_extra_var' + with self.assertRaises(CCPPError) as context: + host_model = parse_host_model_files(module_files, hname, + self._run_env) + # end with + # Check error messages + except_str = str(context.exception) + emsgs = ["Variable mismatch in ddt2_t, variables missing from Fortran ddt.", + + "No Fortran variable for bogus in ddt2_t", + "2 errors found comparing"] + for emsg in emsgs: + self.assertTrue(emsg in except_str) + # end for + +if __name__ == "__main__": + unittest.main() diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 52f62aa1..9598406d 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -40,7 +40,7 @@ Command line arguments: none - Usage: python test_metadata_scheme_file.py # run the unit tests + Usage: python3 test_metadata_scheme_file.py # run the unit tests ----------------------------------------------------------------------- """ import sys @@ -59,6 +59,7 @@ # pylint: disable=wrong-import-position from ccpp_capgen import parse_scheme_files from framework_env import CCPPFrameworkEnv +from parse_tools import CCPPError # pylint: enable=wrong-import-position class MetadataHeaderTestCase(unittest.TestCase): @@ -67,6 +68,7 @@ class MetadataHeaderTestCase(unittest.TestCase): def setUp(self): """Setup important directories and logging""" self._sample_files_dir = os.path.join(_TEST_DIR, "sample_scheme_files") + self._host_files_dir = os.path.join(_TEST_DIR, "sample_host_files") logger = logging.getLogger(self.__class__.__name__) self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', 'scheme_files':'', @@ -85,203 +87,262 @@ def setUp(self): 'CCPP=2'}) def test_good_scheme_file(self): - """Test that good metadata file matches the Fortran, with routines in the same order """ - #Setup + """Test that good metadata file matches the Fortran, + with routines in the same order """ + # Setup scheme_files = [os.path.join(self._sample_files_dir, "temp_adjust.meta")] - #Exercise + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - #Verify size of returned list equals number of scheme headers in the test file - # and that header (subroutine) names are 'temp_adjust_[init,run,finalize]' + # Verify size of returned list equals number of scheme headers + # in the test file and that header (subroutine) names are + # 'temp_adjust_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('temp_adjust_init' in titles) self.assertTrue('temp_adjust_run' in titles) self.assertTrue('temp_adjust_finalize' in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('temp_adjust' in table_dict) def test_reordered_scheme_file(self): - """Test that metadata file matches the Fortran when the routines are not in the same order """ - #Setup + """Test that metadata file matches the Fortran when the + routines are not in the same order """ + # Setup scheme_files = [os.path.join(self._sample_files_dir, "reorder.meta")] - #Exercise + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - #Verify size of returned list equals number of scheme headers in the test file - # and that header (subroutine) names are 'reorder_[init,run,finalize]' + # Verify size of returned list equals number of scheme headers + # in the test file and that header (subroutine) names are + # 'reorder_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('reorder_init' in titles) self.assertTrue('reorder_run' in titles) self.assertTrue('reorder_finalize' in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('reorder' in table_dict) def test_missing_metadata_header(self): - """Test that a missing metadata header (aka arg table) is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "missing_arg_table.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that a missing metadata header (aka arg table) is + corretly detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "missing_arg_table.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned + # Verify correct error message returned emsg = "No matching metadata header found for missing_arg_table_run in" self.assertTrue(emsg in str(context.exception)) def test_missing_fortran_header(self): """Test that a missing fortran header is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "missing_fort_header.meta")] - #Exercise - with self.assertRaises(Exception) as context: + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "missing_fort_header.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned + # Verify correct error message returned emsg = "No matching Fortran routine found for missing_fort_header_run in" self.assertTrue(emsg in str(context.exception)) def test_mismatch_intent(self): - """Test that differing intent, kind, rank, and type between metadata and fortran is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "mismatch_intent.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that differing intent, kind, rank, and type between + metadata and fortran is corretly detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "mismatch_intent.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify 4 correct error messages returned - self.assertTrue('intent mismatch (in != inout) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at' in str(context.exception)) - self.assertTrue('type mismatch (integer != real) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('4 errors found comparing' in str(context.exception)) + # Verify 4 correct error messages returned + emsg = "intent mismatch (in != inout) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + emsg = "kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + emsg = "rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at" + self.assertTrue(emsg in str(context.exception)) + emsg = "type mismatch (integer != real) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("4 errors found comparing" in str(context.exception)) def test_invalid_subr_stmnt(self): - """Test that invalid Fortran subroutine statements are correctly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "invalid_subr_stmnt.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that invalid Fortran subroutine statements are correctly + detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "invalid_subr_stmnt.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned - self.assertTrue("Invalid dummy argument, 'errmsg', at" in str(context.exception)) + # Verify correct error message returned + self.assertTrue("Invalid dummy argument, 'errmsg', at" + in str(context.exception)) def test_invalid_dummy_arg(self): - """Test that invalid dummy argument statements are correctly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "invalid_dummy_arg.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that invalid dummy argument statements are correctly detected""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "invalid_dummy_arg.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned - self.assertTrue("Invalid dummy argument, 'woohoo', at" in str(context.exception)) + # Verify correct error message returned + emsg = "Invalid dummy argument, 'woohoo', at" + self.assertTrue(emsg in str(context.exception)) -# pylint: disable=invalid-name - def test_CCPPnotset_var_missing_in_meta(self): - """Test for correct detection of a variable that REMAINS in the subroutine argument list - (due to an undefined pre-processor directive: #ifndef CCPP), BUT IS NOT PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPnotset_var_missing_in_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: + def test_ccpp_notset_var_missing_in_meta(self): + """Test for correct detection of a variable that REMAINS in the + subroutine argument list + (due to an undefined pre-processor directive: #ifndef CCPP), + BUT IS NOT PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPnotset_var_missing_in_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPnotset_var_missing_in_meta_run, variables missing from metadata header.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) - self.assertTrue('Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPnotset_var_missing_in_meta_run, " + \ + "variables missing from metadata header." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run" + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_missing_in_fort(self): - """Test for correct detection of a variable that IS REMOVED the subroutine argument list - (due to a pre-processor directive: #ifndef CCPP), but IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_fort.meta")] - #Exercise - with self.assertRaises(Exception) as context: + def test_ccpp_eq1_var_missing_in_fort(self): + """Test for correct detection of a variable that IS REMOVED the + subroutine argument list + (due to a pre-processor directive: #ifndef CCPP), + but IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_missing_in_fort.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, variables missing from Fortran scheme.' - in str(context.exception)) - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, no Fortran variable bar.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ + "variables missing from Fortran scheme." + self.assertTrue(emsg in str(context.exception)) + emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ + "no Fortran variable bar." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), and IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_in_fort_meta.meta")] - #Exercise + def test_ccpp_eq1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the + subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), + and IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_in_fort_meta.meta")] + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify size of returned list equals number of scheme headers in the test file (1) - # and that header (subroutine) name is 'CCPPeq1_var_in_fort_meta_run' + # Verify size of returned list equals number of scheme headers in + # the test file (1) and that header (subroutine) name is + # 'CCPPeq1_var_in_fort_meta_run' self.assertEqual(len(scheme_headers), 1) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue('CCPPeq1_var_in_fort_meta_run' in titles) + self.assertTrue("CCPPeq1_var_in_fort_meta_run" in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue('CCPPeq1_var_in_fort_meta' in table_dict) + self.assertTrue("CCPPeq1_var_in_fort_meta" in table_dict) - def test_CCPPgt1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), and IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] - #Exercise + def test_ccpp_gt1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the + subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), + and IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPgt1_var_in_fort_meta.meta")] + # Exercise # Set CCPP directive to > 1 scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp2) - #Verify size of returned list equals number of scheme headers in the test file (1) - # and that header (subroutine) name is 'CCPPgt1_var_in_fort_meta_init' + # Verify size of returned list equals number of scheme headers + # in the test file (1) and that header (subroutine) name is + # 'CCPPgt1_var_in_fort_meta_init' self.assertEqual(len(scheme_headers), 1) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue('CCPPgt1_var_in_fort_meta_init' in titles) + self.assertTrue("CCPPgt1_var_in_fort_meta_init" in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue('CCPPgt1_var_in_fort_meta' in table_dict) + self.assertTrue("CCPPgt1_var_in_fort_meta" in table_dict) - def test_CCPPgt1_var_in_fort_meta2(self): - """Test correct detection of a variable that IS NOT PRESENT the subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), but IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: - parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, variables missing from Fortran scheme.' - in str(context.exception)) - self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, no Fortran variable bar.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + def test_ccpp_gt1_var_in_fort_meta2(self): + """Test correct detection of a variable that + IS NOT PRESENT the subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), + but IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPgt1_var_in_fort_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: + _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ + "variables missing from Fortran scheme." + self.assertTrue(emsg in str(context.exception)) + emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ + "no Fortran variable bar." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_missing_in_meta(self): - """Test correct detection of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), and IS NOT PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: - parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, variables missing from metadata header.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) - self.assertTrue('Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + def test_ccpp_eq1_var_missing_in_meta(self): + """Test correct detection of a variable that + IS PRESENT the subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), + and IS NOT PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_missing_in_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: + _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, "+ \ + "variables missing from metadata header." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize" + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) -# pylint: enable=invalid-name + def test_scheme_ddt_only(self): + """Test correct detection of a "scheme" file which contains only + DDT definitions""" + # Setup + scheme_files = [os.path.join(self._host_files_dir, "ddt2.meta")] + # Exercise + scheme_headers, table_dict = parse_scheme_files(scheme_files, + self._run_env_ccpp) +# with self.assertRaises(CCPPError) as context: +# _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) +# # Verify correct error messages returned -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/unit_tests/test_metadata_table.py b/test/unit_tests/test_metadata_table.py index d9791879..544d3013 100755 --- a/test/unit_tests/test_metadata_table.py +++ b/test/unit_tests/test_metadata_table.py @@ -8,7 +8,7 @@ Command line arguments: none - Usage: python test_metadata_table.py # run the unit tests + Usage: python3 test_metadata_table.py # run the unit tests ----------------------------------------------------------------------- """ import sys From 52e59859341e52bf56546670c6df6af2d9bbec01 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 28 Aug 2023 03:09:14 -0600 Subject: [PATCH 08/49] cleanup and fixes --- scripts/ccpp_capgen.py | 7 +- scripts/ccpp_suite.py | 4 +- scripts/constituents.py | 4 +- scripts/ddt_library.py | 107 ++++++----- scripts/fortran_tools/fortran_write.py | 200 ++++++++++++++++---- scripts/fortran_tools/parse_fortran_file.py | 52 +++-- scripts/host_cap.py | 9 +- scripts/parse_tools/xml_tools.py | 176 +++++++---------- 8 files changed, 338 insertions(+), 221 deletions(-) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 8938a43f..8522f352 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -31,7 +31,7 @@ ## Capture the Framework root _SCRIPT_PATH = os.path.dirname(__file__) -_FRAMEWORK_ROOT = os.path.abspath(os.path.join(__SCRIPT_PATH, os.pardir)) +_FRAMEWORK_ROOT = os.path.abspath(os.path.join(_SCRIPT_PATH, os.pardir)) _SRC_ROOT = os.path.join(_FRAMEWORK_ROOT, "src") ## Init this now so that all Exceptions can be trapped _LOGGER = init_log(os.path.basename(__file__)) @@ -614,7 +614,7 @@ def capgen(run_env): # We always need to parse the ccpp_constituent_prop_ptr_t DDT ##XXgoldyXX: Should this be in framework_env.py? const_prop_mod = os.path.join(src_dir, "ccpp_constituent_prop_mod.meta") - if const_prop_mod and not in scheme_files: + if const_prop_mod not in scheme_files: scheme_files = [const_prop_mod] + scheme_files # end if # Next, parse the scheme files @@ -653,7 +653,8 @@ def capgen(run_env): cap_filenames = ccpp_api.write(outtemp_dir, run_env) if run_env.generate_host_cap: # Create a cap file - host_files = [write_host_cap(host_model, ccpp_api, + cap_module = host_model.ccpp_cap_name() + host_files = [write_host_cap(host_model, ccpp_api, cap_module, outtemp_dir, run_env)] else: host_files = list() diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index d28a026f..cf34d43c 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -843,8 +843,8 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): else: inout_vars[0].add(stdname) # end if - elif intent == 'out' and phase != 'initialize' and constituent - and not const_initialized_in_physics[stdname]: + elif (intent == 'out' and phase != 'initialize' and constituent + and not const_initialized_in_physics[stdname]): # constituents HAVE to be initialized in the init phase because the dycore needs to advect them emsg = "constituent variable '{}' cannot be initialized in the '{}' phase" raise CCPPError(emsg.format(stdname, phase)) diff --git a/scripts/constituents.py b/scripts/constituents.py index 7a60af04..d8935130 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -9,7 +9,7 @@ """ # CCPP framework imports -from parse_tools import ParseInternalError, type_name +from parse_tools import ParseInternalError from metavar import VarDictionary ######################################################################## @@ -406,7 +406,7 @@ def constituent_module_name(self): if not ((self.parent is not None) and hasattr(self.parent.parent, "constituent_module")): emsg = "ConstituentVarDict parent not HostModel?" - emsg += f"\nparent is '{type_name(self.parent.parent)}'" + emsg += f"\nparent is '{type(self.parent.parent)}'" raise ParseInternalError(emsg) # end if return self.parent.parent.constituent_module diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 30614226..118fb3d7 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -44,11 +44,11 @@ def __init__(self, new_field, var_ref, run_env, recur=False): else: # Recurse to find correct (tail) location for self.__field = VarDDT(new_field, var_ref.field, run_env, recur=True) - # End if + # end if if ((not recur) and run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG)): run_env.logger.debug('Adding DDT field, {}'.format(self)) - # End if + # end if def is_ddt(self): """Return True iff is a DDT type.""" @@ -66,18 +66,18 @@ def get_prop_value(self, name): pvalue = super().get_prop_value(name) else: pvalue = self.field.get_prop_value(name) - # End if + # end if return pvalue def intrinsic_elements(self, check_dict=None): """Return the Var intrinsic elements for the leaf Var object. - See Var.intrinsic_elem for details + See Var.intrinsic_elements for details """ if self.field is None: pvalue = super().intrinsic_elements(check_dict=check_dict) else: pvalue = self.field.intrinsic_elements(check_dict=check_dict) - # End if + # end if return pvalue def clone(self, subst_dict, source_name=None, source_type=None, @@ -98,7 +98,7 @@ def clone(self, subst_dict, source_name=None, source_type=None, source_name=source_name, source_type=source_type, context=context) - # End if + # end if return clone_var def call_string(self, var_dict, loop_vars=None): @@ -109,7 +109,7 @@ def call_string(self, var_dict, loop_vars=None): if self.field is not None: call_str += '%' + self.field.call_string(var_dict, loop_vars=loop_vars) - # End if + # end if return call_str def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): @@ -122,7 +122,7 @@ def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): else: self.field.write_def(outfile, indent, ddict, allocatable=allocatable, dummy=dummy) - # End if + # end if @staticmethod def __var_rep(var, prefix=""): @@ -137,14 +137,14 @@ def __var_rep(var, prefix=""): lstr = '{}%{}({})'.format(prefix, lname, ', '.join(ldims)) else: lstr = '{}({})'.format(lname, ', '.join(ldims)) - # End if + # end if else: if prefix: lstr = '{}%{}'.format(prefix, lname) else: lstr = '{}'.format(lname) - # End if - # End if + # end if + # end if return lstr def __repr__(self): @@ -160,9 +160,9 @@ def __repr__(self): elif isinstance(field, Var): lstr = self.__var_rep(field, prefix=lstr) field = None - # End if + # end if sep = '%' - # End while + # end while return "".format(lstr) def __str__(self): @@ -201,33 +201,33 @@ def __init__(self, name, run_env, ddts=None, logger=None): ddts = list() elif not isinstance(ddts, list): ddts = [ddts] - # End if + # end if # Add all the DDT headers, then process for ddt in ddts: if not isinstance(ddt, MetadataSection): errmsg = 'Invalid DDT metadata type, {}' - raise ParseInternalError(errmsg.format(type(ddt))) - # End if + raise ParseInternalError(errmsg.format(type(ddt).__name__)) + # end if if not ddt.header_type == 'ddt': errmsg = 'Metadata table header is for a {}, should be DDT' raise ParseInternalError(errmsg.format(ddt.header_type)) - # End if + # end if if ddt.title in self: errmsg = "Duplicate DDT, {}, found{}, original{}" ctx = context_string(ddt.source.context) octx = context_string(self[ddt.title].source.context) raise CCPPError(errmsg.format(ddt.title, ctx, octx)) - # End if - if logger is not None: - lmsg = 'Adding DDT {} to {}' - logger.debug(lmsg.format(ddt.title, self.name)) - # End if + # end if + if logger and logger.isEnabledFor(logging.DEBUG): + lmsg = f"Adding DDT {ddt.title} to {self.name}" + logger.debug(lmsg) + # end if self[ddt.title] = ddt dlen = len(ddt.module) if dlen > self.__max_mod_name_len: self.__max_mod_name_len = dlen - # End if - # End for + # end if + # end for def check_ddt_type(self, var, header, lname=None): """If is a DDT, check to make sure it is in this DDT library. @@ -239,16 +239,21 @@ def check_ddt_type(self, var, header, lname=None): if vtype not in self: if lname is None: lname = var.get_prop_value('local_name') - # End if + # end if errmsg = 'Variable {} is of unknown type ({}) in {}' ctx = context_string(var.context) raise CCPPError(errmsg.format(lname, vtype, header.title, ctx)) - # End if - # End if (no else needed) + # end if + # end if (no else needed) - def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): + def collect_ddt_fields(self, var_dict, var, run_env, + ddt=None, skip_duplicates=False): """Add all the reachable fields from DDT variable of type, - to . Each field is added as a VarDDT. + to . Each field is added as a VarDDT. + Note: By default, it is an error to try to add a duplicate + field to (i.e., the field already exists in + or one of its parents). To simply skip duplicate + fields, set to True. """ if ddt is None: vtype = var.get_prop_value('type') @@ -259,8 +264,8 @@ def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): ctx = context_string(var.context) errmsg = "Variable, {}, is not a known DDT{}" raise ParseInternalError(errmsg.format(lname, ctx)) - # End if - # End if + # end if + # end if for dvar in ddt.variable_list(): subvar = VarDDT(dvar, var, self.run_env) dvtype = dvar.get_prop_value('type') @@ -268,22 +273,24 @@ def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): # If DDT in our library, we need to add sub-fields recursively. subddt = self[dvtype] self.collect_ddt_fields(var_dict, subvar, run_env, ddt=subddt) - else: - # add_variable only checks the current dictionary. For a - # DDT, the variable also cannot be in our parent dictionaries. - stdname = dvar.get_prop_value('standard_name') - pvar = var_dict.find_variable(standard_name=stdname, - any_scope=True) - if pvar: - emsg = "Attempt to add duplicate DDT sub-variable, {}{}." - emsg += "\nVariable originally defined{}" - ntx = context_string(dvar.context) - ctx = context_string(pvar.context) - raise CCPPError(emsg.format(stdname, ntx, ctx)) - # end if - # Add this intrinsic to + # end if + # add_variable only checks the current dictionary. By default, + # for a DDT, the variable also cannot be in our parent + # dictionaries. + stdname = dvar.get_prop_value('standard_name') + pvar = var_dict.find_variable(standard_name=stdname, any_scope=True) + if pvar and (not skip_duplicates): + emsg = "Attempt to add duplicate DDT sub-variable, {}{}." + emsg += "\nVariable originally defined{}" + ntx = context_string(dvar.context) + ctx = context_string(pvar.context) + raise CCPPError(emsg.format(stdname, ntx, ctx)) + # end if + # Add this intrinsic to + if not pvar: var_dict.add_variable(subvar, run_env) - # End for + # end if + # end for def ddt_modules(self, variable_list, ddt_mods=None): """Collect information for module use statements. @@ -292,14 +299,14 @@ def ddt_modules(self, variable_list, ddt_mods=None): """ if ddt_mods is None: ddt_mods = set() # Need a new set for every call - # End if + # end if for var in variable_list: vtype = var.get_prop_value('type') if vtype in self: module = self[vtype].module ddt_mods.add((module, vtype)) - # End if - # End for + # end if + # end for return ddt_mods def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): @@ -313,7 +320,7 @@ def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): slen = ' '*(pad - len(dmod)) ustring = 'use {},{} only: {}' outfile.write(ustring.format(dmod, slen, dtype), indent) - # End for + # end for @property def name(self): diff --git a/scripts/fortran_tools/fortran_write.py b/scripts/fortran_tools/fortran_write.py index a238dcc5..f9bcfa3f 100644 --- a/scripts/fortran_tools/fortran_write.py +++ b/scripts/fortran_tools/fortran_write.py @@ -4,7 +4,9 @@ """Code to write Fortran code """ -class FortranWriter(object): +import math + +class FortranWriter: """Class to turn output into properly continued and indented Fortran code >>> FortranWriter("foo.F90", 'r', 'test', 'mod_name') #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): @@ -59,9 +61,9 @@ class FortranWriter(object): def indent(self, level=0, continue_line=False): 'Return an indent string for any level' - indent = self._indent * level + indent = self.indent_size * level if continue_line: - indent = indent + self._continue_indent + indent = indent + self.__continue_indent # End if return indent*' ' @@ -71,22 +73,55 @@ def find_best_break(self, choices, last=None): """Find the best line break point given . If is present, use it as a target line length.""" if last is None: - last = self._line_fill + last = self.__line_fill # End if # Find largest good break - possible = [x for x in choices if x < last] + possible = [x for x in choices if 0 < x < last] if not possible: - best = self._line_max + 1 + best = self.__line_max + 1 else: best = max(possible) # End if - if (best > self._line_max) and (last < self._line_max): - best = self.find_best_break(choices, last=self._line_max) + if (best > self.__line_max) and (last < self.__line_max): + best = self.find_best_break(choices, last=self.__line_max) # End if return best ########################################################################### + @staticmethod + def _in_quote(test_str): + """Return True if ends in a character context. + >>> FortranWriter._in_quote("hi'mom") + True + >>> FortranWriter._in_quote("hi mom") + False + >>> FortranWriter._in_quote("'hi mom'") + False + >>> FortranWriter._in_quote("'hi"" mom'") + False + """ + in_single_char = False + in_double_char = False + for char in test_str: + if in_single_char: + if char == "'": + in_single_char = False + # end if + elif in_double_char: + if char == '"': + in_double_char = False + # end if + elif char == "'": + in_single_char = True + elif char == '"': + in_double_char = True + # end if + # end for + return in_single_char or in_double_char + + ########################################################################### + def write(self, statement, indent_level, continue_line=False): """Write to the open file, indenting to (see self.indent). @@ -102,9 +137,18 @@ def write(self, statement, indent_level, continue_line=False): # End for else: istr = self.indent(indent_level, continue_line) - outstr = istr + statement.strip() + ostmt = statement.strip() + is_comment_stmt = ostmt and (ostmt[0] == '!') + in_comment = "" + if ostmt and (ostmt[0] != '&'): + # Skip indent for continue that is in the middle of a + # token or a quoted region + outstr = istr + ostmt + else: + outstr = ostmt + # end if line_len = len(outstr) - if line_len > self._line_fill: + if line_len > self.__line_fill: # Collect pretty break points spaces = list() commas = list() @@ -125,9 +169,14 @@ def write(self, statement, indent_level, continue_line=False): elif outstr[sptr] == '"': in_double_char = True elif outstr[sptr] == '!': - # Comment in non-character context, suck in rest of line + # Comment in non-character context spaces.append(sptr-1) - sptr = line_len - 1 + in_comment = "! " # No continue for comment + if ((not is_comment_stmt) and + (sptr >= self.__max_comment_start)): + # suck in rest of line + sptr = line_len - 1 + # end if elif outstr[sptr] == ' ': # Non-quote spaces are where we can break spaces.append(sptr) @@ -140,31 +189,62 @@ def write(self, statement, indent_level, continue_line=False): # End if (no else, other characters will be ignored) sptr = sptr + 1 # End while + # Before looking for best space, reject any that are on a + # comment line but before any significant characters + if outstr.lstrip()[0] == '!': + first_space = outstr.index('!') + 1 + while ((outstr[first_space] == '!' or + outstr[first_space] == ' ') and + (first_space < line_len)): + first_space += 1 + # end while + if min(spaces) < first_space: + spaces = [x for x in spaces if x >= first_space] + # end if best = self.find_best_break(spaces) - if best >= self._line_fill: - best = self.find_best_break(commas) + if best >= self.__line_fill: + best = min(best, self.find_best_break(commas)) # End if - if best > self._line_max: - # This is probably a bad situation that might not - # compile, just write the line and hope for the best. - line_continue = False - elif len(outstr) > best: - # If next line is just comment, do not use continue - # NB: Is this a Fortran issue or just a gfortran issue? - line_continue = outstr[best+1:].lstrip()[0] != '!' - else: - line_continue = True + line_continue = False + if best >= self.__line_max: + # This is probably a bad situation so we have to break + # in an ugly spot + best = self.__line_max - 1 + if len(outstr) > best: + line_continue = '&' + # end if + # end if + if len(outstr) > best: + if self._in_quote(outstr[0:best+1]): + line_continue = '&' + else: + # If next line is just comment, do not use continue + line_continue = outstr[best+1:].lstrip()[0] != '!' + # end if + elif not line_continue: + line_continue = len(outstr) > best # End if + if in_comment or is_comment_stmt: + line_continue = False + # end if if line_continue: - fill = "{}&".format((self._line_fill - best)*' ') + fill = "{}&".format((self.__line_fill - best)*' ') else: - fill = '' + fill = "" # End if - self._file.write("{}{}\n".format(outstr[0:best+1], fill)) - statement = outstr[best+1:] + outline = f"{outstr[0:best+1]}{fill}".rstrip() + self.__file.write(f"{outline}\n") + if best <= 0: + imsg = "Internal ERROR: Unable to break line" + raise ValueError(f"{imsg}, '{statement}'") + # end if + statement = in_comment + outstr[best+1:] + if isinstance(line_continue, str) and statement: + statement = line_continue + statement + # end if self.write(statement, indent_level, continue_line=line_continue) else: - self._file.write("{}\n".format(outstr)) + self.__file.write("{}\n".format(outstr)) # End if # End if @@ -175,7 +255,7 @@ def __init__(self, filename, mode, file_description, module_name, line_fill=None, line_max=None): """Initialize thie FortranWriter object. Some boilerplate is written automatically.""" - self.__file_desc = file_description + self.__file_desc = file_description.replace('\n', '\n!! ') self.__module = module_name # We only handle writing situations (for now) and only text if 'r' in mode: @@ -184,26 +264,27 @@ def __init__(self, filename, mode, file_description, module_name, if 'b' in mode: raise ValueError('Binary mode not allowed in FortranWriter object') # End if - self._file = open(filename, mode) + self.__file = open(filename, mode) if indent is None: - self._indent = FortranWriter.__INDENT + self.__indent = FortranWriter.__INDENT else: - self._indent = indent + self.__indent = indent # End if if continue_indent is None: - self._continue_indent = FortranWriter.__CONTINUE_INDENT + self.__continue_indent = FortranWriter.__CONTINUE_INDENT else: - self._continue_indent = continue_indent + self.__continue_indent = continue_indent # End if if line_fill is None: - self._line_fill = FortranWriter.__LINE_FILL + self.__line_fill = FortranWriter.__LINE_FILL else: - self._line_fill = line_fill + self.__line_fill = line_fill # End if + self.__max_comment_start = math.ceil(self.__line_fill * 3 / 4) if line_max is None: - self._line_max = FortranWriter.__LINE_MAX + self.__line_max = FortranWriter.__LINE_MAX else: - self._line_max = line_max + self.__line_max = line_max # End if ########################################################################### @@ -234,7 +315,7 @@ def __enter__(self, *args): def __exit__(self, *args): self.write(FortranWriter.__MOD_FOOTER.format(module=self.__module), 0) - self._file.close() + self.__file.close() return False ########################################################################### @@ -247,6 +328,45 @@ def module_header(self): ########################################################################### + def comment(self, comment, indent): + """Write a Fortran comment with contents, """ + mlcomment = comment.replace('\n', '\n! ') # No backslash in f string + self.write(f"! {mlcomment}", indent) + + ########################################################################### + + def blank_line(self): + """Write a blank line""" + self.write("", 0) + + ########################################################################### + + def include(self, filename): + """Insert the contents of verbatim.""" + with open(filename, 'r') as infile: + for line in infile: + self.__file.write(line) + # end for + # end with + + ########################################################################### + + @property + def line_fill(self): + """Return the target line length for this Fortran file""" + return self.__line_fill + + ########################################################################### + + @property + def indent_size(self): + """Return the number of spaces for each indent level for this + Fortran file + """ + return self.__indent + + ########################################################################### + @classmethod def copyright(cls): """Return the standard Fortran file copyright string""" diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index f37b3377..7c6493e7 100755 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,29 +563,39 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): module=spec_name, var_dict=var_dict) mheaders.append(mheader) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if (run_env.logger and + run_env.logger.isEnabledFor(logging.DEBUG)): ctx = context_string(pobj, nodir=True) msg = 'Adding header {}{}' run_env.logger.debug(msg.format(mheader.table_name, ctx)) + # end if break - elif ((type_def is not None) and (active_table is not None) and - (type_def[0].lower() == active_table.lower())): + elif type_def is not None: # Put statement back so caller knows where we are statements.insert(0, statement) - statements, ddt = parse_type_def(statements, type_def, - spec_name, pobj, run_env) - if ddt is None: - ctx = context_string(pobj, nodir=True) - msg = "No DDT found at '{}'{}" - raise CCPPError(msg.format(statement, ctx)) - # End if - mheaders.append(ddt) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): - ctx = context_string(pobj, nodir=True) - msg = 'Adding DDT {}{}' - run_env.logger.debug(msg.format(ddt.table_name, ctx)) - # End if - active_table = None + if ((active_table is not None) and + (type_def[0].lower() == active_table.lower())): + statements, ddt = parse_type_def(statements, type_def, + spec_name, pobj, run_env) + if ddt is None: + ctx = context_string(pobj, nodir=True) + msg = "No DDT found at '{}'{}" + raise CCPPError(msg.format(statement, ctx)) + # End if + mheaders.append(ddt) + if (run_env.logger and + run_env.logger.isEnabledFor(logging.DEBUG)): + ctx = context_string(pobj, nodir=True) + msg = 'Adding DDT {}{}' + run_env.logger.debug(msg.format(ddt.table_name, ctx)) + # End if + active_table = None + else: + # We found a type definition but it is not one with + # metadata. Just parse it and throw away what is found. + _ = parse_type_def(statements, type_def, + spec_name, pobj, run_env) + # end if elif active_table is not None: # We should have a variable definition to add if ((not is_comment_statement(statement)) and @@ -786,6 +796,7 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End program or module pmatch = endmatch.match(statement) asmatch = _ARG_TABLE_START_RE.match(statement) + type_def = fortran_type_definition(statement) if pmatch is not None: # We never found a contains statement inspec = False @@ -812,6 +823,13 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End if inspec = pobj.in_region('MODULE', region_name=mod_name) break + elif type_def: + # We have a type definition without metadata + # Just parse it and throw away what is found. + # Put statement back so caller knows where we are + statements.insert(0, statement) + _ = parse_type_def(statements, type_def, + spec_name, pobj, run_env) elif is_contains_statement(statement, inmod): inspec = False break diff --git a/scripts/host_cap.py b/scripts/host_cap.py index d833c649..479f688b 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -8,7 +8,7 @@ import logging import os # CCPP framework imports -from ccpp_suite import API, API_SOURCE_NAME +from ccpp_suite import API from ccpp_state_machine import CCPP_STATE_MACH from constituents import ConstituentVarDict, CONST_DDT_NAME, CONST_DDT_MOD from constituents import CONST_OBJ_STDNAME @@ -33,7 +33,8 @@ end subroutine {host_model}_ccpp_physics_{stage} ''' -_API_SOURCE = ParseSource(API_SOURCE_NAME, "MODULE", +_API_SRC_NAME = "CCPP_API" +_API_SOURCE = ParseSource(_API_SRC_NAME, "MODULE", ParseContext(filename="host_cap.F90")) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', @@ -59,7 +60,7 @@ 'suites':''}) # Used to prevent loop substitution lookups -_BLANK_DICT = VarDictionary(API_SOURCE_NAME, _MVAR_DUMMY_RUN_ENV) +_BLANK_DICT = VarDictionary(_API_SRC_NAME, _MVAR_DUMMY_RUN_ENV) ############################################################################### def suite_part_list(suite, stage): @@ -509,7 +510,7 @@ def write_host_cap(host_model, api, module_name, output_dir, run_env): subst_dict['intent'] = 'inout' # End if hdvars.append(hvar.clone(subst_dict, - source_name=API_SOURCE_NAME)) + source_name=_API_SRC_NAME)) # End for lnames = [x.get_prop_value('local_name') for x in apivars + hdvars] api_vlist = ", ".join(lnames) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 414ffc5a..07074935 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -7,35 +7,35 @@ # Python library imports from __future__ import print_function import os -import os.path import re +import shutil import subprocess import sys import xml.etree.ElementTree as ET sys.path.insert(0, os.path.dirname(__file__)) -# pylint: disable=wrong-import-position -try: - from distutils.spawn import find_executable - _XMLLINT = find_executable('xmllint') -except ImportError: - _XMLLINT = None -# End try # CCPP framework imports from parse_source import CCPPError from parse_log import init_log, set_log_to_null -# pylint: enable=wrong-import-position # Global data _INDENT_STR = " " +_XMLLINT = shutil.which('xmllint') # Blank if not installed beg_tag_re = re.compile(r"([<][^/][^<>]*[^/][>])") end_tag_re = re.compile(r"([<][/][^<>/]+[>])") simple_tag_re = re.compile(r"([<][^/][^<>/]+[/][>])") # Find python version -PY3 = sys.version_info[0] > 2 PYSUBVER = sys.version_info[1] _LOGGER = None +############################################################################### +class XMLToolsInternalError(ValueError): +############################################################################### + """Error class for reporting internal errors""" + def __init__(self, message): + """Initialize this exception""" + super().__init__(message) + ############################################################################### def call_command(commands, logger, silent=False): ############################################################################### @@ -53,35 +53,12 @@ def call_command(commands, logger, silent=False): result = False outstr = '' try: - if PY3: - if PYSUBVER > 6: - cproc = subprocess.run(commands, check=True, - capture_output=True) - if not silent: - logger.debug(cproc.stdout) - # End if - result = cproc.returncode == 0 - elif PYSUBVER >= 5: - cproc = subprocess.run(commands, check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if not silent: - logger.debug(cproc.stdout) - # End if - result = cproc.returncode == 0 - else: - raise ValueError("Python 3 must be at least version 3.5") - # End if - else: - pproc = subprocess.Popen(commands, stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, _ = pproc.communicate() - if not silent: - logger.debug(output) - # End if - result = pproc.returncode == 0 - # End if + cproc = subprocess.run(commands, check=True, + capture_output=True) + if not silent: + logger.debug(cproc.stdout) + # end if + result = cproc.returncode == 0 except (OSError, CCPPError, subprocess.CalledProcessError) as err: if silent: result = False @@ -90,9 +67,9 @@ def call_command(commands, logger, silent=False): emsg = "Execution of '{}' failed with code:\n" outstr = emsg.format(cmd, err.returncode) outstr += "{}".format(err.output) - raise CCPPError(outstr) - # End if - # End of try + raise CCPPError(outstr) from err + # end if + # end of try return result ############################################################################### @@ -102,6 +79,8 @@ def find_schema_version(root): Find the version of the host registry file represented by root >>> find_schema_version(ET.fromstring('')) [1, 0] + >>> find_schema_version(ET.fromstring('')) + [2, 0] >>> find_schema_version(ET.fromstring('')) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): CCPPError: Illegal version string, '1.a' @@ -118,33 +97,33 @@ def find_schema_version(root): verbits = None if 'version' not in root.attrib: raise CCPPError("version attribute required") - # End if + # end if version = root.attrib['version'] versplit = version.split('.') try: if len(versplit) != 2: raise CCPPError('oops') - # End if (no else needed) + # end if (no else needed) try: verbits = [int(x) for x in versplit] except ValueError as verr: - raise CCPPError(verr) - # End try + raise CCPPError(verr) from verr + # end try if verbits[0] < 1: raise CCPPError('Major version must be at least 1') - # End if + # end if if verbits[1] < 0: raise CCPPError('Minor version must be non-negative') - # End if + # end if except CCPPError as verr: errstr = """Illegal version string, '{}' Format must be .""" ve_str = str(verr) if ve_str: errstr = ve_str + '\n' + errstr - # End if - raise CCPPError(errstr.format(version)) - # End try + # end if + raise CCPPError(errstr.format(version)) from verr + # end try return verbits ############################################################################### @@ -161,10 +140,10 @@ def find_schema_file(schema_root, version, schema_path=None): schema_file = os.path.join(schema_path, schema_filename) else: schema_file = schema_filename - # End if + # end if if os.path.exists(schema_file): return schema_file - # End if + # end if return None ############################################################################### @@ -178,38 +157,43 @@ def validate_xml_file(filename, schema_root, version, logger, # Check the filename if not os.path.isfile(filename): raise CCPPError("validate_xml_file: Filename, '{}', does not exist".format(filename)) - # End if + # end if if not os.access(filename, os.R_OK): raise CCPPError("validate_xml_file: Cannot open '{}'".format(filename)) - # End if - if not schema_path: - # Find the schema, based on the model version - thispath = os.path.abspath(__file__) - pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) - schema_path = os.path.join(pdir, 'schema') - # End if - schema_file = find_schema_file(schema_root, version, schema_path) - if not (schema_file and os.path.isfile(schema_file)): - verstring = '.'.join([str(x) for x in version]) - emsg = """validate_xml_file: Cannot find schema for version {}, - {} does not exist""" - raise CCPPError(emsg.format(verstring, schema_file)) - # End if + # end if + if os.path.isfile(schema_root): + # We already have a file, just use it + schema_file = schema_root + else: + if not schema_path: + # Find the schema, based on the model version + thispath = os.path.abspath(__file__) + pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) + schema_path = os.path.join(pdir, 'schema') + # end if + schema_file = find_schema_file(schema_root, version, schema_path) + if not (schema_file and os.path.isfile(schema_file)): + verstring = '.'.join([str(x) for x in version]) + emsg = """validate_xml_file: Cannot find schema for version {}, + {} does not exist""" + raise CCPPError(emsg.format(verstring, schema_file)) + # end if + # end if if not os.access(schema_file, os.R_OK): emsg = "validate_xml_file: Cannot open schema, '{}'" raise CCPPError(emsg.format(schema_file)) - # End if - if _XMLLINT is not None: + # end if + if _XMLLINT: logger.debug("Checking file {} against schema {}".format(filename, schema_file)) cmd = [_XMLLINT, '--noout', '--schema', schema_file, filename] result = call_command(cmd, logger) return result - # End if + # end if lmsg = "xmllint not found, could not validate file {}" if error_on_noxmllint: raise CCPPError("validate_xml_file: " + lmsg.format(filename)) - # End if + # end if logger.warning(lmsg.format(filename)) return True # We could not check but still need to proceed @@ -218,27 +202,23 @@ def read_xml_file(filename, logger=None): ############################################################################### """Read the XML file, , and return its tree and root""" if os.path.isfile(filename) and os.access(filename, os.R_OK): - if PY3: - file_open = (lambda x: open(x, 'r', encoding='utf-8')) - else: - file_open = (lambda x: open(x, 'r')) - # End if + file_open = (lambda x: open(x, 'r', encoding='utf-8')) with file_open(filename) as file_: try: tree = ET.parse(file_) root = tree.getroot() except ET.ParseError as perr: emsg = "read_xml_file: Cannot read {}, {}" - raise CCPPError(emsg.format(filename, perr)) + raise CCPPError(emsg.format(filename, perr)) from perr elif not os.access(filename, os.R_OK): raise CCPPError("read_xml_file: Cannot open '{}'".format(filename)) else: emsg = "read_xml_file: Filename, '{}', does not exist" raise CCPPError(emsg.format(filename)) - # End if + # end if if logger: logger.debug("Read XML file, '{}'".format(filename)) - # End if + # end if return tree, root ############################################################################### @@ -248,7 +228,7 @@ class PrettyElementTree(ET.ElementTree): def __init__(self, element=None, file=None): """Initialize a PrettyElementTree object""" - super(PrettyElementTree, self).__init__(element, file) + super().__init__(element, file) def _write(self, outfile, line, indent, eol=os.linesep): """Write as an ASCII string to """ @@ -268,35 +248,25 @@ def _inc_pos(outstr, text, txt_beg): # end if emsg = "No output at {} of {}\n{}".format(txt_beg, len(text), text[txt_beg:txt_end]) - raise DatatableInternalError(emsg) + raise XMLToolsInternalError(emsg) def write(self, file, encoding="us-ascii", xml_declaration=None, default_namespace=None, method="xml", short_empty_elements=True): """Subclassed write method to format output.""" - if PY3 and (PYSUBVER >= 4): - if PYSUBVER >= 8: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method, - xml_declaration=xml_declaration, - default_namespace=default_namespace, - short_empty_elements=short_empty_elements) - else: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method, - short_empty_elements=short_empty_elements) - # end if - else: - input = ET.tostring(self.getroot(), - encoding=encoding, method=method) - # end if - if PY3: - fmode = 'wt' - root = str(input, encoding="utf-8") + if PYSUBVER >= 8: + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method, + xml_declaration=xml_declaration, + default_namespace=default_namespace, + short_empty_elements=short_empty_elements) else: - fmode = 'w' - root = input + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method, + short_empty_elements=short_empty_elements) # end if + fmode = 'wt' + root = str(et_str, encoding="utf-8") indent = 0 last_write_text = False with open(file, fmode) as outfile: From 3d75c77a5f6404a8b8500ac73217ad08e908f1b9 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 28 Aug 2023 03:18:40 -0600 Subject: [PATCH 09/49] cleanup and fixes --- scripts/constituents.py | 2 +- scripts/ddt_library.py | 104 +++++----- scripts/fortran_tools/fortran_write.py | 200 ++++---------------- scripts/fortran_tools/parse_fortran_file.py | 52 ++--- scripts/host_cap.py | 10 +- scripts/parse_tools/xml_tools.py | 158 +++++++++------- 6 files changed, 197 insertions(+), 329 deletions(-) diff --git a/scripts/constituents.py b/scripts/constituents.py index be8d4774..659f997f 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -398,7 +398,7 @@ def constituent_module_name(self): if not ((self.parent is not None) and hasattr(self.parent.parent, "constituent_module")): emsg = "ConstituentVarDict parent not HostModel?" - emsg += "\nparent is '{}'".format(type(self.parent.parent)) + emsg += f"\nparent is '{type_name(self.parent.parent)}'" raise ParseInternalError(emsg) # end if return self.parent.parent.constituent_module diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 4be53963..30614226 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -44,11 +44,11 @@ def __init__(self, new_field, var_ref, run_env, recur=False): else: # Recurse to find correct (tail) location for self.__field = VarDDT(new_field, var_ref.field, run_env, recur=True) - # end if + # End if if ((not recur) and run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG)): run_env.logger.debug('Adding DDT field, {}'.format(self)) - # end if + # End if def is_ddt(self): """Return True iff is a DDT type.""" @@ -66,18 +66,18 @@ def get_prop_value(self, name): pvalue = super().get_prop_value(name) else: pvalue = self.field.get_prop_value(name) - # end if + # End if return pvalue def intrinsic_elements(self, check_dict=None): """Return the Var intrinsic elements for the leaf Var object. - See Var.intrinsic_elements for details + See Var.intrinsic_elem for details """ if self.field is None: pvalue = super().intrinsic_elements(check_dict=check_dict) else: pvalue = self.field.intrinsic_elements(check_dict=check_dict) - # end if + # End if return pvalue def clone(self, subst_dict, source_name=None, source_type=None, @@ -98,7 +98,7 @@ def clone(self, subst_dict, source_name=None, source_type=None, source_name=source_name, source_type=source_type, context=context) - # end if + # End if return clone_var def call_string(self, var_dict, loop_vars=None): @@ -109,7 +109,7 @@ def call_string(self, var_dict, loop_vars=None): if self.field is not None: call_str += '%' + self.field.call_string(var_dict, loop_vars=loop_vars) - # end if + # End if return call_str def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): @@ -122,7 +122,7 @@ def write_def(self, outfile, indent, ddict, allocatable=False, dummy=False): else: self.field.write_def(outfile, indent, ddict, allocatable=allocatable, dummy=dummy) - # end if + # End if @staticmethod def __var_rep(var, prefix=""): @@ -137,14 +137,14 @@ def __var_rep(var, prefix=""): lstr = '{}%{}({})'.format(prefix, lname, ', '.join(ldims)) else: lstr = '{}({})'.format(lname, ', '.join(ldims)) - # end if + # End if else: if prefix: lstr = '{}%{}'.format(prefix, lname) else: lstr = '{}'.format(lname) - # end if - # end if + # End if + # End if return lstr def __repr__(self): @@ -160,9 +160,9 @@ def __repr__(self): elif isinstance(field, Var): lstr = self.__var_rep(field, prefix=lstr) field = None - # end if + # End if sep = '%' - # end while + # End while return "".format(lstr) def __str__(self): @@ -201,33 +201,33 @@ def __init__(self, name, run_env, ddts=None, logger=None): ddts = list() elif not isinstance(ddts, list): ddts = [ddts] - # end if + # End if # Add all the DDT headers, then process for ddt in ddts: if not isinstance(ddt, MetadataSection): errmsg = 'Invalid DDT metadata type, {}' - raise ParseInternalError(errmsg.format(type(ddt).__name__)) - # end if + raise ParseInternalError(errmsg.format(type(ddt))) + # End if if not ddt.header_type == 'ddt': errmsg = 'Metadata table header is for a {}, should be DDT' raise ParseInternalError(errmsg.format(ddt.header_type)) - # end if + # End if if ddt.title in self: errmsg = "Duplicate DDT, {}, found{}, original{}" ctx = context_string(ddt.source.context) octx = context_string(self[ddt.title].source.context) raise CCPPError(errmsg.format(ddt.title, ctx, octx)) - # end if - if logger and logger.isEnabledFor(logging.DEBUG): - lmsg = f"Adding DDT {ddt.title} to {self.name}" - logger.debug(lmsg) - # end if + # End if + if logger is not None: + lmsg = 'Adding DDT {} to {}' + logger.debug(lmsg.format(ddt.title, self.name)) + # End if self[ddt.title] = ddt dlen = len(ddt.module) if dlen > self.__max_mod_name_len: self.__max_mod_name_len = dlen - # end if - # end for + # End if + # End for def check_ddt_type(self, var, header, lname=None): """If is a DDT, check to make sure it is in this DDT library. @@ -239,21 +239,16 @@ def check_ddt_type(self, var, header, lname=None): if vtype not in self: if lname is None: lname = var.get_prop_value('local_name') - # end if + # End if errmsg = 'Variable {} is of unknown type ({}) in {}' ctx = context_string(var.context) raise CCPPError(errmsg.format(lname, vtype, header.title, ctx)) - # end if - # end if (no else needed) + # End if + # End if (no else needed) - def collect_ddt_fields(self, var_dict, var, run_env, - ddt=None, skip_duplicates=False): + def collect_ddt_fields(self, var_dict, var, run_env, ddt=None): """Add all the reachable fields from DDT variable of type, to . Each field is added as a VarDDT. - Note: By default, it is an error to try to add a duplicate - field to (i.e., the field already exists in - or one of its parents). To simply skip duplicate - fields, set to True. """ if ddt is None: vtype = var.get_prop_value('type') @@ -264,8 +259,8 @@ def collect_ddt_fields(self, var_dict, var, run_env, ctx = context_string(var.context) errmsg = "Variable, {}, is not a known DDT{}" raise ParseInternalError(errmsg.format(lname, ctx)) - # end if - # end if + # End if + # End if for dvar in ddt.variable_list(): subvar = VarDDT(dvar, var, self.run_env) dvtype = dvar.get_prop_value('type') @@ -273,25 +268,22 @@ def collect_ddt_fields(self, var_dict, var, run_env, # If DDT in our library, we need to add sub-fields recursively. subddt = self[dvtype] self.collect_ddt_fields(var_dict, subvar, run_env, ddt=subddt) - # end if - # add_variable only checks the current dictionary. By default, - # for a DDT, the variable also cannot be in our parent - # dictionaries. - stdname = dvar.get_prop_value('standard_name') - pvar = var_dict.find_variable(standard_name=stdname, + else: + # add_variable only checks the current dictionary. For a + # DDT, the variable also cannot be in our parent dictionaries. + stdname = dvar.get_prop_value('standard_name') + pvar = var_dict.find_variable(standard_name=stdname, any_scope=True) - if pvar and (not skip_duplicates): - emsg = "Attempt to add duplicate DDT sub-variable, {}{}." - emsg += "\nVariable originally defined{}" - ntx = context_string(dvar.context) - ctx = context_string(pvar.context) - raise CCPPError(emsg.format(stdname, ntx, ctx)) - # end if - # Add this intrinsic to - if not pvar: + if pvar: + emsg = "Attempt to add duplicate DDT sub-variable, {}{}." + emsg += "\nVariable originally defined{}" + ntx = context_string(dvar.context) + ctx = context_string(pvar.context) + raise CCPPError(emsg.format(stdname, ntx, ctx)) + # end if + # Add this intrinsic to var_dict.add_variable(subvar, run_env) - # end if - # end for + # End for def ddt_modules(self, variable_list, ddt_mods=None): """Collect information for module use statements. @@ -300,14 +292,14 @@ def ddt_modules(self, variable_list, ddt_mods=None): """ if ddt_mods is None: ddt_mods = set() # Need a new set for every call - # end if + # End if for var in variable_list: vtype = var.get_prop_value('type') if vtype in self: module = self[vtype].module ddt_mods.add((module, vtype)) - # end if - # end for + # End if + # End for return ddt_mods def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): @@ -321,7 +313,7 @@ def write_ddt_use_statements(self, variable_list, outfile, indent, pad=0): slen = ' '*(pad - len(dmod)) ustring = 'use {},{} only: {}' outfile.write(ustring.format(dmod, slen, dtype), indent) - # end for + # End for @property def name(self): diff --git a/scripts/fortran_tools/fortran_write.py b/scripts/fortran_tools/fortran_write.py index f9bcfa3f..a238dcc5 100644 --- a/scripts/fortran_tools/fortran_write.py +++ b/scripts/fortran_tools/fortran_write.py @@ -4,9 +4,7 @@ """Code to write Fortran code """ -import math - -class FortranWriter: +class FortranWriter(object): """Class to turn output into properly continued and indented Fortran code >>> FortranWriter("foo.F90", 'r', 'test', 'mod_name') #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): @@ -61,9 +59,9 @@ class FortranWriter: def indent(self, level=0, continue_line=False): 'Return an indent string for any level' - indent = self.indent_size * level + indent = self._indent * level if continue_line: - indent = indent + self.__continue_indent + indent = indent + self._continue_indent # End if return indent*' ' @@ -73,55 +71,22 @@ def find_best_break(self, choices, last=None): """Find the best line break point given . If is present, use it as a target line length.""" if last is None: - last = self.__line_fill + last = self._line_fill # End if # Find largest good break - possible = [x for x in choices if 0 < x < last] + possible = [x for x in choices if x < last] if not possible: - best = self.__line_max + 1 + best = self._line_max + 1 else: best = max(possible) # End if - if (best > self.__line_max) and (last < self.__line_max): - best = self.find_best_break(choices, last=self.__line_max) + if (best > self._line_max) and (last < self._line_max): + best = self.find_best_break(choices, last=self._line_max) # End if return best ########################################################################### - @staticmethod - def _in_quote(test_str): - """Return True if ends in a character context. - >>> FortranWriter._in_quote("hi'mom") - True - >>> FortranWriter._in_quote("hi mom") - False - >>> FortranWriter._in_quote("'hi mom'") - False - >>> FortranWriter._in_quote("'hi"" mom'") - False - """ - in_single_char = False - in_double_char = False - for char in test_str: - if in_single_char: - if char == "'": - in_single_char = False - # end if - elif in_double_char: - if char == '"': - in_double_char = False - # end if - elif char == "'": - in_single_char = True - elif char == '"': - in_double_char = True - # end if - # end for - return in_single_char or in_double_char - - ########################################################################### - def write(self, statement, indent_level, continue_line=False): """Write to the open file, indenting to (see self.indent). @@ -137,18 +102,9 @@ def write(self, statement, indent_level, continue_line=False): # End for else: istr = self.indent(indent_level, continue_line) - ostmt = statement.strip() - is_comment_stmt = ostmt and (ostmt[0] == '!') - in_comment = "" - if ostmt and (ostmt[0] != '&'): - # Skip indent for continue that is in the middle of a - # token or a quoted region - outstr = istr + ostmt - else: - outstr = ostmt - # end if + outstr = istr + statement.strip() line_len = len(outstr) - if line_len > self.__line_fill: + if line_len > self._line_fill: # Collect pretty break points spaces = list() commas = list() @@ -169,14 +125,9 @@ def write(self, statement, indent_level, continue_line=False): elif outstr[sptr] == '"': in_double_char = True elif outstr[sptr] == '!': - # Comment in non-character context + # Comment in non-character context, suck in rest of line spaces.append(sptr-1) - in_comment = "! " # No continue for comment - if ((not is_comment_stmt) and - (sptr >= self.__max_comment_start)): - # suck in rest of line - sptr = line_len - 1 - # end if + sptr = line_len - 1 elif outstr[sptr] == ' ': # Non-quote spaces are where we can break spaces.append(sptr) @@ -189,62 +140,31 @@ def write(self, statement, indent_level, continue_line=False): # End if (no else, other characters will be ignored) sptr = sptr + 1 # End while - # Before looking for best space, reject any that are on a - # comment line but before any significant characters - if outstr.lstrip()[0] == '!': - first_space = outstr.index('!') + 1 - while ((outstr[first_space] == '!' or - outstr[first_space] == ' ') and - (first_space < line_len)): - first_space += 1 - # end while - if min(spaces) < first_space: - spaces = [x for x in spaces if x >= first_space] - # end if best = self.find_best_break(spaces) - if best >= self.__line_fill: - best = min(best, self.find_best_break(commas)) + if best >= self._line_fill: + best = self.find_best_break(commas) # End if - line_continue = False - if best >= self.__line_max: - # This is probably a bad situation so we have to break - # in an ugly spot - best = self.__line_max - 1 - if len(outstr) > best: - line_continue = '&' - # end if - # end if - if len(outstr) > best: - if self._in_quote(outstr[0:best+1]): - line_continue = '&' - else: - # If next line is just comment, do not use continue - line_continue = outstr[best+1:].lstrip()[0] != '!' - # end if - elif not line_continue: - line_continue = len(outstr) > best - # End if - if in_comment or is_comment_stmt: + if best > self._line_max: + # This is probably a bad situation that might not + # compile, just write the line and hope for the best. line_continue = False - # end if + elif len(outstr) > best: + # If next line is just comment, do not use continue + # NB: Is this a Fortran issue or just a gfortran issue? + line_continue = outstr[best+1:].lstrip()[0] != '!' + else: + line_continue = True + # End if if line_continue: - fill = "{}&".format((self.__line_fill - best)*' ') + fill = "{}&".format((self._line_fill - best)*' ') else: - fill = "" + fill = '' # End if - outline = f"{outstr[0:best+1]}{fill}".rstrip() - self.__file.write(f"{outline}\n") - if best <= 0: - imsg = "Internal ERROR: Unable to break line" - raise ValueError(f"{imsg}, '{statement}'") - # end if - statement = in_comment + outstr[best+1:] - if isinstance(line_continue, str) and statement: - statement = line_continue + statement - # end if + self._file.write("{}{}\n".format(outstr[0:best+1], fill)) + statement = outstr[best+1:] self.write(statement, indent_level, continue_line=line_continue) else: - self.__file.write("{}\n".format(outstr)) + self._file.write("{}\n".format(outstr)) # End if # End if @@ -255,7 +175,7 @@ def __init__(self, filename, mode, file_description, module_name, line_fill=None, line_max=None): """Initialize thie FortranWriter object. Some boilerplate is written automatically.""" - self.__file_desc = file_description.replace('\n', '\n!! ') + self.__file_desc = file_description self.__module = module_name # We only handle writing situations (for now) and only text if 'r' in mode: @@ -264,27 +184,26 @@ def __init__(self, filename, mode, file_description, module_name, if 'b' in mode: raise ValueError('Binary mode not allowed in FortranWriter object') # End if - self.__file = open(filename, mode) + self._file = open(filename, mode) if indent is None: - self.__indent = FortranWriter.__INDENT + self._indent = FortranWriter.__INDENT else: - self.__indent = indent + self._indent = indent # End if if continue_indent is None: - self.__continue_indent = FortranWriter.__CONTINUE_INDENT + self._continue_indent = FortranWriter.__CONTINUE_INDENT else: - self.__continue_indent = continue_indent + self._continue_indent = continue_indent # End if if line_fill is None: - self.__line_fill = FortranWriter.__LINE_FILL + self._line_fill = FortranWriter.__LINE_FILL else: - self.__line_fill = line_fill + self._line_fill = line_fill # End if - self.__max_comment_start = math.ceil(self.__line_fill * 3 / 4) if line_max is None: - self.__line_max = FortranWriter.__LINE_MAX + self._line_max = FortranWriter.__LINE_MAX else: - self.__line_max = line_max + self._line_max = line_max # End if ########################################################################### @@ -315,7 +234,7 @@ def __enter__(self, *args): def __exit__(self, *args): self.write(FortranWriter.__MOD_FOOTER.format(module=self.__module), 0) - self.__file.close() + self._file.close() return False ########################################################################### @@ -328,45 +247,6 @@ def module_header(self): ########################################################################### - def comment(self, comment, indent): - """Write a Fortran comment with contents, """ - mlcomment = comment.replace('\n', '\n! ') # No backslash in f string - self.write(f"! {mlcomment}", indent) - - ########################################################################### - - def blank_line(self): - """Write a blank line""" - self.write("", 0) - - ########################################################################### - - def include(self, filename): - """Insert the contents of verbatim.""" - with open(filename, 'r') as infile: - for line in infile: - self.__file.write(line) - # end for - # end with - - ########################################################################### - - @property - def line_fill(self): - """Return the target line length for this Fortran file""" - return self.__line_fill - - ########################################################################### - - @property - def indent_size(self): - """Return the number of spaces for each indent level for this - Fortran file - """ - return self.__indent - - ########################################################################### - @classmethod def copyright(cls): """Return the standard Fortran file copyright string""" diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index 7c6493e7..f37b3377 100755 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,39 +563,29 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): module=spec_name, var_dict=var_dict) mheaders.append(mheader) - if (run_env.logger and - run_env.logger.isEnabledFor(logging.DEBUG)): + if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): ctx = context_string(pobj, nodir=True) msg = 'Adding header {}{}' run_env.logger.debug(msg.format(mheader.table_name, ctx)) - # end if break - elif type_def is not None: + elif ((type_def is not None) and (active_table is not None) and + (type_def[0].lower() == active_table.lower())): # Put statement back so caller knows where we are statements.insert(0, statement) - if ((active_table is not None) and - (type_def[0].lower() == active_table.lower())): - statements, ddt = parse_type_def(statements, type_def, - spec_name, pobj, run_env) - if ddt is None: - ctx = context_string(pobj, nodir=True) - msg = "No DDT found at '{}'{}" - raise CCPPError(msg.format(statement, ctx)) - # End if - mheaders.append(ddt) - if (run_env.logger and - run_env.logger.isEnabledFor(logging.DEBUG)): - ctx = context_string(pobj, nodir=True) - msg = 'Adding DDT {}{}' - run_env.logger.debug(msg.format(ddt.table_name, ctx)) - # End if - active_table = None - else: - # We found a type definition but it is not one with - # metadata. Just parse it and throw away what is found. - _ = parse_type_def(statements, type_def, - spec_name, pobj, run_env) - # end if + statements, ddt = parse_type_def(statements, type_def, + spec_name, pobj, run_env) + if ddt is None: + ctx = context_string(pobj, nodir=True) + msg = "No DDT found at '{}'{}" + raise CCPPError(msg.format(statement, ctx)) + # End if + mheaders.append(ddt) + if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + ctx = context_string(pobj, nodir=True) + msg = 'Adding DDT {}{}' + run_env.logger.debug(msg.format(ddt.table_name, ctx)) + # End if + active_table = None elif active_table is not None: # We should have a variable definition to add if ((not is_comment_statement(statement)) and @@ -796,7 +786,6 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End program or module pmatch = endmatch.match(statement) asmatch = _ARG_TABLE_START_RE.match(statement) - type_def = fortran_type_definition(statement) if pmatch is not None: # We never found a contains statement inspec = False @@ -823,13 +812,6 @@ def parse_specification(pobj, statements, run_env, mod_name=None, # End if inspec = pobj.in_region('MODULE', region_name=mod_name) break - elif type_def: - # We have a type definition without metadata - # Just parse it and throw away what is found. - # Put statement back so caller knows where we are - statements.insert(0, statement) - _ = parse_type_def(statements, type_def, - spec_name, pobj, run_env) elif is_contains_statement(statement, inmod): inspec = False break diff --git a/scripts/host_cap.py b/scripts/host_cap.py index 9c88cf34..f7447de2 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -8,7 +8,7 @@ import logging import os # CCPP framework imports -from ccpp_suite import API +from ccpp_suite import API, API_SOURCE_NAME from ccpp_state_machine import CCPP_STATE_MACH from constituents import ConstituentVarDict, CONST_DDT_NAME, CONST_DDT_MOD from ddt_library import DDTLibrary @@ -32,9 +32,7 @@ end subroutine {host_model}_ccpp_physics_{stage} ''' -_API_SRC_NAME = "CCPP_API" - -_API_SOURCE = ParseSource(_API_SRC_NAME, "MODULE", +_API_SOURCE = ParseSource(API_SOURCE_NAME, "MODULE", ParseContext(filename="host_cap.F90")) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', @@ -60,7 +58,7 @@ 'suites':''}) # Used to prevent loop substitution lookups -_BLANK_DICT = VarDictionary(_API_SRC_NAME, _MVAR_DUMMY_RUN_ENV) +_BLANK_DICT = VarDictionary(API_SOURCE_NAME, _MVAR_DUMMY_RUN_ENV) ############################################################################### def suite_part_list(suite, stage): @@ -475,7 +473,7 @@ def write_host_cap(host_model, api, output_dir, run_env): subst_dict['intent'] = 'inout' # End if hdvars.append(hvar.clone(subst_dict, - source_name=_API_SRC_NAME)) + source_name=API_SOURCE_NAME)) # End for lnames = [x.get_prop_value('local_name') for x in apivars + hdvars] api_vlist = ", ".join(lnames) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 0886356d..414ffc5a 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -7,19 +7,26 @@ # Python library imports from __future__ import print_function import os +import os.path import re -import shutil import subprocess import sys import xml.etree.ElementTree as ET sys.path.insert(0, os.path.dirname(__file__)) +# pylint: disable=wrong-import-position +try: + from distutils.spawn import find_executable + _XMLLINT = find_executable('xmllint') +except ImportError: + _XMLLINT = None +# End try # CCPP framework imports from parse_source import CCPPError from parse_log import init_log, set_log_to_null +# pylint: enable=wrong-import-position # Global data _INDENT_STR = " " -_XMLLINT = shutil.which('xmllint') # Blank if not installed beg_tag_re = re.compile(r"([<][^/][^<>]*[^/][>])") end_tag_re = re.compile(r"([<][/][^<>/]+[>])") simple_tag_re = re.compile(r"([<][^/][^<>/]+[/][>])") @@ -29,14 +36,6 @@ PYSUBVER = sys.version_info[1] _LOGGER = None -############################################################################### -class XMLToolsInternalError(ValueError): -############################################################################### - """Error class for reporting internal errors""" - def __init__(self, message): - """Initialize this exception""" - super().__init__(message) - ############################################################################### def call_command(commands, logger, silent=False): ############################################################################### @@ -54,12 +53,35 @@ def call_command(commands, logger, silent=False): result = False outstr = '' try: - cproc = subprocess.run(commands, check=True, - capture_output=True) - if not silent: - logger.debug(cproc.stdout) - # end if - result = cproc.returncode == 0 + if PY3: + if PYSUBVER > 6: + cproc = subprocess.run(commands, check=True, + capture_output=True) + if not silent: + logger.debug(cproc.stdout) + # End if + result = cproc.returncode == 0 + elif PYSUBVER >= 5: + cproc = subprocess.run(commands, check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if not silent: + logger.debug(cproc.stdout) + # End if + result = cproc.returncode == 0 + else: + raise ValueError("Python 3 must be at least version 3.5") + # End if + else: + pproc = subprocess.Popen(commands, stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, _ = pproc.communicate() + if not silent: + logger.debug(output) + # End if + result = pproc.returncode == 0 + # End if except (OSError, CCPPError, subprocess.CalledProcessError) as err: if silent: result = False @@ -68,9 +90,9 @@ def call_command(commands, logger, silent=False): emsg = "Execution of '{}' failed with code:\n" outstr = emsg.format(cmd, err.returncode) outstr += "{}".format(err.output) - raise CCPPError(outstr) from err - # end if - # end of try + raise CCPPError(outstr) + # End if + # End of try return result ############################################################################### @@ -80,8 +102,6 @@ def find_schema_version(root): Find the version of the host registry file represented by root >>> find_schema_version(ET.fromstring('')) [1, 0] - >>> find_schema_version(ET.fromstring('')) - [2, 0] >>> find_schema_version(ET.fromstring('')) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): CCPPError: Illegal version string, '1.a' @@ -98,33 +118,33 @@ def find_schema_version(root): verbits = None if 'version' not in root.attrib: raise CCPPError("version attribute required") - # end if + # End if version = root.attrib['version'] versplit = version.split('.') try: if len(versplit) != 2: raise CCPPError('oops') - # end if (no else needed) + # End if (no else needed) try: verbits = [int(x) for x in versplit] except ValueError as verr: - raise CCPPError(verr) from verr - # end try + raise CCPPError(verr) + # End try if verbits[0] < 1: raise CCPPError('Major version must be at least 1') - # end if + # End if if verbits[1] < 0: raise CCPPError('Minor version must be non-negative') - # end if + # End if except CCPPError as verr: errstr = """Illegal version string, '{}' Format must be .""" ve_str = str(verr) if ve_str: errstr = ve_str + '\n' + errstr - # end if - raise CCPPError(errstr.format(version)) from verr - # end try + # End if + raise CCPPError(errstr.format(version)) + # End try return verbits ############################################################################### @@ -141,10 +161,10 @@ def find_schema_file(schema_root, version, schema_path=None): schema_file = os.path.join(schema_path, schema_filename) else: schema_file = schema_filename - # end if + # End if if os.path.exists(schema_file): return schema_file - # end if + # End if return None ############################################################################### @@ -158,42 +178,38 @@ def validate_xml_file(filename, schema_root, version, logger, # Check the filename if not os.path.isfile(filename): raise CCPPError("validate_xml_file: Filename, '{}', does not exist".format(filename)) - # end if + # End if if not os.access(filename, os.R_OK): raise CCPPError("validate_xml_file: Cannot open '{}'".format(filename)) - # end if - if os.path.isfile(schema_root): - # We already have a file, just use it - schema_file = schema_root - else: - if not schema_path: - # Find the schema, based on the model version - thispath = os.path.abspath(__file__) - pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) - schema_path = os.path.join(pdir, 'schema') - # end if - schema_file = find_schema_file(schema_root, version, schema_path) - if not (schema_file and os.path.isfile(schema_file)): - verstring = '.'.join([str(x) for x in version]) - emsg = """validate_xml_file: Cannot find schema for version {}, {} does not exist""" - raise CCPPError(emsg.format(verstring, schema_file)) - # end if - # end if + # End if + if not schema_path: + # Find the schema, based on the model version + thispath = os.path.abspath(__file__) + pdir = os.path.dirname(os.path.dirname(os.path.dirname(thispath))) + schema_path = os.path.join(pdir, 'schema') + # End if + schema_file = find_schema_file(schema_root, version, schema_path) + if not (schema_file and os.path.isfile(schema_file)): + verstring = '.'.join([str(x) for x in version]) + emsg = """validate_xml_file: Cannot find schema for version {}, + {} does not exist""" + raise CCPPError(emsg.format(verstring, schema_file)) + # End if if not os.access(schema_file, os.R_OK): emsg = "validate_xml_file: Cannot open schema, '{}'" raise CCPPError(emsg.format(schema_file)) - # end if - if _XMLLINT: + # End if + if _XMLLINT is not None: logger.debug("Checking file {} against schema {}".format(filename, schema_file)) cmd = [_XMLLINT, '--noout', '--schema', schema_file, filename] result = call_command(cmd, logger) return result - # end if + # End if lmsg = "xmllint not found, could not validate file {}" if error_on_noxmllint: raise CCPPError("validate_xml_file: " + lmsg.format(filename)) - # end if + # End if logger.warning(lmsg.format(filename)) return True # We could not check but still need to proceed @@ -213,13 +229,13 @@ def read_xml_file(filename, logger=None): root = tree.getroot() except ET.ParseError as perr: emsg = "read_xml_file: Cannot read {}, {}" - raise CCPPError(emsg.format(filename, perr)) from perr + raise CCPPError(emsg.format(filename, perr)) elif not os.access(filename, os.R_OK): raise CCPPError("read_xml_file: Cannot open '{}'".format(filename)) else: emsg = "read_xml_file: Filename, '{}', does not exist" raise CCPPError(emsg.format(filename)) - # end if + # End if if logger: logger.debug("Read XML file, '{}'".format(filename)) # End if @@ -232,7 +248,7 @@ class PrettyElementTree(ET.ElementTree): def __init__(self, element=None, file=None): """Initialize a PrettyElementTree object""" - super().__init__(element, file) + super(PrettyElementTree, self).__init__(element, file) def _write(self, outfile, line, indent, eol=os.linesep): """Write as an ASCII string to """ @@ -252,7 +268,7 @@ def _inc_pos(outstr, text, txt_beg): # end if emsg = "No output at {} of {}\n{}".format(txt_beg, len(text), text[txt_beg:txt_end]) - raise XMLToolsInternalError(emsg) + raise DatatableInternalError(emsg) def write(self, file, encoding="us-ascii", xml_declaration=None, default_namespace=None, method="xml", @@ -260,26 +276,26 @@ def write(self, file, encoding="us-ascii", xml_declaration=None, """Subclassed write method to format output.""" if PY3 and (PYSUBVER >= 4): if PYSUBVER >= 8: - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method, - xml_declaration=xml_declaration, - default_namespace=default_namespace, - short_empty_elements=short_empty_elements) + input = ET.tostring(self.getroot(), + encoding=encoding, method=method, + xml_declaration=xml_declaration, + default_namespace=default_namespace, + short_empty_elements=short_empty_elements) else: - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method, - short_empty_elements=short_empty_elements) + input = ET.tostring(self.getroot(), + encoding=encoding, method=method, + short_empty_elements=short_empty_elements) # end if else: - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method) + input = ET.tostring(self.getroot(), + encoding=encoding, method=method) # end if if PY3: fmode = 'wt' - root = str(et_str, encoding="utf-8") + root = str(input, encoding="utf-8") else: fmode = 'w' - root = et_str + root = input # end if indent = 0 last_write_text = False From 1683be3406c5608e9a3e00b72bb8b2e2f1ac6807 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 28 Aug 2023 03:43:11 -0600 Subject: [PATCH 10/49] add latest changes --- .github/workflows/capgen_unit_tests.yaml | 4 +- .github/workflows/python.yaml | 45 +++++-- pytest.ini | 4 +- scripts/code_block.py | 27 ++-- scripts/fortran_tools/parse_fortran.py | 16 +-- scripts/metadata_table.py | 18 +-- scripts/metavar.py | 12 +- scripts/parse_tools/parse_object.py | 11 +- scripts/parse_tools/parse_source.py | 12 +- scripts/parse_tools/xml_tools.py | 18 +-- scripts/var_props.py | 135 ++++++++++++------- test/run_doctest.sh | 35 ----- test/{run_tests.sh => run_fortran_tests.sh} | 20 +-- test/unit_tests/test_fortran_write.py | 3 +- test/unit_tests/test_metadata_host_file.py | 1 + test/unit_tests/test_metadata_scheme_file.py | 1 + test/unit_tests/test_metadata_table.py | 3 +- test/unit_tests/test_var_transforms.py | 3 +- 18 files changed, 162 insertions(+), 206 deletions(-) delete mode 100755 test/run_doctest.sh rename test/{run_tests.sh => run_fortran_tests.sh} (73%) diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml index eb574f2d..4406ae77 100644 --- a/.github/workflows/capgen_unit_tests.yaml +++ b/.github/workflows/capgen_unit_tests.yaml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened] push: branches: - #Trigger workflow on push to any branch or branch heirarchy: + #Trigger workflow on push to any branch or branch hierarchy: - '**' jobs: @@ -18,5 +18,5 @@ jobs: - name: update repos and install dependencies run: sudo apt-get update && sudo apt-get install -y build-essential gfortran cmake python3 git - name: Run unit tests - run: cd test && ./run_tests.sh + run: cd test && ./run_fortran_tests.sh diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index c630c438..3ab5347c 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -1,28 +1,55 @@ name: Python package -on: [push] +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + #Trigger workflow on push to any branch or branch hierarchy: + - '**' jobs: build: - + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest - name: Test with pytest - if: github.repository == 'NCAR/ccpp-framework' # Only run on main repo run: | export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools - pytest + pytest -v + + doctest: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + - name: Doctest + run: | + export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools + pytest -v scripts/ --doctest-modules diff --git a/pytest.ini b/pytest.ini index 83323d6a..d08180f1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,2 @@ [pytest] -addopts = -ra -q --ignore=tests/test_capgen.py -testpaths = - tests \ No newline at end of file +addopts = -ra --ignore=scripts/metadata2html.py --ignore-glob=test/**/test_reports.py \ No newline at end of file diff --git a/scripts/code_block.py b/scripts/code_block.py index 96dc30e9..ccd3f209 100644 --- a/scripts/code_block.py +++ b/scripts/code_block.py @@ -13,7 +13,7 @@ class CodeBlock(object): """Class to store a block of code and a method to write it to a file >>> CodeBlock([]) #doctest: +ELLIPSIS - <__main__.CodeBlock object at 0x...> + >>> CodeBlock(['hi mom']) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ParseInternalError: Each element of must contain exactly two items, a code string and a relative indent @@ -24,7 +24,10 @@ class CodeBlock(object): Traceback (most recent call last): ParseInternalError: Each element of must contain exactly two items, a code string and a relative indent >>> CodeBlock([('hi mom', 1)]) #doctest: +ELLIPSIS - <__main__.CodeBlock object at 0x...> + + >>> from fortran_tools import FortranWriter + >>> outfile_name = "__code_block_temp.F90" + >>> outfile = FortranWriter(outfile_name, 'w', 'test file', 'test_mod') >>> CodeBlock([('hi mom', 1)]).write(outfile, 1, {}) >>> CodeBlock([('hi {greet} mom', 1)]).write(outfile, 1, {}) #doctest: +IGNORE_EXCEPTION_DETAIL @@ -32,6 +35,10 @@ class CodeBlock(object): ParseInternalError: 'greet' missing from >>> CodeBlock([('hi {{greet}} mom', 1)]).write(outfile, 1, {}) >>> CodeBlock([('{greet} there mom', 1)]).write(outfile, 1, {'greet':'hi'}) + >>> outfile.__exit__() + False + >>> import os + >>> os.remove(outfile_name) """ __var_re = re.compile(r"[{][ ]*([A-Za-z][A-Za-z0-9_]*)[ ]*[}]") @@ -110,19 +117,3 @@ def write(self, outfile, indent_level, var_dict): # end for ############################################################################### -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - import os - import sys - from fortran_tools import FortranWriter - # pylint: enable=ungrouped-imports - outfile_name = "__code_block_temp.F90" - with FortranWriter(outfile_name, 'w', 'test file', 'test_mod') as outfile: - fail, _ = doctest.testmod() - # end with - if os.path.exists(outfile_name): - os.remove(outfile_name) - # end if - sys.exit(fail) -# end if diff --git a/scripts/fortran_tools/parse_fortran.py b/scripts/fortran_tools/parse_fortran.py index 624f02cb..817ae184 100644 --- a/scripts/fortran_tools/parse_fortran.py +++ b/scripts/fortran_tools/parse_fortran.py @@ -665,6 +665,10 @@ def parse_fortran_var_decl(line, source, run_env): '(8)' >>> _VAR_ID_RE.match("foo(::,a:b,a:,:b)").group(2) '(::,a:b,a:,:b)' + >>> from framework_env import CCPPFrameworkEnv + >>> _DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}) >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') 'foo' >>> parse_fortran_var_decl("integer :: foo = 0", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') @@ -826,15 +830,3 @@ def parse_fortran_var_decl(line, source, run_env): ######################################################################## ######################################################################## - -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - # pylint: enable=ungrouped-imports - from framework_env import CCPPFrameworkEnv - _DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', - 'scheme_files':'', - 'suites':''}) - fail, _ = doctest.testmod() - sys.exit(fail) -# end if diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 17e8dfdb..c7f84534 100644 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -504,6 +504,10 @@ def table_start(cls, line): class MetadataSection(ParseSource): """Class to hold all information from a metadata header + >>> from framework_env import CCPPFrameworkEnv + >>> _DUMMY_RUN_ENV = CCPPFrameworkEnv(None, {'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}) >>> MetadataSection("footable", "scheme", _DUMMY_RUN_ENV, \ parse_object=ParseObject("foobar.txt", \ ["name = footable", "type = scheme", "module = foo", \ @@ -511,7 +515,7 @@ class MetadataSection(ParseSource): "long_name = horizontal loop extent, start at 1", \ "units = index | type = integer", \ "dimensions = () | intent = in"])) #doctest: +ELLIPSIS - <__main__.MetadataSection foo / footable at 0x...> + >>> MetadataSection("footable", "scheme", _DUMMY_RUN_ENV, \ parse_object=ParseObject("foobar.txt", \ ["name = footable", "type = scheme", "module = foobar", \ @@ -1267,15 +1271,3 @@ def is_scalar_reference(test_val): return check_fortran_ref(test_val, None, False) is not None ######################################################################## - -if __name__ == "__main__": -# pylint: enable=ungrouped-imports - import doctest - import sys -# pylint: disable=ungrouped-imports - from framework_env import CCPPFrameworkEnv - _DUMMY_RUN_ENV = CCPPFrameworkEnv(None, {'host_files':'', - 'scheme_files':'', - 'suites':''}) - fail, _ = doctest.testmod() - sys.exit(fail) diff --git a/scripts/metavar.py b/scripts/metavar.py index 71609580..bf165587 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -1379,11 +1379,11 @@ class VarDictionary(OrderedDict): >>> VarDictionary('bar', _MVAR_DUMMY_RUN_ENV, variables={}) VarDictionary(bar) >>> VarDictionary('baz', _MVAR_DUMMY_RUN_ENV, variables=Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'scheme', ParseContext()), _MVAR_DUMMY_RUN_ENV)) #doctest: +ELLIPSIS - VarDictionary(baz, [('hi_mom', <__main__.Var hi_mom: foo at 0x...>)]) + VarDictionary(baz, [('hi_mom', )]) >>> print("{}".format(VarDictionary('baz', _MVAR_DUMMY_RUN_ENV, variables=Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'scheme', ParseContext()), _MVAR_DUMMY_RUN_ENV)))) VarDictionary(baz, ['hi_mom']) >>> VarDictionary('qux', _MVAR_DUMMY_RUN_ENV, variables=[Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'scheme', ParseContext()), _MVAR_DUMMY_RUN_ENV)]) #doctest: +ELLIPSIS - VarDictionary(qux, [('hi_mom', <__main__.Var hi_mom: foo at 0x...>)]) + VarDictionary(qux, [('hi_mom', )]) >>> VarDictionary('boo', _MVAR_DUMMY_RUN_ENV).add_variable(Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'scheme', ParseContext()), _MVAR_DUMMY_RUN_ENV), _MVAR_DUMMY_RUN_ENV) >>> VarDictionary('who', _MVAR_DUMMY_RUN_ENV, variables=[Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'scheme', ParseContext()), _MVAR_DUMMY_RUN_ENV)]).prop_list('local_name') @@ -1982,11 +1982,3 @@ def new_internal_variable_name(self, prefix=None, max_len=63): _MVAR_DUMMY_RUN_ENV)]) ############################################################################### -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - import sys - # pylint: enable=ungrouped-imports - fail, _ = doctest.testmod() - sys.exit(fail) -# end if diff --git a/scripts/parse_tools/parse_object.py b/scripts/parse_tools/parse_object.py index 4518d75c..216eb5c5 100644 --- a/scripts/parse_tools/parse_object.py +++ b/scripts/parse_tools/parse_object.py @@ -10,7 +10,7 @@ class ParseObject(ParseContext): """ParseObject is a simple class that keeps track of an object's place in a file and safely produces lines from an array of lines >>> ParseObject('foobar.F90', []) #doctest: +ELLIPSIS - <__main__.ParseObject object at 0x...> + >>> ParseObject('foobar.F90', []).filename 'foobar.F90' >>> ParseObject('foobar.F90', ["##hi mom",], line_start=1).curr_line() @@ -170,12 +170,3 @@ def __del__(self): # end try ######################################################################## - -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - import sys - # pylint: enable=ungrouped-imports - fail, _ = doctest.testmod() - sys.exit(fail) -# end if diff --git a/scripts/parse_tools/parse_source.py b/scripts/parse_tools/parse_source.py index 6d28b694..30f05f7f 100644 --- a/scripts/parse_tools/parse_source.py +++ b/scripts/parse_tools/parse_source.py @@ -201,7 +201,7 @@ def __getitem__(self, index): class ParseContext(object): """A class for keeping track of a parsing position >>> ParseContext(32, "source.F90") #doctest: +ELLIPSIS - <__main__.ParseContext object at 0x...> + >>> ParseContext("source.F90", 32) Traceback (most recent call last): CCPPError: ParseContext linenum must be an int @@ -382,7 +382,7 @@ class ParseSource(object): """ A simple object for providing source information >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")) #doctest: +ELLIPSIS - <__main__.ParseSource object at 0x...> + >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).type 'mytype' >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).name @@ -413,11 +413,3 @@ def context(self): return self._context ######################################################################## - -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - # pylint: enable=ungrouped-imports - fail, _ = doctest.testmod() - sys.exit(fail) -# end if diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 414ffc5a..120ab645 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -41,6 +41,8 @@ def call_command(commands, logger, silent=False): ############################################################################### """ Try a command line and return the output on success (None on failure) + >>> _LOGGER = init_log('xml_tools') + >>> set_log_to_null(_LOGGER) >>> call_command(['ls', 'really__improbable_fffilename.foo'], _LOGGER) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): CCPPError: Execution of 'ls really__improbable_fffilename.foo' failed: @@ -350,19 +352,3 @@ def write(self, file, encoding="us-ascii", xml_declaration=None, # end with ############################################################################## - -if __name__ == "__main__": - _LOGGER = init_log('xml_tools') - set_log_to_null(_LOGGER) - try: - # First, run doctest - # pylint: disable=ungrouped-imports - import doctest - # pylint: enable=ungrouped-imports - fail, _ = doctest.testmod() - sys.exit(fail) - except CCPPError as cerr: - print("{}".format(cerr)) - sys.exit(fail) - # end try -# end if diff --git a/scripts/var_props.py b/scripts/var_props.py index 69b6b766..15613567 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -198,7 +198,7 @@ def default_kind_val(prop_dict, context=None): >>> default_kind_val({'local_name':'foo'}) #doctest: +ELLIPSIS Traceback (most recent call last): ... - parse_source.CCPPError: No type to find default kind for foo + parse_source.CCPPError: No type to find default kind for foo >>> default_kind_val({}) #doctest: +ELLIPSIS Traceback (most recent call last): ... @@ -206,7 +206,7 @@ def default_kind_val(prop_dict, context=None): >>> default_kind_val({'local_name':'foo'}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): ... - parse_source.CCPPError: No type to find default kind for foo, at foo.F90:4 + parse_source.CCPPError: No type to find default kind for foo, at foo.F90:4 >>> default_kind_val({}, context=ParseContext(linenum=3, filename='foo.F90')) #doctest: +ELLIPSIS Traceback (most recent call last): ... @@ -515,25 +515,25 @@ def __is_horizontal_loop_dimension(hdim): class VariableProperty: """Class to represent a single property of a metadata header entry >>> VariableProperty('local_name', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('standard_name', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('long_name', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('units', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('dimensions', list) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('type', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('kind', str) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('state_variable', str, valid_values_in=['True', 'False', '.true.', '.false.' ], optional_in=True, default_in=False) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('intent', str, valid_values_in=['in', 'out', 'inout']) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('optional', str, valid_values_in=['True', 'False', '.true.', '.false.' ], optional_in=True, default_in=False) #doctest: +ELLIPSIS - <__main__.VariableProperty object at ...> + >>> VariableProperty('local_name', str).name 'local_name' >>> VariableProperty('standard_name', str).ptype == str @@ -763,31 +763,41 @@ class VarCompatObj: character(len=) # Test that we can create a standard VarCompatObj object + >>> from parse_tools import init_log, set_log_to_null + >>> _DOCTEST_LOGGING = init_log('var_props') + >>> set_log_to_null(_DOCTEST_LOGGING) + >>> _DOCTEST_RUNENV = CCPPFrameworkEnv(_DOCTEST_LOGGING, \ + ndict={'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}, \ + kind_types=["kind_phys=REAL64", \ + "kind_dyn=REAL32", \ + "kind_host=REAL64"]) >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", [], \ "var1_lname", "var_stdname", "real", "kind_phys", \ "m", [], "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS - <__main__.VarCompatObj object at 0x...> + # Test that a 2-D var with no horizontal transform works >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ ['horizontal_dimension'], "var1_lname", "var_stdname", \ "real", "kind_phys", "m", ['horizontal_dimension'], \ "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS - <__main__.VarCompatObj object at 0x...> + # Test that a 2-D var with a horizontal transform works >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ ['horizontal_dimension'], "var1_lname", "var_stdname", \ "real", "kind_phys", "m", ['horizontal_loop_extent'], \ "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS - <__main__.VarCompatObj object at 0x...> + # Test that a 2-D var with unit conversion m->km works >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ ['horizontal_dimension'], "var1_lname", "var_stdname", \ "real", "kind_phys", "km", ['horizontal_dimension'], \ "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS - <__main__.VarCompatObj object at 0x...> + # Test that a 2-D var with unit conversion m->km works and that it # produces the correct forward transformation @@ -1012,6 +1022,26 @@ def _get_kind_convstrs(self, var1_kind, var2_kind, run_env): If a conversion is required, return a tuple with the two kinds, i.e., (var1_kind, var2_kind). + # Initial setup + >>> from parse_tools import init_log, set_log_to_null + >>> _DOCTEST_LOGGING = init_log('var_props') + >>> set_log_to_null(_DOCTEST_LOGGING) + >>> _DOCTEST_RUNENV = CCPPFrameworkEnv(_DOCTEST_LOGGING, \ + ndict={'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}, \ + kind_types=["kind_phys=REAL64", \ + "kind_dyn=REAL32", \ + "kind_host=REAL64"]) + >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') + >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') + >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ + "m", [], "var1_lname", "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", _DOCTEST_RUNENV, \ + v1_context=_DOCTEST_CONTEXT1, \ + v2_context=_DOCTEST_CONTEXT2) + # Try some kind conversions >>> _DOCTEST_VCOMPAT._get_kind_convstrs('kind_phys', 'kind_dyn', \ _DOCTEST_RUNENV) @@ -1046,6 +1076,26 @@ def _get_unit_convstrs(self, var1_units, var2_units): for transforming a variable in to / from a variable in . + # Initial setup + >>> from parse_tools import init_log, set_log_to_null + >>> _DOCTEST_LOGGING = init_log('var_props') + >>> set_log_to_null(_DOCTEST_LOGGING) + >>> _DOCTEST_RUNENV = CCPPFrameworkEnv(_DOCTEST_LOGGING, \ + ndict={'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}, \ + kind_types=["kind_phys=REAL64", \ + "kind_dyn=REAL32", \ + "kind_host=REAL64"]) + >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') + >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') + >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ + "m", [], "var1_lname", "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", _DOCTEST_RUNENV, \ + v1_context=_DOCTEST_CONTEXT1, \ + v2_context=_DOCTEST_CONTEXT2) + # Try some working unit transforms >>> _DOCTEST_VCOMPAT._get_unit_convstrs('m', 'mm') ('1.0E+3{kind}*{var}', '1.0E-3{kind}*{var}') @@ -1098,20 +1148,40 @@ def _get_dim_transforms(self, var1_dims, var2_dims): The reverse dimension transformation is a permutation of the indices of the second variable to the first. + # Initial setup + >>> from parse_tools import init_log, set_log_to_null + >>> _DOCTEST_LOGGING = init_log('var_props') + >>> set_log_to_null(_DOCTEST_LOGGING) + >>> _DOCTEST_RUNENV = CCPPFrameworkEnv(_DOCTEST_LOGGING, \ + ndict={'host_files':'', \ + 'scheme_files':'', \ + 'suites':''}, \ + kind_types=["kind_phys=REAL64", \ + "kind_dyn=REAL32", \ + "kind_host=REAL64"]) + >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') + >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') + >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ + "m", [], "var1_lname", "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", _DOCTEST_RUNENV, \ + v1_context=_DOCTEST_CONTEXT1, \ + v2_context=_DOCTEST_CONTEXT2) + # Test simple permutations >>> _DOCTEST_VCOMPAT._get_dim_transforms(['horizontal_dimension', \ 'vertical_layer_dimension'], \ ['vertical_layer_dimension', \ 'horizontal_dimension']) \ #doctest: +ELLIPSIS - <__main__.DimTransform object at 0x...> + >>> _DOCTEST_VCOMPAT._get_dim_transforms(['horizontal_dimension', \ 'vertical_layer_dimension', \ 'xdim'], \ ['vertical_layer_dimension', \ 'horizontal_dimension', \ 'xdim']) #doctest: +ELLIPSIS - <__main__.DimTransform object at 0x...> + >>> _DOCTEST_VCOMPAT._get_dim_transforms(['horizontal_dimension', \ 'vertical_layer_dimension', \ 'xdim'], \ @@ -1119,7 +1189,7 @@ def _get_dim_transforms(self, var1_dims, var2_dims): 'horizontal_dimension', \ 'vertical_layer_dimension']) \ #doctest: +ELLIPSIS - <__main__.DimTransform object at 0x...> + # Test some mismatch sets >>> _DOCTEST_VCOMPAT._get_dim_transforms(['horizontal_dimension', \ @@ -1323,30 +1393,3 @@ def __bool__(self): return self.equiv ############################################################################### -if __name__ == "__main__": - # pylint: disable=ungrouped-imports - import doctest - import sys - from parse_tools import init_log, set_log_to_null - # pylint: enable=ungrouped-imports - _DOCTEST_LOGGING = init_log('var_props') - set_log_to_null(_DOCTEST_LOGGING) - _DOCTEST_RUNENV = CCPPFrameworkEnv(_DOCTEST_LOGGING, - ndict={'host_files':'', - 'scheme_files':'', - 'suites':''}, - kind_types=["kind_phys=REAL64", - "kind_dyn=REAL32", - "kind_host=REAL64"]) - _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') - _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') - _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", - "m", [], "var1_lname", "var_stdname", - "real", "kind_phys", "m", [], - "var2_lname", _DOCTEST_RUNENV, - v1_context=_DOCTEST_CONTEXT1, - v2_context=_DOCTEST_CONTEXT2) - OPTIONS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE - fail, _ = doctest.testmod(optionflags=OPTIONS) - sys.exit(fail) -# end if diff --git a/test/run_doctest.sh b/test/run_doctest.sh deleted file mode 100755 index aeecb133..00000000 --- a/test/run_doctest.sh +++ /dev/null @@ -1,35 +0,0 @@ -#! /bin/bash - -root=$( dirname $( cd $( dirname ${0}); pwd -P ) ) -scripts=${root}/scripts - -perr() { - # Print error message ($2) on error ($1) - if [ ${1} -ne 0 ]; then - echo "ERROR: ${2}" - if [ $# -gt 2 ]; then - exit ${3} - else - exit 1 - fi - fi -} - -cd ${scripts} -perr $? "Cannot cd to scripts directory, '${scripts}'" - -errcnt=0 - -export PYTHONPATH="${scripts}:${PYTHONPATH}" -# Find all python scripts that have doctest -for pyfile in $(find . -name \*.py); do - if [ -f "${pyfile}" ]; then - if [ $(grep -c doctest ${pyfile}) -ne 0 ]; then - python3 ${pyfile} - res=$? - errcnt=$((errcnt + res)) - fi - fi -done - -exit ${errcnt} diff --git a/test/run_tests.sh b/test/run_fortran_tests.sh similarity index 73% rename from test/run_tests.sh rename to test/run_fortran_tests.sh index 04ce5e8c..3b3512c2 100755 --- a/test/run_tests.sh +++ b/test/run_fortran_tests.sh @@ -47,27 +47,9 @@ fi # fi echo "Skipping var_action_test/run_test until feature is fully implemented" -# Run doctests -./run_doctest.sh -res=$? -errcnt=$((errcnt + res)) -if [ $res -ne 0 ]; then - echo "${errcnt} doctest failures" -fi - -for test in `ls unit_tests/test_*.py`; do - echo "Running unit test, ${test}" - python3 ${test} - res=$? - errcnt=$((errcnt + res)) - if [ $res -ne 0 ]; then - echo "Failure, '${res}', running unit test, ${test}" - fi -done - if [ $errcnt -eq 0 ]; then echo "All tests PASSed!" else echo "${errcnt} tests FAILed" - return 1 + exit 1 fi diff --git a/test/unit_tests/test_fortran_write.py b/test/unit_tests/test_fortran_write.py index fdc00085..87e64baa 100644 --- a/test/unit_tests/test_fortran_write.py +++ b/test/unit_tests/test_fortran_write.py @@ -122,5 +122,6 @@ def test_good_comments(self): amsg = f"{generate} does not match {compare}" self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() + diff --git a/test/unit_tests/test_metadata_host_file.py b/test/unit_tests/test_metadata_host_file.py index cc5ba7b5..d01fda97 100644 --- a/test/unit_tests/test_metadata_host_file.py +++ b/test/unit_tests/test_metadata_host_file.py @@ -258,3 +258,4 @@ def test_module_with_two_ddts_and_extra_var(self): if __name__ == "__main__": unittest.main() + diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 9598406d..6651c2de 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -346,3 +346,4 @@ def test_scheme_ddt_only(self): if __name__ == "__main__": unittest.main() + diff --git a/test/unit_tests/test_metadata_table.py b/test/unit_tests/test_metadata_table.py index 544d3013..3a01f98b 100755 --- a/test/unit_tests/test_metadata_table.py +++ b/test/unit_tests/test_metadata_table.py @@ -394,5 +394,6 @@ def test_invalid_table_properties_type(self): emsg = "Invalid metadata table type, 'banana', at " self.assertTrue(emsg in str(context.exception)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() + diff --git a/test/unit_tests/test_var_transforms.py b/test/unit_tests/test_var_transforms.py index 70f31d1e..12cffcdb 100644 --- a/test/unit_tests/test_var_transforms.py +++ b/test/unit_tests/test_var_transforms.py @@ -432,5 +432,6 @@ def test_valid_dim_transforms(self): expected = f"{v5_lname}({lind_str}) = {v4_lname}({rind_str})" self.assertEqual(rev_stmt, expected) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() + From 52626d09d2f8b89776bc4e58c51fc8aaee73d8df Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 28 Aug 2023 04:27:33 -0600 Subject: [PATCH 11/49] bugfixes --- scripts/ccpp_capgen.py | 5 ++--- scripts/ccpp_suite.py | 6 ++---- scripts/framework_env.py | 2 +- scripts/metadata_table.py | 2 +- scripts/state_machine.py | 2 +- test/advection_test/cld_ice.F90 | 12 ++++++------ test/advection_test/cld_ice.meta | 4 ++-- test/advection_test/cld_liq.F90 | 12 ++++++------ test/advection_test/cld_liq.meta | 4 ++-- test/capgen_test/test_host.F90 | 8 ++++---- 10 files changed, 27 insertions(+), 30 deletions(-) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index d28bfd6a..dfa2e211 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -31,7 +31,7 @@ ## Capture the Framework root __SCRIPT_PATH = os.path.dirname(__file__) -__FRAMEWORK_ROOT = os.path.abspath(os.path.join(_SCRIPT_PATH, os.pardir)) +__FRAMEWORK_ROOT = os.path.abspath(os.path.join(__SCRIPT_PATH, os.pardir)) ## Init this now so that all Exceptions can be trapped _LOGGER = init_log(os.path.basename(__file__)) @@ -629,8 +629,7 @@ def capgen(run_env, return_db=False): cap_filenames = ccpp_api.write(outtemp_dir, run_env) if run_env.generate_host_cap: # Create a cap file - cap_module = host_model.ccpp_cap_name() - host_files = [write_host_cap(host_model, ccpp_api, cap_module, + host_files = [write_host_cap(host_model, ccpp_api, outtemp_dir, run_env)] else: host_files = list() diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index ba8f8609..c4d7fbab 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -729,7 +729,6 @@ def write_var_set_loop(ofile, varlist_name, var_list, indent, def write_suite_part_list_sub(self, ofile, errmsg_name, errcode_name): """Write the suite-part list subroutine""" - ofile.blank_line() inargs = f"suite_name, part_list, {errmsg_name}, {errcode_name}" ofile.write(f"subroutine {API.__part_fname}({inargs})", 1) oline = "character(len=*), intent(in) :: suite_name" @@ -740,7 +739,7 @@ def write_suite_part_list_sub(self, ofile, errmsg_name, errcode_name): self._errcode_var.write_def(ofile, 2, self) else_str = '' ename = self._errcode_var.get_prop_value('local_name') - ofile.write(f"{ename} = ''", 2) + ofile.write(f"{ename} = 0", 2) ename = self._errmsg_var.get_prop_value('local_name') ofile.write(f"{ename} = ''", 2) for suite in self.suites: @@ -820,8 +819,7 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): protected = pvar.get_prop_value("protected") # end if # end if - elements = var.intrinsic_elements(check_dict=self.parent, - ddt_lib=self.__ddt_lib) + elements = var.intrinsic_elements(check_dict=self.parent) if (intent == 'in') and (not protected): if isinstance(elements, list): input_vars[1].add(stdname) diff --git a/scripts/framework_env.py b/scripts/framework_env.py index b5550ca6..6456134e 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -103,7 +103,7 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, # String of definitions, separated by spaces preproc_list = [x.strip() for x in preproc_defs.split(' ') if x] else: - wmsg = f"Error: Bad preproc list type, '{type_name{preproc_defs)}'" + wmsg = f"Error: Bad preproc list type, '{type_name(preproc_defs)}'" emsg += esep + wmsg esep = '\n' # end if diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 3501e40e..5e9820d7 100644 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -683,7 +683,7 @@ def __init__(self, table_name, table_type, run_env, parse_object=None, register_fortran_ddt_name(self.title) # end if # Categorize the variables - self._var_intents = {'in' : [], 'out' : [], 'inout' : [])} + self._var_intents = {'in' : [], 'out' : [], 'inout' : []} for var in self.variable_list(): intent = var.get_prop_value('intent') if intent is not None: diff --git a/scripts/state_machine.py b/scripts/state_machine.py index 393e423d..692da7a9 100644 --- a/scripts/state_machine.py +++ b/scripts/state_machine.py @@ -30,7 +30,7 @@ class StateMachine: >>> StateMachine([('ab','a','b','a')]).final_state('ab') 'b' >>> StateMachine([('ab','a','b','a')]).transition_regex('ab') - re.compile('a$, re.IGNORECASE') + re.compile('a$', re.IGNORECASE) >>> StateMachine([('ab','a','b','a')]).function_match('foo_a', transition='ab') ('foo', 'a', 'ab') >>> StateMachine([('ab','a','b',r'ax?')]).function_match('foo_a', transition='ab') diff --git a/test/advection_test/cld_ice.F90 b/test/advection_test/cld_ice.F90 index 5a91282f..a7da57e5 100644 --- a/test/advection_test/cld_ice.F90 +++ b/test/advection_test/cld_ice.F90 @@ -19,7 +19,7 @@ MODULE cld_ice !> \section arg_table_cld_ice_run Argument Table !! \htmlinclude arg_table_cld_ice_run.html !! - subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & + subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -27,7 +27,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_ice(:,:) + REAL(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -44,7 +44,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & do ilev = 1, size(temp, 2) if (temp(icol, ilev) < tcld) then frz = MAX(qv(icol, ilev) - 0.5_kind_phys, 0.0_kind_phys) - cld_ice(icol, ilev) = cld_ice(icol, ilev) + frz + cld_ice_array(icol, ilev) = cld_ice_array(icol, ilev) + frz qv(icol, ilev) = qv(icol, ilev) - frz if (frz > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + 1.0_kind_phys @@ -58,16 +58,16 @@ END SUBROUTINE cld_ice_run !> \section arg_table_cld_ice_init Argument Table !! \htmlinclude arg_table_cld_ice_init.html !! - subroutine cld_ice_init(tfreeze, cld_ice, errmsg, errflg) + subroutine cld_ice_init(tfreeze, cld_ice_array, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(inout) :: cld_ice(:,:) + real(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg errmsg = '' errflg = 0 - cld_ice = 0.0_kind_phys + cld_ice_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_ice_init diff --git a/test/advection_test/cld_ice.meta b/test/advection_test/cld_ice.meta index e4a961ab..f66888e0 100644 --- a/test/advection_test/cld_ice.meta +++ b/test/advection_test/cld_ice.meta @@ -41,7 +41,7 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. units = kg kg-1 @@ -73,7 +73,7 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. units = kg kg-1 diff --git a/test/advection_test/cld_liq.F90 b/test/advection_test/cld_liq.F90 index 2e1e5a57..9411e0cb 100644 --- a/test/advection_test/cld_liq.F90 +++ b/test/advection_test/cld_liq.F90 @@ -16,7 +16,7 @@ MODULE cld_liq !> \section arg_table_cld_liq_run Argument Table !! \htmlinclude arg_table_cld_liq_run.html !! - subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & + subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -25,7 +25,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_liq(:,:) + REAL(kind_phys), intent(inout) :: cld_liq_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -43,7 +43,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & if ( (qv(icol, ilev) > 0.0_kind_phys) .and. & (temp(icol, ilev) <= tcld)) then cond = MIN(qv(icol, ilev), 0.1_kind_phys) - cld_liq(icol, ilev) = cld_liq(icol, ilev) + cond + cld_liq_array(icol, ilev) = cld_liq_array(icol, ilev) + cond qv(icol, ilev) = qv(icol, ilev) - cond if (cond > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + (cond * 5.0_kind_phys) @@ -57,10 +57,10 @@ END SUBROUTINE cld_liq_run !> \section arg_table_cld_liq_init Argument Table !! \htmlinclude arg_table_cld_liq_init.html !! - subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) + subroutine cld_liq_init(tfreeze, cld_liq_array, tcld, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(out) :: cld_liq(:,:) + real(kind_phys), intent(out) :: cld_liq_array(:,:) real(kind_phys), intent(out) :: tcld character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg @@ -69,7 +69,7 @@ subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) errmsg = '' errflg = 0 - cld_liq = 0.0_kind_phys + cld_liq_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_liq_init diff --git a/test/advection_test/cld_liq.meta b/test/advection_test/cld_liq.meta index 1186d741..4c87f4b7 100644 --- a/test/advection_test/cld_liq.meta +++ b/test/advection_test/cld_liq.meta @@ -47,7 +47,7 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 @@ -79,7 +79,7 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 diff --git a/test/capgen_test/test_host.F90 b/test/capgen_test/test_host.F90 index 55c7159e..2114cdac 100644 --- a/test/capgen_test/test_host.F90 +++ b/test/capgen_test/test_host.F90 @@ -130,7 +130,7 @@ logical function check_suite(test_suite) suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR 1 ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -144,7 +144,7 @@ logical function check_suite(test_suite) 'input variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR 2 ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -158,7 +158,7 @@ logical function check_suite(test_suite) 'output variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR 3 ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -172,7 +172,7 @@ logical function check_suite(test_suite) 'required variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR 4 ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then From a5aa37b427d7102b71c771c8d1d8b7cee7a22ec4 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 09:50:27 -0600 Subject: [PATCH 12/49] add is_scheme_constituent interface --- scripts/constituents.py | 36 +++++++++++++++++++++++++++++------- scripts/host_cap.py | 38 ++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/scripts/constituents.py b/scripts/constituents.py index d8935130..e01d2eae 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -9,7 +9,7 @@ """ # CCPP framework imports -from parse_tools import ParseInternalError +from parse_tools import ParseInternalError, type_name from metavar import VarDictionary ######################################################################## @@ -28,7 +28,7 @@ class ConstituentVarDict(VarDictionary): allocation and support for these variables. """ - __const_prop_array_name = "ccpp_constituent_array" + __const_prop_array_name = "ccpp_constituents" __const_prop_init_name = "ccpp_constituents_initialized" __const_prop_init_consts = "ccpp_create_constituent_array" __constituent_type = "suite" @@ -112,7 +112,7 @@ def find_variable(self, standard_name=None, source_var=None, # end for newdims.append(':'.join(new_dnames)) # end for - var = source_var.clone({'dimensions' : newdims}, remove_intent=False, + var = source_var.clone({'dimensions' : newdims}, remove_intent=True, source_type=self.__constituent_type) self.add_variable(var, self.__run_env) return var @@ -406,7 +406,7 @@ def constituent_module_name(self): if not ((self.parent is not None) and hasattr(self.parent.parent, "constituent_module")): emsg = "ConstituentVarDict parent not HostModel?" - emsg += f"\nparent is '{type(self.parent.parent)}'" + emsg += f"\nparent is '{type_name(self.parent.parent)}'" raise ParseInternalError(emsg) # end if return self.parent.parent.constituent_module @@ -456,9 +456,9 @@ def write_constituent_use_statements(cap, suite_list, indent): @staticmethod def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcname, - copy_in_funcname, copy_out_funcname, const_obj_name, - const_names_name, const_indices_name, const_array_func, - advect_array_func, prop_array_func, + query_const_funcname, copy_in_funcname, copy_out_funcname, + const_obj_name, const_names_name, const_indices_name, + const_array_func, advect_array_func, prop_array_func, const_index_func, suite_list, err_vars): """Write out the host model routine which will instantiate constituent fields for all the constituents in . @@ -466,6 +466,7 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna Also write out the following routines: : Initialize constituent data : Number of constituents + : Check if standard name matches existing constituent : Collect constituent fields for host : Update constituent fields from host : Return a pointer to the constituent array @@ -634,6 +635,27 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna call_str = "call {}%num_constituents(num_flds, advected=advected, {})" cap.write(call_str.format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) + # Write query_consts routine + substmt = f"subroutine {query_const_funcname}" + cap.blank_line() + cap.write(f"{substmt}(var_name, constituent_exists, {err_dummy_str})", 1) + cap.comment(f"Return constituent_exists = true iff var_name appears in {host.name}_model_const_stdnames", 2) + cap.blank_line() + cap.write("character(len=*), intent(in) :: var_name", 2) + cap.write("logical, intent(out) :: constituent_exists", 2) + for evar in err_vars: + evar.write_def(cap, 2, host, dummy=True, add_intent="out") + # end for + cap.blank_line() + cap.write(f"{herrcode} = 0", 2) + cap.write(f"{herrmsg} = ''", 2) + cap.blank_line() + cap.write("constituent_exists = .false.", 2) + cap.write(f"if (any({host.name}_model_const_stdnames == var_name)) then", 2) + cap.write("constituent_exists = .true.", 3) + cap.write("end if", 2) + cap.blank_line() + cap.write(f"end {substmt}", 1) # Write copy_in routine substmt = "subroutine {}".format(copy_in_funcname) cap.write("", 0) diff --git a/scripts/host_cap.py b/scripts/host_cap.py index 479f688b..d2b3066f 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -8,7 +8,7 @@ import logging import os # CCPP framework imports -from ccpp_suite import API +from ccpp_suite import API, API_SOURCE_NAME from ccpp_state_machine import CCPP_STATE_MACH from constituents import ConstituentVarDict, CONST_DDT_NAME, CONST_DDT_MOD from constituents import CONST_OBJ_STDNAME @@ -33,8 +33,7 @@ end subroutine {host_model}_ccpp_physics_{stage} ''' -_API_SRC_NAME = "CCPP_API" -_API_SOURCE = ParseSource(_API_SRC_NAME, "MODULE", +_API_SOURCE = ParseSource(API_SOURCE_NAME, "MODULE", ParseContext(filename="host_cap.F90")) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', @@ -60,7 +59,7 @@ 'suites':''}) # Used to prevent loop substitution lookups -_BLANK_DICT = VarDictionary(_API_SRC_NAME, _MVAR_DUMMY_RUN_ENV) +_BLANK_DICT = VarDictionary(API_SOURCE_NAME, _MVAR_DUMMY_RUN_ENV) ############################################################################### def suite_part_list(suite, stage): @@ -110,6 +109,14 @@ def constituent_num_consts_funcname(host_model): Because this is a user interface API function, the name is fixed.""" return "{}_ccpp_number_constituents".format(host_model.name) +############################################################################### +def query_scheme_constituents_funcname(host_model): +############################################################################### + """Return the name of the function to return True if the standard name + passed in matches an existing constituent + Because this is a user interface API function, the name is fixed.""" + return f"{host_model.name}_ccpp_is_scheme_constituent" + ############################################################################### def constituent_copyin_subname(host_model): ############################################################################### @@ -210,7 +217,7 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): to create the dictionary. """ # First create a MetadataTable for the constituents DDT - stdname_layer = "ccpp_num_constituents" + stdname_layer = "number_of_ccpp_constituents" horiz_dim = "horizontal_dimension" vert_layer_dim = "vertical_layer_dimension" vert_interface_dim = "vertical_interface_dimension" @@ -225,7 +232,7 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): f" standard_name = {stdname_layer}", " units = count", " dimensions = ()", " type = integer", f"[ {array_layer} ]", - " standard_name = ccpp_constituent_array", + " standard_name = ccpp_constituents", " units = none", f" dimensions = ({horiz_dim}, {vert_layer_dim}, {stdname_layer})", " type = real", " kind = kind_phys"] @@ -445,15 +452,17 @@ def write_host_cap(host_model, api, module_name, output_dir, run_env): API.declare_inspection_interfaces(cap) # Write the host-model interfaces for constituents reg_name = constituent_register_subname(host_model) - cap.write("public :: {}".format(reg_name), 1) + cap.write(f"public :: {reg_name}", 1) init_name = constituent_initialize_subname(host_model) - cap.write("public :: {}".format(init_name), 1) + cap.write(f"public :: {init_name}", 1) numconsts_name = constituent_num_consts_funcname(host_model) - cap.write("public :: {}".format(numconsts_name), 1) + cap.write(f"public :: {numconsts_name}", 1) + queryconsts_name = query_scheme_constituents_funcname(host_model) + cap.write(f"public :: {queryconsts_name}", 1) copyin_name = constituent_copyin_subname(host_model) - cap.write("public :: {}".format(copyin_name), 1) + cap.write(f"public :: {copyin_name}", 1) copyout_name = constituent_copyout_subname(host_model) - cap.write("public :: {}".format(copyout_name), 1) + cap.write(f"public :: {copyout_name}", 1) const_array_func = constituent_model_consts(host_model) cap.write(f"public :: {const_array_func}", 1) advect_array_func = constituent_model_advected_consts(host_model) @@ -510,7 +519,7 @@ def write_host_cap(host_model, api, module_name, output_dir, run_env): subst_dict['intent'] = 'inout' # End if hdvars.append(hvar.clone(subst_dict, - source_name=_API_SRC_NAME)) + source_name=API_SOURCE_NAME)) # End for lnames = [x.get_prop_value('local_name') for x in apivars + hdvars] api_vlist = ", ".join(lnames) @@ -599,8 +608,9 @@ def write_host_cap(host_model, api, module_name, output_dir, run_env): const_names_name = constituent_model_const_stdnames(host_model) const_indices_name = constituent_model_const_indices(host_model) ConstituentVarDict.write_host_routines(cap, host_model, reg_name, init_name, - numconsts_name, copyin_name, - copyout_name, const_obj_name, + numconsts_name, queryconsts_name, + copyin_name, copyout_name, + const_obj_name, const_names_name, const_indices_name, const_array_func, From 9897870d873b0fdc6cc96158798e49d5fa828276 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:02:33 -0600 Subject: [PATCH 13/49] fix tests and add is_thermo_active property --- scripts/constituents.py | 4 +- scripts/host_cap.py | 10 +- src/ccpp_constituent_prop_mod.F90 | 142 ++++++++++++-- test/advection_test/cld_ice.F90 | 12 +- test/advection_test/cld_ice.meta | 6 +- test/advection_test/cld_liq.F90 | 12 +- test/advection_test/cld_liq.meta | 4 +- test/advection_test/test_host.F90 | 310 ++++++++++++++++++++++++------ 8 files changed, 401 insertions(+), 99 deletions(-) diff --git a/scripts/constituents.py b/scripts/constituents.py index e01d2eae..a828e5e5 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -9,7 +9,7 @@ """ # CCPP framework imports -from parse_tools import ParseInternalError, type_name +from parse_tools import ParseInternalError from metavar import VarDictionary ######################################################################## @@ -406,7 +406,7 @@ def constituent_module_name(self): if not ((self.parent is not None) and hasattr(self.parent.parent, "constituent_module")): emsg = "ConstituentVarDict parent not HostModel?" - emsg += f"\nparent is '{type_name(self.parent.parent)}'" + emsg += f"\nparent is '{type(self.parent.parent)}'" raise ParseInternalError(emsg) # end if return self.parent.parent.constituent_module diff --git a/scripts/host_cap.py b/scripts/host_cap.py index d2b3066f..8a2193cb 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -8,7 +8,7 @@ import logging import os # CCPP framework imports -from ccpp_suite import API, API_SOURCE_NAME +from ccpp_suite import API from ccpp_state_machine import CCPP_STATE_MACH from constituents import ConstituentVarDict, CONST_DDT_NAME, CONST_DDT_MOD from constituents import CONST_OBJ_STDNAME @@ -33,7 +33,9 @@ end subroutine {host_model}_ccpp_physics_{stage} ''' -_API_SOURCE = ParseSource(API_SOURCE_NAME, "MODULE", +_API_SRC_NAME = "CCPP_API" + +_API_SOURCE = ParseSource(_API_SRC_NAME, "MODULE", ParseContext(filename="host_cap.F90")) _API_DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', @@ -59,7 +61,7 @@ 'suites':''}) # Used to prevent loop substitution lookups -_BLANK_DICT = VarDictionary(API_SOURCE_NAME, _MVAR_DUMMY_RUN_ENV) +_BLANK_DICT = VarDictionary(_API_SRC_NAME, _MVAR_DUMMY_RUN_ENV) ############################################################################### def suite_part_list(suite, stage): @@ -519,7 +521,7 @@ def write_host_cap(host_model, api, module_name, output_dir, run_env): subst_dict['intent'] = 'inout' # End if hdvars.append(hvar.clone(subst_dict, - source_name=API_SOURCE_NAME)) + source_name=_API_SRC_NAME)) # End for lnames = [x.get_prop_value('local_name') for x in apivars + hdvars] api_vlist = ", ".join(lnames) diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index df80ec0d..c9f5322b 100644 --- a/src/ccpp_constituent_prop_mod.F90 +++ b/src/ccpp_constituent_prop_mod.F90 @@ -35,6 +35,7 @@ module ccpp_constituent_prop_mod character(len=:), private, allocatable :: vert_dim integer, private :: const_ind = int_unassigned logical, private :: advected = .false. + logical, private :: thermo_active = .false. ! While the quantities below can be derived from the standard name, ! this implementation avoids string searching in parameterizations ! const_type distinguishes mass, volume, and number conc. mixing ratios @@ -61,6 +62,7 @@ module ccpp_constituent_prop_mod procedure :: vertical_dimension => ccp_get_vertical_dimension procedure :: const_index => ccp_const_index procedure :: is_advected => ccp_is_advected + procedure :: is_thermo_active => ccp_is_thermo_active procedure :: equivalent => ccp_is_equivalent procedure :: is_mass_mixing_ratio => ccp_is_mass_mixing_ratio procedure :: is_volume_mixing_ratio => ccp_is_volume_mixing_ratio @@ -76,9 +78,10 @@ module ccpp_constituent_prop_mod procedure :: copyConstituent generic :: assignment(=) => copyConstituent ! Methods that change state (XXgoldyXX: make private?) - procedure :: instantiate => ccp_instantiate - procedure :: deallocate => ccp_deallocate - procedure :: set_const_index => ccp_set_const_index + procedure :: instantiate => ccp_instantiate + procedure :: deallocate => ccp_deallocate + procedure :: set_const_index => ccp_set_const_index + procedure :: set_thermo_active => ccp_set_thermo_active end type ccpp_constituent_properties_t !! \section arg_table_ccpp_constituent_prop_ptr_t @@ -96,6 +99,7 @@ module ccpp_constituent_prop_mod procedure :: vertical_dimension => ccpt_get_vertical_dimension procedure :: const_index => ccpt_const_index procedure :: is_advected => ccpt_is_advected + procedure :: is_thermo_active => ccpt_is_thermo_active procedure :: is_mass_mixing_ratio => ccpt_is_mass_mixing_ratio procedure :: is_volume_mixing_ratio => ccpt_is_volume_mixing_ratio procedure :: is_number_concentration => ccpt_is_number_concentration @@ -109,8 +113,9 @@ module ccpp_constituent_prop_mod ! ccpt_set: Set the internal pointer procedure :: set => ccpt_set ! Methods that change state (XXgoldyXX: make private?) - procedure :: deallocate => ccpt_deallocate - procedure :: set_const_index => ccpt_set_const_index + procedure :: deallocate => ccpt_deallocate + procedure :: set_const_index => ccpt_set_const_index + procedure :: set_thermo_active => ccpt_set_thermo_active end type ccpp_constituent_prop_ptr_t !! \section arg_table_ccpp_model_constituents_t @@ -596,6 +601,45 @@ end subroutine ccp_set_const_index !####################################################################### + subroutine ccp_set_thermo_active(this, thermo_flag, errcode, errmsg) + ! Set whether this constituent is thermodynamically active, which + ! means that certain physics schemes will use this constitutent + ! when calculating thermodynamic quantities (e.g. enthalpy). + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(inout) :: this + logical, intent(in) :: thermo_flag + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + + !Set thermodynamically active flag for this constituent: + if (this%is_instantiated(errcode, errmsg)) then + this%thermo_active = thermo_flag + end if + + end subroutine ccp_set_thermo_active + + !####################################################################### + + subroutine ccp_is_thermo_active(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_properties_t), intent(in) :: this + logical, intent(out) :: val_out + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + + !If instantiated then check if constituent is + !thermodynamically active, otherwise return false: + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%thermo_active + else + val_out = .false. + end if + end subroutine ccp_is_thermo_active + + !####################################################################### + subroutine ccp_is_advected(this, val_out, errcode, errmsg) ! Dummy arguments @@ -628,7 +672,8 @@ subroutine ccp_is_equivalent(this, oconst, equiv, errcode, errmsg) (trim(this%var_long_name) == trim(oconst%var_long_name)) .and. & (trim(this%vert_dim) == trim(oconst%vert_dim)) .and. & (this%advected .eqv. oconst%advected) .and. & - (this%const_default_value == oconst%const_default_value) + (this%const_default_value == oconst%const_default_value) .and. & + (this%thermo_active .eqv. oconst%thermo_active) else equiv = .false. end if @@ -1339,8 +1384,8 @@ end subroutine ccp_model_const_reset !######################################################################## - logical function ccp_model_const_is_match(this, index, advected) & - result(is_match) + logical function ccp_model_const_is_match(this, index, advected, & + thermo_active) result(is_match) ! Return .true. iff the constituent at matches a pattern ! Each (optional) property which is present represents something ! which is required as part of a match. @@ -1351,6 +1396,7 @@ logical function ccp_model_const_is_match(this, index, advected) & class(ccpp_model_constituents_t), intent(in) :: this integer, intent(in) :: index logical, optional, intent(in) :: advected + logical, optional, intent(in) :: thermo_active ! Local variable logical :: check @@ -1363,11 +1409,20 @@ logical function ccp_model_const_is_match(this, index, advected) & end if end if + if (present(thermo_active)) then + call this%const_metadata(index)%is_thermo_active(check) + if (thermo_active .neqv. check) then + is_match = .false. + end if + end if + + end function ccp_model_const_is_match !######################################################################## - subroutine ccp_model_const_num_match(this, nmatch, advected, errcode, errmsg) + subroutine ccp_model_const_num_match(this, nmatch, advected, thermo_active, & + errcode, errmsg) ! Query number of constituents matching pattern ! Each (optional) property which is present represents something ! which is required as part of a match. @@ -1377,6 +1432,7 @@ subroutine ccp_model_const_num_match(this, nmatch, advected, errcode, errmsg) class(ccpp_model_constituents_t), intent(in) :: this integer, intent(out) :: nmatch logical, optional, intent(in) :: advected + logical, optional, intent(in) :: thermo_active integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg ! Local variables @@ -1386,7 +1442,7 @@ subroutine ccp_model_const_num_match(this, nmatch, advected, errcode, errmsg) nmatch = 0 if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then + if (this%is_match(index, advected=advected, thermo_active=thermo_active)) then nmatch = nmatch + 1 end if end do @@ -1452,7 +1508,7 @@ end subroutine ccp_model_const_metadata !######################################################################## subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & - errcode, errmsg) + thermo_active, errcode, errmsg) ! Gather constituent fields matching pattern ! Each (optional) property which is present represents something ! which is required as part of a match. @@ -1462,6 +1518,7 @@ subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & class(ccpp_model_constituents_t), intent(in) :: this real(kind_phys), intent(out) :: const_array(:,:,:) logical, optional, intent(in) :: advected + logical, optional, intent(in) :: thermo_active integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg ! Local variables @@ -1478,7 +1535,8 @@ subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & max_cind = SIZE(const_array, 3) num_levels = SIZE(const_array, 2) do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then + if (this%is_match(index, advected=advected, & + thermo_active=thermo_active)) then ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then @@ -1527,7 +1585,7 @@ end subroutine ccp_model_const_copy_in_3d !######################################################################## subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & - errcode, errmsg) + thermo_active, errcode, errmsg) ! Update constituent fields matching pattern ! Each (optional) property which is present represents something ! which is required as part of a match. @@ -1537,6 +1595,7 @@ subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & class(ccpp_model_constituents_t), intent(inout) :: this real(kind_phys), intent(in) :: const_array(:,:,:) logical, optional, intent(in) :: advected + logical, optional, intent(in) :: thermo_active integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg ! Local variables @@ -1553,7 +1612,8 @@ subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & max_cind = SIZE(const_array, 3) num_levels = SIZE(const_array, 2) do index = 1, SIZE(this%const_metadata) - if (this%is_match(index, advected=advected)) then + if (this%is_match(index, advected=advected, & + thermo_active=thermo_active)) then ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then @@ -1828,6 +1888,28 @@ end subroutine ccpt_const_index !####################################################################### + subroutine ccpt_is_thermo_active(this, val_out, errcode, errmsg) + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(in) :: this + logical, intent(out) :: val_out + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_is_thermo_active' + + if (associated(this%prop)) then + call this%prop%is_thermo_active(val_out, errcode, errmsg) + else + val_out = .false. + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_thermo_active + + !####################################################################### + subroutine ccpt_is_advected(this, val_out, errcode, errmsg) ! Dummy arguments @@ -2123,9 +2205,9 @@ subroutine ccpt_set_const_index(this, index, errcode, errmsg) ! Dummy arguments class(ccpp_constituent_prop_ptr_t), intent(inout) :: this - integer, intent(in) :: index - integer, optional, intent(out) :: errcode - character(len=*), optional, intent(out) :: errmsg + integer, intent(in) :: index + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg ! Local variable character(len=*), parameter :: subname = 'ccpt_set_const_index' @@ -2146,4 +2228,30 @@ subroutine ccpt_set_const_index(this, index, errcode, errmsg) end subroutine ccpt_set_const_index + !####################################################################### + + subroutine ccpt_set_thermo_active(this, thermo_flag, errcode, errmsg) + ! Set whether this constituent is thermodynamically active, which + ! means that certain physics schemes will use this constitutent + ! when calculating thermodynamic quantities (e.g. enthalpy). + + ! Dummy arguments + class(ccpp_constituent_prop_ptr_t), intent(inout) :: this + logical, intent(in) :: thermo_flag + integer, optional, intent(out) :: errcode + character(len=*), optional, intent(out) :: errmsg + ! Local variable + character(len=*), parameter :: subname = 'ccpt_set_thermo_active' + + if (associated(this%prop)) then + if (this%prop%is_instantiated(errcode, errmsg)) then + this%prop%thermo_active = thermo_flag + end if + else + call set_errvars(1, subname//": invalid constituent pointer", & + errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_set_thermo_active + end module ccpp_constituent_prop_mod diff --git a/test/advection_test/cld_ice.F90 b/test/advection_test/cld_ice.F90 index 5a91282f..9c1e769a 100644 --- a/test/advection_test/cld_ice.F90 +++ b/test/advection_test/cld_ice.F90 @@ -19,7 +19,7 @@ MODULE cld_ice !> \section arg_table_cld_ice_run Argument Table !! \htmlinclude arg_table_cld_ice_run.html !! - subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & + subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -27,7 +27,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_ice(:,:) + REAL(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -44,7 +44,7 @@ subroutine cld_ice_run(ncol, timestep, temp, qv, ps, cld_ice, & do ilev = 1, size(temp, 2) if (temp(icol, ilev) < tcld) then frz = MAX(qv(icol, ilev) - 0.5_kind_phys, 0.0_kind_phys) - cld_ice(icol, ilev) = cld_ice(icol, ilev) + frz + cld_ice_array(icol, ilev) = cld_ice_array(icol, ilev) + frz qv(icol, ilev) = qv(icol, ilev) - frz if (frz > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + 1.0_kind_phys @@ -58,16 +58,16 @@ END SUBROUTINE cld_ice_run !> \section arg_table_cld_ice_init Argument Table !! \htmlinclude arg_table_cld_ice_init.html !! - subroutine cld_ice_init(tfreeze, cld_ice, errmsg, errflg) + subroutine cld_ice_init(tfreeze, cld_ice_array, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(inout) :: cld_ice(:,:) + real(kind_phys), intent(inout) :: cld_ice_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg errmsg = '' errflg = 0 - cld_ice = 0.0_kind_phys + cld_ice_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_ice_init diff --git a/test/advection_test/cld_ice.meta b/test/advection_test/cld_ice.meta index e4a961ab..010fb419 100644 --- a/test/advection_test/cld_ice.meta +++ b/test/advection_test/cld_ice.meta @@ -41,9 +41,10 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. + default_value = 0.0_kind_phys units = kg kg-1 dimensions = (horizontal_loop_extent, vertical_layer_dimension) type = real | kind = kind_phys @@ -73,9 +74,10 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_ice ] +[ cld_ice_array ] standard_name = cloud_ice_dry_mixing_ratio advected = .true. + default_value = 0.0_kind_phys units = kg kg-1 dimensions = (horizontal_dimension, vertical_layer_dimension) type = real | kind = kind_phys diff --git a/test/advection_test/cld_liq.F90 b/test/advection_test/cld_liq.F90 index 2e1e5a57..b9b841d4 100644 --- a/test/advection_test/cld_liq.F90 +++ b/test/advection_test/cld_liq.F90 @@ -16,7 +16,7 @@ MODULE cld_liq !> \section arg_table_cld_liq_run Argument Table !! \htmlinclude arg_table_cld_liq_run.html !! - subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & + subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq_array, & errmsg, errflg) integer, intent(in) :: ncol @@ -25,7 +25,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & real(kind_phys), intent(inout) :: temp(:,:) real(kind_phys), intent(inout) :: qv(:,:) real(kind_phys), intent(in) :: ps(:) - REAL(kind_phys), intent(inout) :: cld_liq(:,:) + REAL(kind_phys), intent(inout) :: cld_liq_array(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -43,7 +43,7 @@ subroutine cld_liq_run(ncol, timestep, tcld, temp, qv, ps, cld_liq, & if ( (qv(icol, ilev) > 0.0_kind_phys) .and. & (temp(icol, ilev) <= tcld)) then cond = MIN(qv(icol, ilev), 0.1_kind_phys) - cld_liq(icol, ilev) = cld_liq(icol, ilev) + cond + cld_liq_array(icol, ilev) = cld_liq_array(icol, ilev) + cond qv(icol, ilev) = qv(icol, ilev) - cond if (cond > 0.0_kind_phys) then temp(icol, ilev) = temp(icol, ilev) + (cond * 5.0_kind_phys) @@ -57,10 +57,10 @@ END SUBROUTINE cld_liq_run !> \section arg_table_cld_liq_init Argument Table !! \htmlinclude arg_table_cld_liq_init.html !! - subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) + subroutine cld_liq_init(tfreeze, cld_liq_array, tcld, errmsg, errflg) real(kind_phys), intent(in) :: tfreeze - real(kind_phys), intent(out) :: cld_liq(:,:) + real(kind_phys), intent(out) :: cld_liq_array(:,:) real(kind_phys), intent(out) :: tcld character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg @@ -69,7 +69,7 @@ subroutine cld_liq_init(tfreeze, cld_liq, tcld, errmsg, errflg) errmsg = '' errflg = 0 - cld_liq = 0.0_kind_phys + cld_liq_array = 0.0_kind_phys tcld = tfreeze - 20.0_kind_phys end subroutine cld_liq_init diff --git a/test/advection_test/cld_liq.meta b/test/advection_test/cld_liq.meta index 1186d741..4c87f4b7 100644 --- a/test/advection_test/cld_liq.meta +++ b/test/advection_test/cld_liq.meta @@ -47,7 +47,7 @@ units = Pa dimensions = (horizontal_loop_extent) intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 @@ -79,7 +79,7 @@ dimensions = () type = real | kind = kind_phys intent = in -[ cld_liq ] +[ cld_liq_array ] standard_name = cloud_liquid_dry_mixing_ratio advected = .true. units = kg kg-1 diff --git a/test/advection_test/test_host.F90 b/test/advection_test/test_host.F90 index 9c7abd42..49a3c602 100644 --- a/test/advection_test/test_host.F90 +++ b/test/advection_test/test_host.F90 @@ -33,15 +33,19 @@ module test_prog CONTAINS - subroutine check_errflg(subname, errflg, errmsg) + subroutine check_errflg(subname, errflg, errmsg, errflg_final) ! If errflg is not zero, print an error message - character(len=*), intent(in) :: subname - integer, intent(in) :: errflg - character(len=*), intent(in) :: errmsg + character(len=*), intent(in) :: subname + integer, intent(in) :: errflg + character(len=*), intent(in) :: errmsg + + integer, intent(out) :: errflg_final if (errflg /= 0) then write(6, '(a,i0,4a)') "Error ", errflg, " from ", trim(subname), & ':', trim(errmsg) + !Notify test script that a failure occurred: + errflg_final = -1 !Notify test script that a failure occured end if end subroutine check_errflg @@ -222,6 +226,7 @@ subroutine test_host(retval, test_suites) use test_host_mod, only: init_data, compare_data use test_host_mod, only: ncols, pver use test_host_ccpp_cap, only: test_host_ccpp_register_constituents + use test_host_ccpp_cap, only: test_host_ccpp_is_scheme_constituent use test_host_ccpp_cap, only: test_host_ccpp_initialize_constituents use test_host_ccpp_cap, only: test_host_ccpp_number_constituents use test_host_ccpp_cap, only: test_host_constituents_array @@ -245,14 +250,22 @@ subroutine test_host(retval, test_suites) integer :: num_suites integer :: num_advected ! Num advected species logical :: const_log + logical :: is_constituent + logical :: has_default character(len=128), allocatable :: suite_names(:) character(len=256) :: const_str character(len=512) :: errmsg integer :: errflg + integer :: errflg_final ! Used to notify testing script of test failure real(kind_phys), pointer :: const_ptr(:,:,:) + real(kind_phys) :: default_value type(ccpp_constituent_prop_ptr_t), pointer :: const_props(:) character(len=*), parameter :: subname = 'test_host' + ! Initialized "final" error flag used to report a failure to the larged + ! testing script: + errflg_final = 0 + ! Gather and test the inspection routines num_suites = size(test_suites) call ccpp_physics_suite_list(suite_names) @@ -280,112 +293,279 @@ subroutine test_host(retval, test_suites) return end if - ! Register the constituents to find out what needs advecting - call host_constituents(1)%instantiate(std_name="specific_humidity", & + errflg = 0 + errmsg = '' + + ! Check that is_scheme_constituent works as expected + call test_host_ccpp_is_scheme_constituent('specific_humidity', & + is_constituent, errflg, errmsg) + call check_errflg(subname//"_ccpp_is_scheme_constituent", errflg, & + errmsg, errflg_final) + ! specific_humidity should not be an existing constituent + if (is_constituent) then + write(6, *) "ERROR: specific humidity is already a constituent" + errflg_final = -1 !Notify test script that a failure occurred + end if + call test_host_ccpp_is_scheme_constituent('cloud_ice_dry_mixing_ratio', & + is_constituent, errflg, errmsg) + call check_errflg(subname//"_ccpp_is_scheme_constituent", errflg, & + errmsg, errflg_final) + ! cloud_ice_dry_mixing_ratio should be an existing constituent + if (.not. is_constituent) then + write(6, *) "ERROR: cloud_ice_dry_mixing ratio not found in ", & + "host cap constituent list" + errflg_final = -1 !Notify test script that a failure occurred + end if + + + ! Register the constituents to find out what needs advecting + call host_constituents(1)%instantiate(std_name="specific_humidity", & long_name="Specific humidity", units="kg kg-1", & vertical_dim="vertical_layer_dimension", advected=.true., & errcode=errflg, errmsg=errmsg) - call check_errflg(subname//'.initialize', errflg, errmsg) + call check_errflg(subname//'.initialize', errflg, errmsg, errflg_final) if (errflg == 0) then call test_host_ccpp_register_constituents(suite_names(:), & host_constituents, errmsg=errmsg, errflg=errflg) end if if (errflg /= 0) then write(6, '(2a)') 'ERROR register_constituents: ', trim(errmsg) + retval = .false. + return end if ! Check number of advected constituents if (errflg == 0) then call test_host_ccpp_number_constituents(num_advected, errmsg=errmsg, & errflg=errflg) - call check_errflg(subname//".num_advected", errflg, errmsg) + call check_errflg(subname//".num_advected", errflg, errmsg, errflg_final) end if if (num_advected /= 3) then write(6, '(a,i0)') "ERROR: num advected constituents = ", num_advected - STOP 2 + retval = .false. + return end if ! Initialize constituent data call test_host_ccpp_initialize_constituents(ncols, pver, errflg, errmsg) - ! Initialize our 'data' - if (errflg == 0) then - const_ptr => test_host_constituents_array() - call test_host_const_get_index('specific_humidity', index, & - errflg, errmsg) - call check_errflg(subname//".index_specific_humidity", errflg, errmsg) - end if - if (errflg == 0) then - call test_host_const_get_index('cloud_liquid_dry_mixing_ratio', & - index_liq, errflg, errmsg) - call check_errflg(subname//".index_cld_liq", errflg, errmsg) + !Stop tests here if initialization failed (as all other tests will likely + !fail as well: + if (errflg /= 0) then + retval = .false. + return end if - if (errflg == 0) then - call test_host_const_get_index('cloud_ice_dry_mixing_ratio', & - index_ice, errflg, errmsg) - call check_errflg(subname//".index_cld_ice", errflg, errmsg) + + ! Initialize our 'data' + const_ptr => test_host_constituents_array() + + !Check if the specific humidity index can be found: + call test_host_const_get_index('specific_humidity', index, & + errflg, errmsg) + call check_errflg(subname//".index_specific_humidity", errflg, errmsg, & + errflg_final) + + !Check if the cloud liquid index can be found: + call test_host_const_get_index('cloud_liquid_dry_mixing_ratio', & + index_liq, errflg, errmsg) + call check_errflg(subname//".index_cld_liq", errflg, errmsg, & + errflg_final) + + !Check if the cloud ice index can be found: + call test_host_const_get_index('cloud_ice_dry_mixing_ratio', & + index_ice, errflg, errmsg) + call check_errflg(subname//".index_cld_ice", errflg, errmsg, & + errflg_final) + + !Stop tests here if the index checks failed, as all other tests will + !likely fail as well: + if (errflg_final /= 0) then + retval = .false. + return end if + call init_data(const_ptr, index, index_liq, index_ice) + ! Check some constituent properties - if (errflg == 0) then - const_props => test_host_model_const_properties() - call const_props(index)%standard_name(const_str, errflg, errmsg) - if (errflg /= 0) then - write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & - "to get standard_name for specific_humidity, index = ", & - index, trim(errmsg) - end if + !++++++++++++++++++++++++++++++++++ + + const_props => test_host_model_const_properties() + + !Standard name: + call const_props(index)%standard_name(const_str, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get standard_name for specific_humidity, index = ", & + index, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occured end if if (errflg == 0) then if (trim(const_str) /= 'specific_humidity') then write(6, *) "ERROR: standard name, '", trim(const_str), & "' should be 'specific_humidity'" - errflg = -1 + errflg_final = -1 !Notify test script that a failure occured end if + else + !Reset error flag to continue testing other properties: + errflg = 0 end if - if (errflg == 0) then - call const_props(index_liq)%long_name(const_str, errflg, errmsg) - if (errflg /= 0) then - write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & - "to get long_name for cld_liq index = ", & - index_liq, trim(errmsg) - end if + + !Long name: + call const_props(index_liq)%long_name(const_str, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get long_name for cld_liq index = ", & + index_liq, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occured end if if (errflg == 0) then if (trim(const_str) /= 'Cloud liquid dry mixing ratio') then write(6, *) "ERROR: long name, '", trim(const_str), & "' should be 'Cloud liquid dry mixing ratio'" - errflg = -1 + errflg_final = -1 !Notify test script that a failure occured end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + + !Mass mixing ratio: + call const_props(index_ice)%is_mass_mixing_ratio(const_log, errflg, & + errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get mass mixing ratio prop for cld_ice index = ", & + index_ice, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occured end if if (errflg == 0) then - call const_props(index_ice)%is_mass_mixing_ratio(const_log, & - errflg, errmsg) - if (errflg /= 0) then - write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & - "to get mass mixing ratio prop for cld_ice index = ", & - index_ice, trim(errmsg) + if (.not. const_log) then + write(6, *) "ERROR: cloud ice is not a mass mixing_ratio" + errflg_final = -1 !Notify test script that a failure occured end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + + !Dry mixing ratio: + call const_props(index_ice)%is_dry(const_log, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get dry prop for cld_ice index = ", index_ice, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred end if if (errflg == 0) then if (.not. const_log) then - write(6, *) "ERROR: cloud ice is not a mass mixing_ratio" - errflg = -1 + write(6, *) "ERROR: cloud ice mass_mixing_ratio is not dry" + errflg_final = -1 + end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + + !Check that being thermodynamically active defaults to False: + call const_props(index_ice)%is_thermo_active(check, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to get thermo_active prop for cld_ice index = ", index_ice, & + trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred + end if + if (errflg == 0) then + if (check) then !Should be False + write(6, *) "ERROR: 'is_thermo_active' should default to False ", & + "for all constituents unless set by host model." + errflg_final = -1 !Notify test script that a failure occured end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + + !Check that setting a constituent to be thermodynamically active works + !as expected: + call const_props(index_ice)%set_thermo_active(.true., errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,a,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to set thermo_active prop for cld_ice index = ", index_ice, & + trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred end if if (errflg == 0) then - call const_props(index_ice)%is_dry(const_log, errflg, errmsg) + call const_props(index_ice)%is_thermo_active(check, errflg, errmsg) if (errflg /= 0) then - write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & - "to get dry prop for cld_ice index = ", index_ice, trim(errmsg) + write(6, '(a,i0,a,i0,/,a)') "ERROR: Error, ", errflg, & + " tryingto get thermo_active prop for cld_ice index = ", & + index_ice, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred end if end if if (errflg == 0) then - if (.not. const_log) then - write(6, *) "ERROR: cloud ice mass_mixing_ratio is not dry" - errflg = -1 + if (.not.check) then !Should now be True + write(6, *) "ERROR: 'set_thermo_active' did not set", & + " thermo_active constituent property correctly." + errflg_final = -1 !Notify test script that a failure occurred + end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + + !Check that setting a constituent's default value works as expected + call const_props(index_liq)%has_default(has_default, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,2a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to check for default for cld_liq index = ", index_liq, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred + end if + if (errflg == 0) then + if (has_default) then + write(6, *) "ERROR: cloud liquid mass_mixing_ratio should not have default but does" + errflg_final = -1 !Notify test script that a failure occurred + end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + call const_props(index_ice)%has_default(has_default, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,2a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to check for default for cld_ice index = ", index_ice, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred + end if + if (errflg == 0) then + if (.not. has_default) then + write(6, *) "ERROR: cloud ice mass_mixing_ratio should have default but doesn't" + errflg_final = -1 !Notify test script that a failure occurred end if + else + !Reset error flag to continue testing other properties: + errflg = 0 end if + call const_props(index_ice)%default_value(default_value, errflg, errmsg) + if (errflg /= 0) then + write(6, '(a,i0,2a,i0,/,a)') "ERROR: Error, ", errflg, " trying ", & + "to grab default for cld_ice index = ", index_ice, trim(errmsg) + errflg_final = -1 !Notify test script that a failure occurred + end if + if (errflg == 0) then + if (default_value /= 0.0_kind_phys) then + write(6, *) "ERROR: cloud ice mass_mixing_ratio default is ", default_value, & + " but should be 0.0" + errflg_final = -1 !Notify test script that a failure occurred + end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + end if + !++++++++++++++++++++++++++++++++++ - ! Use the suite information to setup the run + !Set error flag to the "final" value, because any error + !above will likely result in a large number of failures + !below: + errflg = errflg_final + + ! Use the suite information to setup the run do sind = 1, num_suites if (errflg == 0) then call test_host_ccpp_physics_initialize( & @@ -397,6 +577,7 @@ subroutine test_host(retval, test_suites) end if end if end do + ! Loop over time steps do time_step = 1, num_time_steps ! Initialize the timestep @@ -478,7 +659,13 @@ subroutine test_host(retval, test_suites) end if end if - retval = errflg == 0 + !Make sure "final" flag is non-zero if "errflg" is: + if (errflg /= 0) then + errflg_final = -1 !Notify test script that a failure occured + end if + + !Set return value to False if any errors were found: + retval = errflg_final == 0 end subroutine test_host @@ -490,9 +677,9 @@ program test implicit none character(len=cs), target :: test_parts1(1) - character(len=cm), target :: test_invars1(6) - character(len=cm), target :: test_outvars1(5) - character(len=cm), target :: test_reqvars1(8) + character(len=cm), target :: test_invars1(7) + character(len=cm), target :: test_outvars1(6) + character(len=cm), target :: test_reqvars1(9) type(suite_info) :: test_suites(1) logical :: run_okay @@ -500,6 +687,7 @@ program test test_parts1 = (/ 'physics '/) test_invars1 = (/ & 'cloud_ice_dry_mixing_ratio ', & + 'cloud_liquid_dry_mixing_ratio ', & 'surface_air_pressure ', & 'temperature ', & 'time_step_for_physics ', & @@ -510,11 +698,13 @@ program test 'ccpp_error_code ', & 'temperature ', & 'water_vapor_specific_humidity ', & + 'cloud_liquid_dry_mixing_ratio ', & 'cloud_ice_dry_mixing_ratio ' /) test_reqvars1 = (/ & 'surface_air_pressure ', & 'temperature ', & 'time_step_for_physics ', & + 'cloud_liquid_dry_mixing_ratio ', & 'cloud_ice_dry_mixing_ratio ', & 'water_temperature_at_freezing ', & 'water_vapor_specific_humidity ', & From 3c6a9e2fb2649cecf911e19a1e3e10a24895de63 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:07:42 -0600 Subject: [PATCH 14/49] add code owners --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 97c9a2a7..22f6e1b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,7 +3,7 @@ # These owners will be the default owners for everything in the repo. #* @defunkt -* @climbfuji @gold2718 +* @climbfuji @gold2718 @dustinswales @mwaxmonsky @peverwhee # Order is important. The last matching pattern has the most precedence. # So if a pull request only touches javascript files, only these owners From b0851398428635d9de1d7beaf28df3e5f3bdfee4 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:09:55 -0600 Subject: [PATCH 15/49] fix grammar --- test/run_fortran_tests.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/run_fortran_tests.sh b/test/run_fortran_tests.sh index 3b3512c2..8b0f5bcb 100755 --- a/test/run_fortran_tests.sh +++ b/test/run_fortran_tests.sh @@ -50,6 +50,11 @@ echo "Skipping var_action_test/run_test until feature is fully implemented" if [ $errcnt -eq 0 ]; then echo "All tests PASSed!" else - echo "${errcnt} tests FAILed" + if [ $errcnt -eq 1 ]; then + echo "${errcnt} test FAILed" + else + echo "${errcnt} tests FAILed" + fi + #Exit with non-zero exit code exit 1 fi From 73ef0a397058be29b1ad4c7a3bfd0f8d73f689a5 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:19:26 -0600 Subject: [PATCH 16/49] add ddt testing and update constituent standard names --- src/ccpp_constituent_prop_mod.meta | 16 +- .../sample_host_files/data1_mod.F90 | 11 + .../sample_host_files/data1_mod.meta | 25 ++ test/unit_tests/sample_host_files/ddt1.F90 | 17 ++ test/unit_tests/sample_host_files/ddt1.meta | 20 ++ .../sample_host_files/ddt1_plus.F90 | 33 +++ .../sample_host_files/ddt1_plus.meta | 20 ++ test/unit_tests/sample_host_files/ddt2.F90 | 24 ++ test/unit_tests/sample_host_files/ddt2.meta | 29 ++ .../sample_host_files/ddt2_extra_var.F90 | 34 +++ .../sample_host_files/ddt2_extra_var.meta | 34 +++ .../sample_host_files/ddt_data1_mod.F90 | 30 ++ .../sample_host_files/ddt_data1_mod.meta | 56 ++++ test/unit_tests/test_metadata_host_file.py | 261 ++++++++++++++++++ 14 files changed, 602 insertions(+), 8 deletions(-) create mode 100644 test/unit_tests/sample_host_files/data1_mod.F90 create mode 100644 test/unit_tests/sample_host_files/data1_mod.meta create mode 100644 test/unit_tests/sample_host_files/ddt1.F90 create mode 100644 test/unit_tests/sample_host_files/ddt1.meta create mode 100644 test/unit_tests/sample_host_files/ddt1_plus.F90 create mode 100644 test/unit_tests/sample_host_files/ddt1_plus.meta create mode 100644 test/unit_tests/sample_host_files/ddt2.F90 create mode 100644 test/unit_tests/sample_host_files/ddt2.meta create mode 100644 test/unit_tests/sample_host_files/ddt2_extra_var.F90 create mode 100644 test/unit_tests/sample_host_files/ddt2_extra_var.meta create mode 100644 test/unit_tests/sample_host_files/ddt_data1_mod.F90 create mode 100644 test/unit_tests/sample_host_files/ddt_data1_mod.meta create mode 100644 test/unit_tests/test_metadata_host_file.py diff --git a/src/ccpp_constituent_prop_mod.meta b/src/ccpp_constituent_prop_mod.meta index dd60eb13..99cf3145 100644 --- a/src/ccpp_constituent_prop_mod.meta +++ b/src/ccpp_constituent_prop_mod.meta @@ -16,32 +16,32 @@ name = ccpp_model_constituents_t type = ddt [ num_layer_vars ] - standard_name = ccpp_num_constituents + standard_name = number_of_ccpp_constituents long_name = Number of constituents managed by CCPP Framework units = count dimensions = () type = integer [ num_advected_vars ] - standard_name = ccpp_num_advected_constituents + standard_name = number_of_ccpp_advected_constituents long_name = Number of advected constituents managed by CCPP Framework units = count dimensions = () type = integer [ vars_layer ] - standard_name = ccpp_constituent_array + standard_name = ccpp_constituents long_name = Array of constituents managed by CCPP Framework units = none state_variable = true - dimensions = (horizontal_dimension, vertical_layer_dimension, ccpp_num_constituents) + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) type = real | kind = kind_phys [ const_metadata ] - standard_name = ccpp_constituent_properties_array + standard_name = ccpp_constituent_properties units = None type = ccpp_constituent_prop_ptr_t - dimensions = (ccpp_num_constituents) + dimensions = (number_of_ccpp_constituents) [ vars_minvalue ] - standard_name = ccpp_constituent_array_minimum_values + standard_name = ccpp_constituent_minimum_values units = kg kg-1 type = real | kind = kind_phys - dimensions = (ccpp_num_constituents) + dimensions = (number_of_ccpp_constituents) protected = True diff --git a/test/unit_tests/sample_host_files/data1_mod.F90 b/test/unit_tests/sample_host_files/data1_mod.F90 new file mode 100644 index 00000000..b85db315 --- /dev/null +++ b/test/unit_tests/sample_host_files/data1_mod.F90 @@ -0,0 +1,11 @@ +module data1_mod + + use ccpp_kinds, only: kind_phys + + !> \section arg_table_data1_mod Argument Table + !! \htmlinclude arg_table_data1_mod.html + real(kind_phys) :: ps1 + real(kind_phys), allocatable :: xbox(:,:) + real(kind_phys), allocatable :: switch(:,:) + +end module data1_mod diff --git a/test/unit_tests/sample_host_files/data1_mod.meta b/test/unit_tests/sample_host_files/data1_mod.meta new file mode 100644 index 00000000..37e2de96 --- /dev/null +++ b/test/unit_tests/sample_host_files/data1_mod.meta @@ -0,0 +1,25 @@ +[ccpp-table-properties] + name = data1_mod + type = module +[ccpp-arg-table] + name = data1_mod + type = module +[ ps1 ] + standard_name = play_station + state_variable = true + type = real | kind = kind_phys + units = Pa + dimensions = () +[ xbox ] + standard_name = xbox + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) +[ switch ] + standard_name = nintendo_switch + long_name = Incompatible junk + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) diff --git a/test/unit_tests/sample_host_files/ddt1.F90 b/test/unit_tests/sample_host_files/ddt1.F90 new file mode 100644 index 00000000..966a62a6 --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt1.F90 @@ -0,0 +1,17 @@ +module ddt1 + + use ccpp_kinds, only: kind_phys + + private + implicit none + + !! \section arg_table_ddt1_t + !! \htmlinclude ddt1_t.html + !! + type, public :: ddt1_t + integer, private :: num_vars = 0 + real(kind_phys), allocatable :: vars(:,:,:) + + end type ddt1_t + +end module ddt1 diff --git a/test/unit_tests/sample_host_files/ddt1.meta b/test/unit_tests/sample_host_files/ddt1.meta new file mode 100644 index 00000000..e1a0f1ac --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt1.meta @@ -0,0 +1,20 @@ +######################################################################## +[ccpp-table-properties] + name = ddt1_t + type = ddt + +[ccpp-arg-table] + name = ddt1_t + type = ddt +[ num_vars ] + standard_name = ddt_var_array_dimension + long_name = Number of vars managed by ddt1 + units = count + dimensions = () + type = integer +[ vars ] + standard_name = vars_array + long_name = Array of vars managed by ddt1 + units = none + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys diff --git a/test/unit_tests/sample_host_files/ddt1_plus.F90 b/test/unit_tests/sample_host_files/ddt1_plus.F90 new file mode 100644 index 00000000..90571a73 --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt1_plus.F90 @@ -0,0 +1,33 @@ +module ddt1_plus + + use ccpp_kinds, only: kind_phys + + private + implicit none + + !! + type, public :: ddt1_t + real, pointer :: undocumented_array(:) => NULL() + contains + procedure :: this_is_a_documented_object + end type ddt1_t + + !! \section arg_table_ddt2_t + !! \htmlinclude ddt2_t.html + !! + type, public :: ddt2_t + integer, private :: num_vars = 0 + real(kind_phys), allocatable :: vars(:,:,:) + + end type ddt2_t + +CONTAINS + + logical function this_is_a_documented_object(this) + class(ddt1_t) :: intent(in) :: this + + this_is_a_documented_object = .false. + + end function this_is_a_documented_object + +end module ddt1_plus diff --git a/test/unit_tests/sample_host_files/ddt1_plus.meta b/test/unit_tests/sample_host_files/ddt1_plus.meta new file mode 100644 index 00000000..ca3a92ab --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt1_plus.meta @@ -0,0 +1,20 @@ +######################################################################## +[ccpp-table-properties] + name = ddt2_t + type = ddt + +[ccpp-arg-table] + name = ddt2_t + type = ddt +[ num_vars ] + standard_name = ddt_var_array_dimension + long_name = Number of vars managed by ddt2 + units = count + dimensions = () + type = integer +[ vars ] + standard_name = vars_array + long_name = Array of vars managed by ddt2 + units = none + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys diff --git a/test/unit_tests/sample_host_files/ddt2.F90 b/test/unit_tests/sample_host_files/ddt2.F90 new file mode 100644 index 00000000..f4ed9cd0 --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt2.F90 @@ -0,0 +1,24 @@ +module ddt2 + + use ccpp_kinds, only: kind_phys + + private + implicit none + + !! \section arg_table_ddt1_t + !! \htmlinclude ddt1_t.html + !! + type, public :: ddt1_t + real, pointer :: undocumented_array(:) => NULL() + end type ddt1_t + + !! \section arg_table_ddt2_t + !! \htmlinclude ddt2_t.html + !! + type, public :: ddt2_t + integer, private :: num_vars = 0 + real(kind_phys), allocatable :: vars(:,:,:) + + end type ddt2_t + +end module ddt2 diff --git a/test/unit_tests/sample_host_files/ddt2.meta b/test/unit_tests/sample_host_files/ddt2.meta new file mode 100644 index 00000000..159f08b0 --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt2.meta @@ -0,0 +1,29 @@ +######################################################################## +[ccpp-table-properties] + name = ddt1_t + type = ddt + +[ccpp-arg-table] + name = ddt1_t + type = ddt + +######################################################################## +[ccpp-table-properties] + name = ddt2_t + type = ddt + +[ccpp-arg-table] + name = ddt2_t + type = ddt +[ num_vars ] + standard_name = ddt_var_array_dimension + long_name = Number of vars managed by ddt2 + units = count + dimensions = () + type = integer +[ vars ] + standard_name = vars_array + long_name = Array of vars managed by ddt2 + units = none + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys diff --git a/test/unit_tests/sample_host_files/ddt2_extra_var.F90 b/test/unit_tests/sample_host_files/ddt2_extra_var.F90 new file mode 100644 index 00000000..07bafe4b --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt2_extra_var.F90 @@ -0,0 +1,34 @@ +module ddt2_extra_var + + use ccpp_kinds, only: kind_phys + + private + implicit none + + !! \section arg_table_ddt1_t + !! \htmlinclude ddt1_t.html + !! + type, public :: ddt1_t + real, pointer :: undocumented_array(:) => NULL() + end type ddt1_t + + !! \section arg_table_ddt2_t + !! \htmlinclude ddt2_t.html + !! + type, public :: ddt2_t + integer, private :: num_vars = 0 + real(kind_phys), allocatable :: vars(:,:,:) + contains + procedure :: get_num_vars + end type ddt2_t + +CONTAINS + + integer function get_num_vars(this) + class(ddt2_t), intent(in) :: this + + get_num_vars = this%num_vars + + end function get_num_vars + +end module ddt2_extra_var diff --git a/test/unit_tests/sample_host_files/ddt2_extra_var.meta b/test/unit_tests/sample_host_files/ddt2_extra_var.meta new file mode 100644 index 00000000..867720e5 --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt2_extra_var.meta @@ -0,0 +1,34 @@ +######################################################################## +[ccpp-table-properties] + name = ddt1_t + type = ddt + +[ccpp-arg-table] + name = ddt1_t + type = ddt + +######################################################################## +[ccpp-table-properties] + name = ddt2_t + type = ddt + +[ccpp-arg-table] + name = ddt2_t + type = ddt +[ num_vars ] + standard_name = ddt_var_array_dimension + long_name = Number of vars managed by ddt2 + units = count + dimensions = () + type = integer +[ vars ] + standard_name = vars_array + long_name = Array of vars managed by ddt2 + units = none + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys +[ bogus ] + standard_name = misplaced_variable + units = count + dimensions = () + type = integer diff --git a/test/unit_tests/sample_host_files/ddt_data1_mod.F90 b/test/unit_tests/sample_host_files/ddt_data1_mod.F90 new file mode 100644 index 00000000..0607f11c --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt_data1_mod.F90 @@ -0,0 +1,30 @@ +module ddt_data1_mod + + use ccpp_kinds, only: kind_phys + + private + implicit none + + !! \section arg_table_ddt1_t + !! \htmlinclude ddt1_t.html + !! + type, public :: ddt1_t + real, pointer :: undocumented_array(:) => NULL() + end type ddt1_t + + !! \section arg_table_ddt2_t + !! \htmlinclude ddt2_t.html + !! + type, public :: ddt2_t + integer, private :: num_vars = 0 + real(kind_phys), allocatable :: vars(:,:,:) + + end type ddt2_t + + !> \section arg_table_ddt_data1_mod Argument Table + !! \htmlinclude arg_table_ddt_data1_mod.html + real(kind_phys) :: ps1 + real(kind_phys), allocatable :: xbox(:,:) + real(kind_phys), allocatable :: switch(:,:) + +end module ddt_data1_mod diff --git a/test/unit_tests/sample_host_files/ddt_data1_mod.meta b/test/unit_tests/sample_host_files/ddt_data1_mod.meta new file mode 100644 index 00000000..e149c07b --- /dev/null +++ b/test/unit_tests/sample_host_files/ddt_data1_mod.meta @@ -0,0 +1,56 @@ +######################################################################## +[ccpp-table-properties] + name = ddt1_t + type = ddt + +[ccpp-arg-table] + name = ddt1_t + type = ddt + +######################################################################## +[ccpp-table-properties] + name = ddt2_t + type = ddt + +[ccpp-arg-table] + name = ddt2_t + type = ddt +[ num_vars ] + standard_name = ddt_var_array_dimension + long_name = Number of vars managed by ddt2 + units = count + dimensions = () + type = integer +[ vars ] + standard_name = vars_array + long_name = Array of vars managed by ddt2 + units = none + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys + +######################################################################## +[ccpp-table-properties] + name = ddt_data1_mod + type = module +[ccpp-arg-table] + name = ddt_data1_mod + type = module +[ ps1 ] + standard_name = play_station + state_variable = true + type = real | kind = kind_phys + units = Pa + dimensions = () +[ xbox ] + standard_name = xbox + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) +[ switch ] + standard_name = nintendo_switch + long_name = Incompatible junk + state_variable = true + type = real | kind = kind_phys + units = m s-1 + dimensions = (horizontal_dimension, vertical_layer_dimension) diff --git a/test/unit_tests/test_metadata_host_file.py b/test/unit_tests/test_metadata_host_file.py new file mode 100644 index 00000000..d01fda97 --- /dev/null +++ b/test/unit_tests/test_metadata_host_file.py @@ -0,0 +1,261 @@ +#! /usr/bin/env python3 + +""" +----------------------------------------------------------------------- + Description: capgen needs to compare a metadata header against the + associated CCPP Fortran interface routine. This set of + tests is testing the parse_host_model_files function in + ccpp_capgen.py which performs the operations in the first + bullet below. Each test calls this function. + + * This script contains unit tests that do the following: + 1) Read one or more metadata files (to collect + the metadata headers) + 2) Read the associated CCPP Fortran host file(s) (to + collect Fortran interfaces) + 3) Create a CCPP host model object + 3) Test the properties of the CCPP host model object + + * Tests include: + - Correctly parse and match a simple module file with + data fields (a data block) + - Correctly parse and match a simple module file with + a DDT definition + - Correctly parse and match a simple module file with + two DDT definitions + - Correctly parse and match a simple module file with + two DDT definitions and a data block + + Assumptions: + + Command line arguments: none + + Usage: python3 test_metadata_host_file.py # run the unit tests +----------------------------------------------------------------------- +""" +import sys +import os +import logging +import unittest + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError("Cannot find scripts directory") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from ccpp_capgen import parse_host_model_files +from framework_env import CCPPFrameworkEnv +from parse_tools import CCPPError +# pylint: enable=wrong-import-position + +class MetadataHeaderTestCase(unittest.TestCase): + """Unit tests for parse_host_model_files""" + + def setUp(self): + """Setup important directories and logging""" + self._sample_files_dir = os.path.join(_TEST_DIR, "sample_host_files") + logger = logging.getLogger(self.__class__.__name__) + self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', + 'scheme_files':'', + 'suites':''}) + + def test_module_with_data(self): + """Test that a module containing a data block is parsed and matched + correctly.""" + # Setup + module_files = [os.path.join(self._sample_files_dir, "data1_mod.meta")] + # Exercise + hname = 'host_name_data1' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue('data1_mod' in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('play_station' in std_names) + self.assertTrue('xbox' in std_names) + self.assertTrue('nintendo_switch' in std_names) + + def test_module_with_one_ddt(self): + """Test that a module containing a DDT definition is parsed and matched + correctly.""" + # Setup + ddt_name = 'ddt1_t' + module_files = [os.path.join(self._sample_files_dir, "ddt1.meta")] + # Exercise + hname = 'host_name_ddt1' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue(ddt_name in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that the DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), 2) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('ddt_var_array_dimension' in std_names) + self.assertTrue('vars_array' in std_names) + + def test_module_with_two_ddts(self): + """Test that a module containing two DDT definitions is parsed and + matched correctly.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, "ddt2.meta")] + # Exercise + hname = 'host_name_ddt2' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), len(ddt_names)) + # Verify header titles + for ddt_name in ddt_names: + self.assertTrue(ddt_name in module_headers) + # end for + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that each DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + for index, ddt_name in enumerate(ddt_names): + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), len(ddt_vars[index])) + std_names = [x.get_prop_value('standard_name') for x in vlist] + for sname in ddt_vars[index]: + self.assertTrue(sname in std_names) + # end for + # end for + + def test_module_with_two_ddts_and_data(self): + """Test that a module containing two DDT definitions and a block of + module data is parsed and matched correctly.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, + "ddt_data1_mod.meta")] + # Exercise + hname = 'host_name_ddt_data' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), len(ddt_names) + 1) + # Verify header titles + for ddt_name in ddt_names: + self.assertTrue(ddt_name in module_headers) + # end for + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + # Verify that each DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + for index, ddt_name in enumerate(ddt_names): + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), len(ddt_vars[index])) + std_names = [x.get_prop_value('standard_name') for x in vlist] + for sname in ddt_vars[index]: + self.assertTrue(sname in std_names) + # end for + # end for + # Verify header titles + self.assertTrue('ddt_data1_mod' in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 3) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('play_station' in std_names) + self.assertTrue('xbox' in std_names) + self.assertTrue('nintendo_switch' in std_names) + + def test_module_with_one_ddt_plus_undoc(self): + """Test that a module containing a one documented DDT definition + (i.e., with metadata) and one DDT without (i.e., no metadata) + is parsed and matched correctly.""" + # Setup + ddt_name = 'ddt2_t' + module_files = [os.path.join(self._sample_files_dir, "ddt1_plus.meta")] + # Exercise + hname = 'host_name_ddt1_plus' + host_model = parse_host_model_files(module_files, hname, self._run_env) + # Verify the name of the host model + self.assertEqual(host_model.name, hname) + module_headers = host_model.metadata_tables() + self.assertEqual(len(module_headers), 1) + # Verify header titles + self.assertTrue(ddt_name in module_headers) + # Verify host model variable list + vlist = host_model.variable_list() + self.assertEqual(len(vlist), 0) + # Verify that the DDT was found and parsed + ddt_lib = host_model.ddt_lib + self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") + # Check DDT variables + ddt_mod = ddt_lib[ddt_name] + self.assertEqual(ddt_mod.name, ddt_name) + vlist = ddt_mod.variable_list() + self.assertEqual(len(vlist), 2) + std_names = [x.get_prop_value('standard_name') for x in vlist] + self.assertTrue('ddt_var_array_dimension' in std_names) + self.assertTrue('vars_array' in std_names) + + def test_module_with_two_ddts_and_extra_var(self): + """Test that a module containing two DDT definitions is parsed and + a useful error message is produced if the DDT metadata has an + extra variable.""" + # Setup + ddt_names = ['ddt1_t', 'ddt2_t'] + ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] + + module_files = [os.path.join(self._sample_files_dir, + "ddt2_extra_var.meta")] + # Exercise + hname = 'host_name_ddt_extra_var' + with self.assertRaises(CCPPError) as context: + host_model = parse_host_model_files(module_files, hname, + self._run_env) + # end with + # Check error messages + except_str = str(context.exception) + emsgs = ["Variable mismatch in ddt2_t, variables missing from Fortran ddt.", + + "No Fortran variable for bogus in ddt2_t", + "2 errors found comparing"] + for emsg in emsgs: + self.assertTrue(emsg in except_str) + # end for + +if __name__ == "__main__": + unittest.main() + From 1be9b3d064fac5bb183eaee2dd4b9e9f4b37371c Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:22:48 -0600 Subject: [PATCH 17/49] code cleanup --- test/capgen_test/test_host.F90 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/capgen_test/test_host.F90 b/test/capgen_test/test_host.F90 index 2114cdac..55c7159e 100644 --- a/test/capgen_test/test_host.F90 +++ b/test/capgen_test/test_host.F90 @@ -130,7 +130,7 @@ logical function check_suite(test_suite) suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR 1 ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -144,7 +144,7 @@ logical function check_suite(test_suite) 'input variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR 2 ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -158,7 +158,7 @@ logical function check_suite(test_suite) 'output variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR 3 ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then @@ -172,7 +172,7 @@ logical function check_suite(test_suite) 'required variable names', suite_name=test_suite%suite_name) else check = .false. - write(6, '(a,i0,2a)') 'ERROR 4 ', errflg, ': ', trim(errmsg) + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) end if check_suite = check_suite .and. check if (allocated(test_list)) then From a30888726e1f8638b0d4e01c8185e6a49e69ef8b Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:39:14 -0600 Subject: [PATCH 18/49] remove tests that should be in constituents branch --- test/unit_tests/test_fortran_write.py | 127 -------- test/unit_tests/test_metadata_host_file.py | 261 --------------- test/unit_tests/test_metadata_scheme_file.py | 326 ++++++++----------- 3 files changed, 132 insertions(+), 582 deletions(-) delete mode 100644 test/unit_tests/test_fortran_write.py delete mode 100644 test/unit_tests/test_metadata_host_file.py diff --git a/test/unit_tests/test_fortran_write.py b/test/unit_tests/test_fortran_write.py deleted file mode 100644 index 87e64baa..00000000 --- a/test/unit_tests/test_fortran_write.py +++ /dev/null @@ -1,127 +0,0 @@ -#! /usr/bin/env python3 -""" ------------------------------------------------------------------------ - Description: Contains unit tests for FortranWriter - in scripts file fortran/fortran_write.py - - Assumptions: - - Command line arguments: none - - Usage: python3 test_fortran_write.py # run the unit tests ------------------------------------------------------------------------ -""" - -import filecmp -import glob -import os -import sys -import unittest - -_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, - os.pardir, "scripts")) -_SAMPLE_FILES_DIR = os.path.join(_TEST_DIR, "sample_files", "fortran_files") -_PRE_TMP_DIR = os.path.join(_TEST_DIR, "tmp") -_TMP_DIR = os.path.join(_PRE_TMP_DIR, "fortran_files") - -if not os.path.exists(_SCRIPTS_DIR): - raise ImportError(f"Cannot find scripts directory, {_SCRIPTS_DIR}") - -sys.path.append(_SCRIPTS_DIR) - -# pylint: disable=wrong-import-position -from fortran_tools import FortranWriter -# pylint: enable=wrong-import-position - -############################################################################### -def remove_files(file_list): -############################################################################### - """Remove files in if they exist""" - if isinstance(file_list, str): - file_list = [file_list] - # end if - for fpath in file_list: - if os.path.exists(fpath): - os.remove(fpath) - # End if - # End for - -class MetadataTableTestCase(unittest.TestCase): - - """Tests for `FortranWriter`.""" - - @classmethod - def setUpClass(cls): - """Clean output directory (tmp) before running tests""" - #Does "tmp" directory exist? If not then create it: - if not os.path.exists(_PRE_TMP_DIR): - os.mkdir(_PRE_TMP_DIR) - # Ensure the "sample_files/fortran_files" directory exists and is empty - if os.path.exists(_TMP_DIR): - # Clear out all files: - remove_files(glob.iglob(os.path.join(_TMP_DIR, '*.*'))) - else: - os.makedirs(_TMP_DIR) - # end if - - #Run inherited setup method: - super().setUpClass() - - def test_line_breaking(self): - """Test that FortranWriter correctly breaks long lines""" - # Setup - testname = "linebreak_test" - compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") - generate = os.path.join(_TMP_DIR, f"{testname}.F90") - # Exercise - header = "Test of line breaking for FortranWriter" - with FortranWriter(generate, 'w', header, f"{testname}") as gen: - # Test long declaration - data_items = ', '.join([f"name{x:03}" for x in range(100)]) - gen.write(f"character(len=7) :: data = (/ {data_items} /)", 1) - gen.end_module_header() - # Test long code lines - line_items = ["call endrun('Cannot read columns_on_task from ", - "file'//', columns_on_task has no horizontal ", - "dimension; columns_on_task is a ", - "protected variable')"] - gen.write(f"{''.join(line_items)}", 2) - # end with - - # Check that file was generated - amsg = f"{generate} does not exist" - self.assertTrue(os.path.exists(generate), msg=amsg) - amsg = f"{generate} does not match {compare}" - self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) - - def test_good_comments(self): - """Test that comments are written and broken correctly.""" - # Setup - testname = "comments_test" - compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") - generate = os.path.join(_TMP_DIR, f"{testname}.F90") - # Exercise - header = "Test of comment writing for FortranWriter" - with FortranWriter(generate, 'w', header, f"{testname}") as gen: - gen.comment("We can write comments in the module header", 0) - gen.comment("We can write indented comments in the header", 1) - gen.write("integer :: foo ! Comment at end of line works", 1) - # Test long comments at end of line - gen.write(f"integer :: bar ! {'x'*100}", 1) - gen.write(f"integer :: baz ! {'y'*130}", 1) - gen.end_module_header() - # Test comment line in body - gen.comment("We can write comments in the module body", 1) - - # end with - - # Check that file was generated - amsg = f"{generate} does not exist" - self.assertTrue(os.path.exists(generate), msg=amsg) - amsg = f"{generate} does not match {compare}" - self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) - -if __name__ == "__main__": - unittest.main() - diff --git a/test/unit_tests/test_metadata_host_file.py b/test/unit_tests/test_metadata_host_file.py deleted file mode 100644 index d01fda97..00000000 --- a/test/unit_tests/test_metadata_host_file.py +++ /dev/null @@ -1,261 +0,0 @@ -#! /usr/bin/env python3 - -""" ------------------------------------------------------------------------ - Description: capgen needs to compare a metadata header against the - associated CCPP Fortran interface routine. This set of - tests is testing the parse_host_model_files function in - ccpp_capgen.py which performs the operations in the first - bullet below. Each test calls this function. - - * This script contains unit tests that do the following: - 1) Read one or more metadata files (to collect - the metadata headers) - 2) Read the associated CCPP Fortran host file(s) (to - collect Fortran interfaces) - 3) Create a CCPP host model object - 3) Test the properties of the CCPP host model object - - * Tests include: - - Correctly parse and match a simple module file with - data fields (a data block) - - Correctly parse and match a simple module file with - a DDT definition - - Correctly parse and match a simple module file with - two DDT definitions - - Correctly parse and match a simple module file with - two DDT definitions and a data block - - Assumptions: - - Command line arguments: none - - Usage: python3 test_metadata_host_file.py # run the unit tests ------------------------------------------------------------------------ -""" -import sys -import os -import logging -import unittest - -_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, - os.pardir, "scripts")) -if not os.path.exists(_SCRIPTS_DIR): - raise ImportError("Cannot find scripts directory") - -sys.path.append(_SCRIPTS_DIR) - -# pylint: disable=wrong-import-position -from ccpp_capgen import parse_host_model_files -from framework_env import CCPPFrameworkEnv -from parse_tools import CCPPError -# pylint: enable=wrong-import-position - -class MetadataHeaderTestCase(unittest.TestCase): - """Unit tests for parse_host_model_files""" - - def setUp(self): - """Setup important directories and logging""" - self._sample_files_dir = os.path.join(_TEST_DIR, "sample_host_files") - logger = logging.getLogger(self.__class__.__name__) - self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', - 'scheme_files':'', - 'suites':''}) - - def test_module_with_data(self): - """Test that a module containing a data block is parsed and matched - correctly.""" - # Setup - module_files = [os.path.join(self._sample_files_dir, "data1_mod.meta")] - # Exercise - hname = 'host_name_data1' - host_model = parse_host_model_files(module_files, hname, self._run_env) - # Verify the name of the host model - self.assertEqual(host_model.name, hname) - module_headers = host_model.metadata_tables() - self.assertEqual(len(module_headers), 1) - # Verify header titles - self.assertTrue('data1_mod' in module_headers) - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 3) - std_names = [x.get_prop_value('standard_name') for x in vlist] - self.assertTrue('play_station' in std_names) - self.assertTrue('xbox' in std_names) - self.assertTrue('nintendo_switch' in std_names) - - def test_module_with_one_ddt(self): - """Test that a module containing a DDT definition is parsed and matched - correctly.""" - # Setup - ddt_name = 'ddt1_t' - module_files = [os.path.join(self._sample_files_dir, "ddt1.meta")] - # Exercise - hname = 'host_name_ddt1' - host_model = parse_host_model_files(module_files, hname, self._run_env) - # Verify the name of the host model - self.assertEqual(host_model.name, hname) - module_headers = host_model.metadata_tables() - self.assertEqual(len(module_headers), 1) - # Verify header titles - self.assertTrue(ddt_name in module_headers) - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 0) - # Verify that the DDT was found and parsed - ddt_lib = host_model.ddt_lib - self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") - # Check DDT variables - ddt_mod = ddt_lib[ddt_name] - self.assertEqual(ddt_mod.name, ddt_name) - vlist = ddt_mod.variable_list() - self.assertEqual(len(vlist), 2) - std_names = [x.get_prop_value('standard_name') for x in vlist] - self.assertTrue('ddt_var_array_dimension' in std_names) - self.assertTrue('vars_array' in std_names) - - def test_module_with_two_ddts(self): - """Test that a module containing two DDT definitions is parsed and - matched correctly.""" - # Setup - ddt_names = ['ddt1_t', 'ddt2_t'] - ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] - - module_files = [os.path.join(self._sample_files_dir, "ddt2.meta")] - # Exercise - hname = 'host_name_ddt2' - host_model = parse_host_model_files(module_files, hname, self._run_env) - # Verify the name of the host model - self.assertEqual(host_model.name, hname) - module_headers = host_model.metadata_tables() - self.assertEqual(len(module_headers), len(ddt_names)) - # Verify header titles - for ddt_name in ddt_names: - self.assertTrue(ddt_name in module_headers) - # end for - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 0) - # Verify that each DDT was found and parsed - ddt_lib = host_model.ddt_lib - self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") - # Check DDT variables - for index, ddt_name in enumerate(ddt_names): - ddt_mod = ddt_lib[ddt_name] - self.assertEqual(ddt_mod.name, ddt_name) - vlist = ddt_mod.variable_list() - self.assertEqual(len(vlist), len(ddt_vars[index])) - std_names = [x.get_prop_value('standard_name') for x in vlist] - for sname in ddt_vars[index]: - self.assertTrue(sname in std_names) - # end for - # end for - - def test_module_with_two_ddts_and_data(self): - """Test that a module containing two DDT definitions and a block of - module data is parsed and matched correctly.""" - # Setup - ddt_names = ['ddt1_t', 'ddt2_t'] - ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] - - module_files = [os.path.join(self._sample_files_dir, - "ddt_data1_mod.meta")] - # Exercise - hname = 'host_name_ddt_data' - host_model = parse_host_model_files(module_files, hname, self._run_env) - # Verify the name of the host model - self.assertEqual(host_model.name, hname) - module_headers = host_model.metadata_tables() - self.assertEqual(len(module_headers), len(ddt_names) + 1) - # Verify header titles - for ddt_name in ddt_names: - self.assertTrue(ddt_name in module_headers) - # end for - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 3) - # Verify that each DDT was found and parsed - ddt_lib = host_model.ddt_lib - self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") - # Check DDT variables - for index, ddt_name in enumerate(ddt_names): - ddt_mod = ddt_lib[ddt_name] - self.assertEqual(ddt_mod.name, ddt_name) - vlist = ddt_mod.variable_list() - self.assertEqual(len(vlist), len(ddt_vars[index])) - std_names = [x.get_prop_value('standard_name') for x in vlist] - for sname in ddt_vars[index]: - self.assertTrue(sname in std_names) - # end for - # end for - # Verify header titles - self.assertTrue('ddt_data1_mod' in module_headers) - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 3) - std_names = [x.get_prop_value('standard_name') for x in vlist] - self.assertTrue('play_station' in std_names) - self.assertTrue('xbox' in std_names) - self.assertTrue('nintendo_switch' in std_names) - - def test_module_with_one_ddt_plus_undoc(self): - """Test that a module containing a one documented DDT definition - (i.e., with metadata) and one DDT without (i.e., no metadata) - is parsed and matched correctly.""" - # Setup - ddt_name = 'ddt2_t' - module_files = [os.path.join(self._sample_files_dir, "ddt1_plus.meta")] - # Exercise - hname = 'host_name_ddt1_plus' - host_model = parse_host_model_files(module_files, hname, self._run_env) - # Verify the name of the host model - self.assertEqual(host_model.name, hname) - module_headers = host_model.metadata_tables() - self.assertEqual(len(module_headers), 1) - # Verify header titles - self.assertTrue(ddt_name in module_headers) - # Verify host model variable list - vlist = host_model.variable_list() - self.assertEqual(len(vlist), 0) - # Verify that the DDT was found and parsed - ddt_lib = host_model.ddt_lib - self.assertEqual(ddt_lib.name, f"{hname}_ddts_ddt_lib") - # Check DDT variables - ddt_mod = ddt_lib[ddt_name] - self.assertEqual(ddt_mod.name, ddt_name) - vlist = ddt_mod.variable_list() - self.assertEqual(len(vlist), 2) - std_names = [x.get_prop_value('standard_name') for x in vlist] - self.assertTrue('ddt_var_array_dimension' in std_names) - self.assertTrue('vars_array' in std_names) - - def test_module_with_two_ddts_and_extra_var(self): - """Test that a module containing two DDT definitions is parsed and - a useful error message is produced if the DDT metadata has an - extra variable.""" - # Setup - ddt_names = ['ddt1_t', 'ddt2_t'] - ddt_vars = [(), ('ddt_var_array_dimension', 'vars_array')] - - module_files = [os.path.join(self._sample_files_dir, - "ddt2_extra_var.meta")] - # Exercise - hname = 'host_name_ddt_extra_var' - with self.assertRaises(CCPPError) as context: - host_model = parse_host_model_files(module_files, hname, - self._run_env) - # end with - # Check error messages - except_str = str(context.exception) - emsgs = ["Variable mismatch in ddt2_t, variables missing from Fortran ddt.", - - "No Fortran variable for bogus in ddt2_t", - "2 errors found comparing"] - for emsg in emsgs: - self.assertTrue(emsg in except_str) - # end for - -if __name__ == "__main__": - unittest.main() - diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 6651c2de..28b2ee50 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -59,7 +59,6 @@ # pylint: disable=wrong-import-position from ccpp_capgen import parse_scheme_files from framework_env import CCPPFrameworkEnv -from parse_tools import CCPPError # pylint: enable=wrong-import-position class MetadataHeaderTestCase(unittest.TestCase): @@ -68,7 +67,6 @@ class MetadataHeaderTestCase(unittest.TestCase): def setUp(self): """Setup important directories and logging""" self._sample_files_dir = os.path.join(_TEST_DIR, "sample_scheme_files") - self._host_files_dir = os.path.join(_TEST_DIR, "sample_host_files") logger = logging.getLogger(self.__class__.__name__) self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', 'scheme_files':'', @@ -87,263 +85,203 @@ def setUp(self): 'CCPP=2'}) def test_good_scheme_file(self): - """Test that good metadata file matches the Fortran, - with routines in the same order """ - # Setup + """Test that good metadata file matches the Fortran, with routines in the same order """ + #Setup scheme_files = [os.path.join(self._sample_files_dir, "temp_adjust.meta")] - # Exercise + #Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - # Verify size of returned list equals number of scheme headers - # in the test file and that header (subroutine) names are - # 'temp_adjust_[init,run,finalize]' + #Verify size of returned list equals number of scheme headers in the test file + # and that header (subroutine) names are 'temp_adjust_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - # Verify header titles + #Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('temp_adjust_init' in titles) self.assertTrue('temp_adjust_run' in titles) self.assertTrue('temp_adjust_finalize' in titles) - # Verify size and name of table_dict matches scheme name + #Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('temp_adjust' in table_dict) def test_reordered_scheme_file(self): - """Test that metadata file matches the Fortran when the - routines are not in the same order """ - # Setup + """Test that metadata file matches the Fortran when the routines are not in the same order """ + #Setup scheme_files = [os.path.join(self._sample_files_dir, "reorder.meta")] - # Exercise + #Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - # Verify size of returned list equals number of scheme headers - # in the test file and that header (subroutine) names are - # 'reorder_[init,run,finalize]' + #Verify size of returned list equals number of scheme headers in the test file + # and that header (subroutine) names are 'reorder_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - # Verify header titles + #Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('reorder_init' in titles) self.assertTrue('reorder_run' in titles) self.assertTrue('reorder_finalize' in titles) - # Verify size and name of table_dict matches scheme name + #Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('reorder' in table_dict) def test_missing_metadata_header(self): - """Test that a missing metadata header (aka arg table) is - corretly detected """ - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "missing_arg_table.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + """Test that a missing metadata header (aka arg table) is corretly detected """ + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "missing_arg_table.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify correct error message returned + #Verify correct error message returned emsg = "No matching metadata header found for missing_arg_table_run in" self.assertTrue(emsg in str(context.exception)) def test_missing_fortran_header(self): """Test that a missing fortran header is corretly detected """ - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "missing_fort_header.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "missing_fort_header.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify correct error message returned + #Verify correct error message returned emsg = "No matching Fortran routine found for missing_fort_header_run in" self.assertTrue(emsg in str(context.exception)) def test_mismatch_intent(self): - """Test that differing intent, kind, rank, and type between - metadata and fortran is corretly detected """ - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "mismatch_intent.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + """Test that differing intent, kind, rank, and type between metadata and fortran is corretly detected """ + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "mismatch_intent.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify 4 correct error messages returned - emsg = "intent mismatch (in != inout) in mismatch_intent_run, at" - self.assertTrue(emsg in str(context.exception)) - emsg = "kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at" - self.assertTrue(emsg in str(context.exception)) - emsg = "rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at" - self.assertTrue(emsg in str(context.exception)) - emsg = "type mismatch (integer != real) in mismatch_intent_run, at" - self.assertTrue(emsg in str(context.exception)) - self.assertTrue("4 errors found comparing" in str(context.exception)) + #Verify 4 correct error messages returned + self.assertTrue('intent mismatch (in != inout) in mismatch_intent_run, at' in str(context.exception)) + self.assertTrue('kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at' in str(context.exception)) + self.assertTrue('rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at' in str(context.exception)) + self.assertTrue('type mismatch (integer != real) in mismatch_intent_run, at' in str(context.exception)) + self.assertTrue('4 errors found comparing' in str(context.exception)) def test_invalid_subr_stmnt(self): - """Test that invalid Fortran subroutine statements are correctly - detected """ - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "invalid_subr_stmnt.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + """Test that invalid Fortran subroutine statements are correctly detected """ + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "invalid_subr_stmnt.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify correct error message returned - self.assertTrue("Invalid dummy argument, 'errmsg', at" - in str(context.exception)) + #Verify correct error message returned + self.assertTrue("Invalid dummy argument, 'errmsg', at" in str(context.exception)) def test_invalid_dummy_arg(self): - """Test that invalid dummy argument statements are correctly detected""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "invalid_dummy_arg.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + """Test that invalid dummy argument statements are correctly detected """ + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "invalid_dummy_arg.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify correct error message returned - emsg = "Invalid dummy argument, 'woohoo', at" - self.assertTrue(emsg in str(context.exception)) + #Verify correct error message returned + self.assertTrue("Invalid dummy argument, 'woohoo', at" in str(context.exception)) - def test_ccpp_notset_var_missing_in_meta(self): - """Test for correct detection of a variable that REMAINS in the - subroutine argument list - (due to an undefined pre-processor directive: #ifndef CCPP), - BUT IS NOT PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPnotset_var_missing_in_meta.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: +# pylint: disable=invalid-name + def test_CCPPnotset_var_missing_in_meta(self): + """Test for correct detection of a variable that REMAINS in the subroutine argument list + (due to an undefined pre-processor directive: #ifndef CCPP), BUT IS NOT PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPnotset_var_missing_in_meta.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env) - # Verify 3 correct error messages returned - emsg = "Variable mismatch in CCPPnotset_var_missing_in_meta_run, " + \ - "variables missing from metadata header." - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run" - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run" - self.assertTrue(emsg in str(context.exception)) - self.assertTrue("3 errors found comparing" in str(context.exception)) + #Verify 3 correct error messages returned + self.assertTrue('Variable mismatch in CCPPnotset_var_missing_in_meta_run, variables missing from metadata header.' + in str(context.exception)) + self.assertTrue('Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) + self.assertTrue('Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) + self.assertTrue('3 errors found comparing' in str(context.exception)) - def test_ccpp_eq1_var_missing_in_fort(self): - """Test for correct detection of a variable that IS REMOVED the - subroutine argument list - (due to a pre-processor directive: #ifndef CCPP), - but IS PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPeq1_var_missing_in_fort.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: + def test_CCPPeq1_var_missing_in_fort(self): + """Test for correct detection of a variable that IS REMOVED the subroutine argument list + (due to a pre-processor directive: #ifndef CCPP), but IS PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_fort.meta")] + #Exercise + with self.assertRaises(Exception) as context: parse_scheme_files(scheme_files, self._run_env_ccpp) - # Verify 3 correct error messages returned - emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ - "variables missing from Fortran scheme." - self.assertTrue(emsg in str(context.exception)) - emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ - "no Fortran variable bar." - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run" - self.assertTrue(emsg in str(context.exception)) - self.assertTrue("3 errors found comparing" in str(context.exception)) + #Verify 3 correct error messages returned + self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, variables missing from Fortran scheme.' + in str(context.exception)) + self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, no Fortran variable bar.' + in str(context.exception)) + self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run' in str(context.exception)) + self.assertTrue('3 errors found comparing' in str(context.exception)) - def test_ccpp_eq1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the - subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), - and IS PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPeq1_var_in_fort_meta.meta")] - # Exercise + def test_CCPPeq1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), and IS PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_in_fort_meta.meta")] + #Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp) - # Verify size of returned list equals number of scheme headers in - # the test file (1) and that header (subroutine) name is - # 'CCPPeq1_var_in_fort_meta_run' + #Verify size of returned list equals number of scheme headers in the test file (1) + # and that header (subroutine) name is 'CCPPeq1_var_in_fort_meta_run' self.assertEqual(len(scheme_headers), 1) - # Verify header titles + #Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue("CCPPeq1_var_in_fort_meta_run" in titles) + self.assertTrue('CCPPeq1_var_in_fort_meta_run' in titles) - # Verify size and name of table_dict matches scheme name + #Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue("CCPPeq1_var_in_fort_meta" in table_dict) + self.assertTrue('CCPPeq1_var_in_fort_meta' in table_dict) - def test_ccpp_gt1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the - subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), - and IS PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPgt1_var_in_fort_meta.meta")] - # Exercise + def test_CCPPgt1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), and IS PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] + #Exercise # Set CCPP directive to > 1 scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp2) - # Verify size of returned list equals number of scheme headers - # in the test file (1) and that header (subroutine) name is - # 'CCPPgt1_var_in_fort_meta_init' + #Verify size of returned list equals number of scheme headers in the test file (1) + # and that header (subroutine) name is 'CCPPgt1_var_in_fort_meta_init' self.assertEqual(len(scheme_headers), 1) - # Verify header titles + #Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue("CCPPgt1_var_in_fort_meta_init" in titles) + self.assertTrue('CCPPgt1_var_in_fort_meta_init' in titles) - # Verify size and name of table_dict matches scheme name + #Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue("CCPPgt1_var_in_fort_meta" in table_dict) + self.assertTrue('CCPPgt1_var_in_fort_meta' in table_dict) - def test_ccpp_gt1_var_in_fort_meta2(self): - """Test correct detection of a variable that - IS NOT PRESENT the subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), - but IS PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPgt1_var_in_fort_meta.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: - _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) - # Verify 3 correct error messages returned - emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ - "variables missing from Fortran scheme." - self.assertTrue(emsg in str(context.exception)) - emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ - "no Fortran variable bar." - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init" - self.assertTrue(emsg in str(context.exception)) - self.assertTrue("3 errors found comparing" in str(context.exception)) + def test_CCPPgt1_var_in_fort_meta2(self): + """Test correct detection of a variable that IS NOT PRESENT the subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), but IS PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] + #Exercise + with self.assertRaises(Exception) as context: + parse_scheme_files(scheme_files, self._run_env_ccpp) + #Verify 3 correct error messages returned + self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, variables missing from Fortran scheme.' + in str(context.exception)) + self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, no Fortran variable bar.' + in str(context.exception)) + self.assertTrue('Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init' in str(context.exception)) + self.assertTrue('3 errors found comparing' in str(context.exception)) - def test_ccpp_eq1_var_missing_in_meta(self): - """Test correct detection of a variable that - IS PRESENT the subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), - and IS NOT PRESENT in meta file""" - # Setup - scheme_files = [os.path.join(self._sample_files_dir, - "CCPPeq1_var_missing_in_meta.meta")] - # Exercise - with self.assertRaises(CCPPError) as context: - _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) - # Verify 3 correct error messages returned - emsg = "Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, "+ \ - "variables missing from metadata header." - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize" - self.assertTrue(emsg in str(context.exception)) - emsg = "Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize" - self.assertTrue(emsg in str(context.exception)) - self.assertTrue("3 errors found comparing" in str(context.exception)) + def test_CCPPeq1_var_missing_in_meta(self): + """Test correct detection of a variable that IS PRESENT the subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), and IS NOT PRESENT in meta file""" + #Setup + scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_meta.meta")] + #Exercise + with self.assertRaises(Exception) as context: + parse_scheme_files(scheme_files, self._run_env_ccpp) + #Verify 3 correct error messages returned + self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, variables missing from metadata header.' + in str(context.exception)) + self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) + self.assertTrue('Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) + self.assertTrue('3 errors found comparing' in str(context.exception)) - def test_scheme_ddt_only(self): - """Test correct detection of a "scheme" file which contains only - DDT definitions""" - # Setup - scheme_files = [os.path.join(self._host_files_dir, "ddt2.meta")] - # Exercise - scheme_headers, table_dict = parse_scheme_files(scheme_files, - self._run_env_ccpp) -# with self.assertRaises(CCPPError) as context: -# _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) -# # Verify correct error messages returned +# pylint: enable=invalid-name if __name__ == "__main__": unittest.main() - From a1aaf9ec64cb789194dd83f10b4651e528d11337 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:43:07 -0600 Subject: [PATCH 19/49] remove testing files --- .../fortran_files/comments_test.F90 | 33 ---------------- .../fortran_files/linebreak_test.F90 | 39 ------------------- .../sample_host_files/data1_mod.F90 | 11 ------ .../sample_host_files/data1_mod.meta | 25 ------------ 4 files changed, 108 deletions(-) delete mode 100644 test/unit_tests/sample_files/fortran_files/comments_test.F90 delete mode 100644 test/unit_tests/sample_files/fortran_files/linebreak_test.F90 delete mode 100644 test/unit_tests/sample_host_files/data1_mod.F90 delete mode 100644 test/unit_tests/sample_host_files/data1_mod.meta diff --git a/test/unit_tests/sample_files/fortran_files/comments_test.F90 b/test/unit_tests/sample_files/fortran_files/comments_test.F90 deleted file mode 100644 index d4820a36..00000000 --- a/test/unit_tests/sample_files/fortran_files/comments_test.F90 +++ /dev/null @@ -1,33 +0,0 @@ -! -! This work (Common Community Physics Package Framework), identified by -! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is -! placed in the public domain. -! -! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -!> -!! @brief Auto-generated Test of comment writing for FortranWriter -!! -! -module comments_test - -! We can write comments in the module header - ! We can write indented comments in the header - integer :: foo ! Comment at end of line works - integer :: bar ! - ! xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - ! - integer :: baz ! - ! yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy - ! yyyyy - -CONTAINS - ! We can write comments in the module body - -end module comments_test diff --git a/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 b/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 deleted file mode 100644 index 4f89441f..00000000 --- a/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 +++ /dev/null @@ -1,39 +0,0 @@ -! -! This work (Common Community Physics Package Framework), identified by -! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is -! placed in the public domain. -! -! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -!> -!! @brief Auto-generated Test of line breaking for FortranWriter -!! -! -module linebreak_test - - character(len=7) :: data = (/ name000, name001, name002, name003, name004, name005, name006, & - name007, name008, name009, name010, name011, name012, name013, name014, name015, & - name016, name017, name018, name019, name020, name021, name022, name023, name024, & - name025, name026, name027, name028, name029, name030, name031, name032, name033, & - name034, name035, name036, name037, name038, name039, name040, name041, name042, & - name043, name044, name045, name046, name047, name048, name049, name050, name051, & - name052, name053, name054, name055, name056, name057, name058, name059, name060, & - name061, name062, name063, name064, name065, name066, name067, name068, name069, & - name070, name071, name072, name073, name074, name075, name076, name077, name078, & - name079, name080, name081, name082, name083, name084, name085, name086, name087, & - name088, name089, name090, name091, name092, name093, name094, name095, name096, & - name097, name098, name099 /) - -CONTAINS - call & - endrun('Cannot read columns_on_task from file'// & - ', columns_on_task has no horizontal dimension; columns_on_task is a protected variable') - - -end module linebreak_test diff --git a/test/unit_tests/sample_host_files/data1_mod.F90 b/test/unit_tests/sample_host_files/data1_mod.F90 deleted file mode 100644 index b85db315..00000000 --- a/test/unit_tests/sample_host_files/data1_mod.F90 +++ /dev/null @@ -1,11 +0,0 @@ -module data1_mod - - use ccpp_kinds, only: kind_phys - - !> \section arg_table_data1_mod Argument Table - !! \htmlinclude arg_table_data1_mod.html - real(kind_phys) :: ps1 - real(kind_phys), allocatable :: xbox(:,:) - real(kind_phys), allocatable :: switch(:,:) - -end module data1_mod diff --git a/test/unit_tests/sample_host_files/data1_mod.meta b/test/unit_tests/sample_host_files/data1_mod.meta deleted file mode 100644 index 37e2de96..00000000 --- a/test/unit_tests/sample_host_files/data1_mod.meta +++ /dev/null @@ -1,25 +0,0 @@ -[ccpp-table-properties] - name = data1_mod - type = module -[ccpp-arg-table] - name = data1_mod - type = module -[ ps1 ] - standard_name = play_station - state_variable = true - type = real | kind = kind_phys - units = Pa - dimensions = () -[ xbox ] - standard_name = xbox - state_variable = true - type = real | kind = kind_phys - units = m s-1 - dimensions = (horizontal_dimension, vertical_layer_dimension) -[ switch ] - standard_name = nintendo_switch - long_name = Incompatible junk - state_variable = true - type = real | kind = kind_phys - units = m s-1 - dimensions = (horizontal_dimension, vertical_layer_dimension) From 071687e57fdda69bb918b51dd1d2db0f2e919fa5 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 10:51:36 -0600 Subject: [PATCH 20/49] add testing files --- .../fortran_files/comments_test.F90 | 33 ++ .../fortran_files/linebreak_test.F90 | 39 +++ test/unit_tests/test_fortran_write.py | 127 +++++++ test/unit_tests/test_metadata_scheme_file.py | 326 +++++++++++------- 4 files changed, 393 insertions(+), 132 deletions(-) create mode 100644 test/unit_tests/sample_files/fortran_files/comments_test.F90 create mode 100644 test/unit_tests/sample_files/fortran_files/linebreak_test.F90 create mode 100644 test/unit_tests/test_fortran_write.py diff --git a/test/unit_tests/sample_files/fortran_files/comments_test.F90 b/test/unit_tests/sample_files/fortran_files/comments_test.F90 new file mode 100644 index 00000000..d4820a36 --- /dev/null +++ b/test/unit_tests/sample_files/fortran_files/comments_test.F90 @@ -0,0 +1,33 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated Test of comment writing for FortranWriter +!! +! +module comments_test + +! We can write comments in the module header + ! We can write indented comments in the header + integer :: foo ! Comment at end of line works + integer :: bar ! + ! xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ! + integer :: baz ! + ! yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + ! yyyyy + +CONTAINS + ! We can write comments in the module body + +end module comments_test diff --git a/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 b/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 new file mode 100644 index 00000000..4f89441f --- /dev/null +++ b/test/unit_tests/sample_files/fortran_files/linebreak_test.F90 @@ -0,0 +1,39 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated Test of line breaking for FortranWriter +!! +! +module linebreak_test + + character(len=7) :: data = (/ name000, name001, name002, name003, name004, name005, name006, & + name007, name008, name009, name010, name011, name012, name013, name014, name015, & + name016, name017, name018, name019, name020, name021, name022, name023, name024, & + name025, name026, name027, name028, name029, name030, name031, name032, name033, & + name034, name035, name036, name037, name038, name039, name040, name041, name042, & + name043, name044, name045, name046, name047, name048, name049, name050, name051, & + name052, name053, name054, name055, name056, name057, name058, name059, name060, & + name061, name062, name063, name064, name065, name066, name067, name068, name069, & + name070, name071, name072, name073, name074, name075, name076, name077, name078, & + name079, name080, name081, name082, name083, name084, name085, name086, name087, & + name088, name089, name090, name091, name092, name093, name094, name095, name096, & + name097, name098, name099 /) + +CONTAINS + call & + endrun('Cannot read columns_on_task from file'// & + ', columns_on_task has no horizontal dimension; columns_on_task is a protected variable') + + +end module linebreak_test diff --git a/test/unit_tests/test_fortran_write.py b/test/unit_tests/test_fortran_write.py new file mode 100644 index 00000000..87e64baa --- /dev/null +++ b/test/unit_tests/test_fortran_write.py @@ -0,0 +1,127 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Contains unit tests for FortranWriter + in scripts file fortran/fortran_write.py + + Assumptions: + + Command line arguments: none + + Usage: python3 test_fortran_write.py # run the unit tests +----------------------------------------------------------------------- +""" + +import filecmp +import glob +import os +import sys +import unittest + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +_SAMPLE_FILES_DIR = os.path.join(_TEST_DIR, "sample_files", "fortran_files") +_PRE_TMP_DIR = os.path.join(_TEST_DIR, "tmp") +_TMP_DIR = os.path.join(_PRE_TMP_DIR, "fortran_files") + +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError(f"Cannot find scripts directory, {_SCRIPTS_DIR}") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from fortran_tools import FortranWriter +# pylint: enable=wrong-import-position + +############################################################################### +def remove_files(file_list): +############################################################################### + """Remove files in if they exist""" + if isinstance(file_list, str): + file_list = [file_list] + # end if + for fpath in file_list: + if os.path.exists(fpath): + os.remove(fpath) + # End if + # End for + +class MetadataTableTestCase(unittest.TestCase): + + """Tests for `FortranWriter`.""" + + @classmethod + def setUpClass(cls): + """Clean output directory (tmp) before running tests""" + #Does "tmp" directory exist? If not then create it: + if not os.path.exists(_PRE_TMP_DIR): + os.mkdir(_PRE_TMP_DIR) + # Ensure the "sample_files/fortran_files" directory exists and is empty + if os.path.exists(_TMP_DIR): + # Clear out all files: + remove_files(glob.iglob(os.path.join(_TMP_DIR, '*.*'))) + else: + os.makedirs(_TMP_DIR) + # end if + + #Run inherited setup method: + super().setUpClass() + + def test_line_breaking(self): + """Test that FortranWriter correctly breaks long lines""" + # Setup + testname = "linebreak_test" + compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") + generate = os.path.join(_TMP_DIR, f"{testname}.F90") + # Exercise + header = "Test of line breaking for FortranWriter" + with FortranWriter(generate, 'w', header, f"{testname}") as gen: + # Test long declaration + data_items = ', '.join([f"name{x:03}" for x in range(100)]) + gen.write(f"character(len=7) :: data = (/ {data_items} /)", 1) + gen.end_module_header() + # Test long code lines + line_items = ["call endrun('Cannot read columns_on_task from ", + "file'//', columns_on_task has no horizontal ", + "dimension; columns_on_task is a ", + "protected variable')"] + gen.write(f"{''.join(line_items)}", 2) + # end with + + # Check that file was generated + amsg = f"{generate} does not exist" + self.assertTrue(os.path.exists(generate), msg=amsg) + amsg = f"{generate} does not match {compare}" + self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) + + def test_good_comments(self): + """Test that comments are written and broken correctly.""" + # Setup + testname = "comments_test" + compare = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") + generate = os.path.join(_TMP_DIR, f"{testname}.F90") + # Exercise + header = "Test of comment writing for FortranWriter" + with FortranWriter(generate, 'w', header, f"{testname}") as gen: + gen.comment("We can write comments in the module header", 0) + gen.comment("We can write indented comments in the header", 1) + gen.write("integer :: foo ! Comment at end of line works", 1) + # Test long comments at end of line + gen.write(f"integer :: bar ! {'x'*100}", 1) + gen.write(f"integer :: baz ! {'y'*130}", 1) + gen.end_module_header() + # Test comment line in body + gen.comment("We can write comments in the module body", 1) + + # end with + + # Check that file was generated + amsg = f"{generate} does not exist" + self.assertTrue(os.path.exists(generate), msg=amsg) + amsg = f"{generate} does not match {compare}" + self.assertTrue(filecmp.cmp(generate, compare, shallow=False), msg=amsg) + +if __name__ == "__main__": + unittest.main() + diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 52f62aa1..03f446e3 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -59,6 +59,7 @@ # pylint: disable=wrong-import-position from ccpp_capgen import parse_scheme_files from framework_env import CCPPFrameworkEnv +from parse_tools import CCPPError # pylint: enable=wrong-import-position class MetadataHeaderTestCase(unittest.TestCase): @@ -67,6 +68,7 @@ class MetadataHeaderTestCase(unittest.TestCase): def setUp(self): """Setup important directories and logging""" self._sample_files_dir = os.path.join(_TEST_DIR, "sample_scheme_files") + self._host_files_dir = os.path.join(_TEST_DIR, "sample_host_files") logger = logging.getLogger(self.__class__.__name__) self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', 'scheme_files':'', @@ -85,203 +87,263 @@ def setUp(self): 'CCPP=2'}) def test_good_scheme_file(self): - """Test that good metadata file matches the Fortran, with routines in the same order """ - #Setup + """Test that good metadata file matches the Fortran, + with routines in the same order """ + # Setup scheme_files = [os.path.join(self._sample_files_dir, "temp_adjust.meta")] - #Exercise + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - #Verify size of returned list equals number of scheme headers in the test file - # and that header (subroutine) names are 'temp_adjust_[init,run,finalize]' + # Verify size of returned list equals number of scheme headers + # in the test file and that header (subroutine) names are + # 'temp_adjust_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('temp_adjust_init' in titles) self.assertTrue('temp_adjust_run' in titles) self.assertTrue('temp_adjust_finalize' in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('temp_adjust' in table_dict) def test_reordered_scheme_file(self): - """Test that metadata file matches the Fortran when the routines are not in the same order """ - #Setup + """Test that metadata file matches the Fortran when the + routines are not in the same order """ + # Setup scheme_files = [os.path.join(self._sample_files_dir, "reorder.meta")] - #Exercise + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env) - #Verify size of returned list equals number of scheme headers in the test file - # and that header (subroutine) names are 'reorder_[init,run,finalize]' + # Verify size of returned list equals number of scheme headers + # in the test file and that header (subroutine) names are + # 'reorder_[init,run,finalize]' self.assertEqual(len(scheme_headers), 3) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] self.assertTrue('reorder_init' in titles) self.assertTrue('reorder_run' in titles) self.assertTrue('reorder_finalize' in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) self.assertTrue('reorder' in table_dict) def test_missing_metadata_header(self): - """Test that a missing metadata header (aka arg table) is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "missing_arg_table.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that a missing metadata header (aka arg table) is + corretly detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "missing_arg_table.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned + # Verify correct error message returned emsg = "No matching metadata header found for missing_arg_table_run in" self.assertTrue(emsg in str(context.exception)) def test_missing_fortran_header(self): """Test that a missing fortran header is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "missing_fort_header.meta")] - #Exercise - with self.assertRaises(Exception) as context: + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "missing_fort_header.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned + # Verify correct error message returned emsg = "No matching Fortran routine found for missing_fort_header_run in" self.assertTrue(emsg in str(context.exception)) def test_mismatch_intent(self): - """Test that differing intent, kind, rank, and type between metadata and fortran is corretly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "mismatch_intent.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that differing intent, kind, rank, and type between + metadata and fortran is corretly detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "mismatch_intent.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify 4 correct error messages returned - self.assertTrue('intent mismatch (in != inout) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at' in str(context.exception)) - self.assertTrue('type mismatch (integer != real) in mismatch_intent_run, at' in str(context.exception)) - self.assertTrue('4 errors found comparing' in str(context.exception)) + # Verify 4 correct error messages returned + emsg = "intent mismatch (in != inout) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + emsg = "kind mismatch (kind_fizz != kind_phys) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + emsg = "rank mismatch in mismatch_intent_run/potential_temperature (0 != 1), at" + self.assertTrue(emsg in str(context.exception)) + emsg = "type mismatch (integer != real) in mismatch_intent_run, at" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("4 errors found comparing" in str(context.exception)) def test_invalid_subr_stmnt(self): - """Test that invalid Fortran subroutine statements are correctly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "invalid_subr_stmnt.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that invalid Fortran subroutine statements are correctly + detected """ + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "invalid_subr_stmnt.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned - self.assertTrue("Invalid dummy argument, 'errmsg', at" in str(context.exception)) + # Verify correct error message returned + self.assertTrue("Invalid dummy argument, 'errmsg', at" + in str(context.exception)) def test_invalid_dummy_arg(self): - """Test that invalid dummy argument statements are correctly detected """ - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "invalid_dummy_arg.meta")] - #Exercise - with self.assertRaises(Exception) as context: + """Test that invalid dummy argument statements are correctly detected""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "invalid_dummy_arg.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify correct error message returned - self.assertTrue("Invalid dummy argument, 'woohoo', at" in str(context.exception)) + # Verify correct error message returned + emsg = "Invalid dummy argument, 'woohoo', at" + self.assertTrue(emsg in str(context.exception)) -# pylint: disable=invalid-name - def test_CCPPnotset_var_missing_in_meta(self): - """Test for correct detection of a variable that REMAINS in the subroutine argument list - (due to an undefined pre-processor directive: #ifndef CCPP), BUT IS NOT PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPnotset_var_missing_in_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: + def test_ccpp_notset_var_missing_in_meta(self): + """Test for correct detection of a variable that REMAINS in the + subroutine argument list + (due to an undefined pre-processor directive: #ifndef CCPP), + BUT IS NOT PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPnotset_var_missing_in_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPnotset_var_missing_in_meta_run, variables missing from metadata header.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) - self.assertTrue('Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPnotset_var_missing_in_meta_run, " + \ + "variables missing from metadata header." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPnotset_var_missing_in_meta_run" + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errflg in CCPPnotset_var_missing_in_meta_run" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_missing_in_fort(self): - """Test for correct detection of a variable that IS REMOVED the subroutine argument list - (due to a pre-processor directive: #ifndef CCPP), but IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_fort.meta")] - #Exercise - with self.assertRaises(Exception) as context: + def test_ccpp_eq1_var_missing_in_fort(self): + """Test for correct detection of a variable that IS REMOVED the + subroutine argument list + (due to a pre-processor directive: #ifndef CCPP), + but IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_missing_in_fort.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, variables missing from Fortran scheme.' - in str(context.exception)) - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_fort_run, no Fortran variable bar.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ + "variables missing from Fortran scheme." + self.assertTrue(emsg in str(context.exception)) + emsg = "Variable mismatch in CCPPeq1_var_missing_in_fort_run, " + \ + "no Fortran variable bar." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_fort_run" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), and IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_in_fort_meta.meta")] - #Exercise + def test_ccpp_eq1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the + subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), + and IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_in_fort_meta.meta")] + # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify size of returned list equals number of scheme headers in the test file (1) - # and that header (subroutine) name is 'CCPPeq1_var_in_fort_meta_run' + # Verify size of returned list equals number of scheme headers in + # the test file (1) and that header (subroutine) name is + # 'CCPPeq1_var_in_fort_meta_run' self.assertEqual(len(scheme_headers), 1) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue('CCPPeq1_var_in_fort_meta_run' in titles) + self.assertTrue("CCPPeq1_var_in_fort_meta_run" in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue('CCPPeq1_var_in_fort_meta' in table_dict) + self.assertTrue("CCPPeq1_var_in_fort_meta" in table_dict) - def test_CCPPgt1_var_in_fort_meta(self): - """Test positive case of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), and IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] - #Exercise + def test_ccpp_gt1_var_in_fort_meta(self): + """Test positive case of a variable that IS PRESENT the + subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), + and IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPgt1_var_in_fort_meta.meta")] + # Exercise # Set CCPP directive to > 1 scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp2) - #Verify size of returned list equals number of scheme headers in the test file (1) - # and that header (subroutine) name is 'CCPPgt1_var_in_fort_meta_init' + # Verify size of returned list equals number of scheme headers + # in the test file (1) and that header (subroutine) name is + # 'CCPPgt1_var_in_fort_meta_init' self.assertEqual(len(scheme_headers), 1) - #Verify header titles + # Verify header titles titles = [elem.title for elem in scheme_headers] - self.assertTrue('CCPPgt1_var_in_fort_meta_init' in titles) + self.assertTrue("CCPPgt1_var_in_fort_meta_init" in titles) - #Verify size and name of table_dict matches scheme name + # Verify size and name of table_dict matches scheme name self.assertEqual(len(table_dict), 1) - self.assertTrue('CCPPgt1_var_in_fort_meta' in table_dict) + self.assertTrue("CCPPgt1_var_in_fort_meta" in table_dict) - def test_CCPPgt1_var_in_fort_meta2(self): - """Test correct detection of a variable that IS NOT PRESENT the subroutine argument list - (due to a pre-processor directive: #if CCPP > 1), but IS PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPgt1_var_in_fort_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: - parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, variables missing from Fortran scheme.' - in str(context.exception)) - self.assertTrue('Variable mismatch in CCPPgt1_var_in_fort_meta_init, no Fortran variable bar.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + def test_ccpp_gt1_var_in_fort_meta2(self): + """Test correct detection of a variable that + IS NOT PRESENT the subroutine argument list + (due to a pre-processor directive: #if CCPP > 1), + but IS PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPgt1_var_in_fort_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: + _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ + "variables missing from Fortran scheme." + self.assertTrue(emsg in str(context.exception)) + emsg = "Variable mismatch in CCPPgt1_var_in_fort_meta_init, " + \ + "no Fortran variable bar." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPgt1_var_in_fort_meta_init" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) - def test_CCPPeq1_var_missing_in_meta(self): - """Test correct detection of a variable that IS PRESENT the subroutine argument list - (due to a pre-processor directive: #ifdef CCPP), and IS NOT PRESENT in meta file""" - #Setup - scheme_files = [os.path.join(self._sample_files_dir, "CCPPeq1_var_missing_in_meta.meta")] - #Exercise - with self.assertRaises(Exception) as context: - parse_scheme_files(scheme_files, self._run_env_ccpp) - #Verify 3 correct error messages returned - self.assertTrue('Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, variables missing from metadata header.' - in str(context.exception)) - self.assertTrue('Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) - self.assertTrue('Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize' in str(context.exception)) - self.assertTrue('3 errors found comparing' in str(context.exception)) + def test_ccpp_eq1_var_missing_in_meta(self): + """Test correct detection of a variable that + IS PRESENT the subroutine argument list + (due to a pre-processor directive: #ifdef CCPP), + and IS NOT PRESENT in meta file""" + # Setup + scheme_files = [os.path.join(self._sample_files_dir, + "CCPPeq1_var_missing_in_meta.meta")] + # Exercise + with self.assertRaises(CCPPError) as context: + _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) + # Verify 3 correct error messages returned + emsg = "Variable mismatch in CCPPeq1_var_missing_in_meta_finalize, "+ \ + "variables missing from metadata header." + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errmsg in CCPPeq1_var_missing_in_meta_finalize" + self.assertTrue(emsg in str(context.exception)) + emsg = "Out of order argument, errflg in CCPPeq1_var_missing_in_meta_finalize" + self.assertTrue(emsg in str(context.exception)) + self.assertTrue("3 errors found comparing" in str(context.exception)) -# pylint: enable=invalid-name + def test_scheme_ddt_only(self): + """Test correct detection of a "scheme" file which contains only + DDT definitions""" + # Setup + scheme_files = [os.path.join(self._host_files_dir, "ddt2.meta")] + # Exercise + scheme_headers, table_dict = parse_scheme_files(scheme_files, + self._run_env_ccpp) +# with self.assertRaises(CCPPError) as context: +# _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) +# # Verify correct error messages returned if __name__ == '__main__': unittest.main() + From 424701962fd0bc42a6ade3b1489c1ad7d3a3f22d Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 11:09:02 -0600 Subject: [PATCH 21/49] update regexes in state_machine --- scripts/ccpp_state_machine.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/ccpp_state_machine.py b/scripts/ccpp_state_machine.py index 30540fc9..0de2c7bd 100644 --- a/scripts/ccpp_state_machine.py +++ b/scripts/ccpp_state_machine.py @@ -3,11 +3,11 @@ # CCPP framework imports from state_machine import StateMachine -_INIT_ST = r"(?:(?i)init(?:ial(?:ize)?)?)" -_FINAL_ST = r"(?:(?i)final(?:ize)?)" -_RUN_ST = r"(?:(?i)run)" -_TS_INIT_ST = r"(?:(?i)timestep_init(?:ial(?:ize)?)?)" -_TS_FINAL_ST = r"(?:(?i)timestep_final(?:ize)?)" +_INIT_ST = r"(?:init(?:ial(?:ize)?)?)" +_FINAL_ST = r"(?:final(?:ize)?)" +_RUN_ST = r"(?:run)" +_TS_INIT_ST = r"(?:timestep_init(?:ial(?:ize)?)?)" +_TS_FINAL_ST = r"(?:timestep_final(?:ize)?)" # Allowed CCPP transitions # pylint: disable=bad-whitespace From 104e18f93495924ed04bb78546fb563452ada3c6 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 11:13:43 -0600 Subject: [PATCH 22/49] fix doctest --- scripts/parse_tools/parse_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/parse_tools/parse_source.py b/scripts/parse_tools/parse_source.py index 30f05f7f..44a2014f 100644 --- a/scripts/parse_tools/parse_source.py +++ b/scripts/parse_tools/parse_source.py @@ -204,7 +204,7 @@ class ParseContext(object): >>> ParseContext("source.F90", 32) Traceback (most recent call last): - CCPPError: ParseContext linenum must be an int + parse_tools.parse_source.CCPPError: ParseContext linenum must be an int >>> ParseContext(32, 90) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): CCPPError: ParseContext filenum must be a string From bbdf0d05ba419a7618700e5236fdf5a57fe1ce58 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 1 Sep 2023 11:56:39 -0600 Subject: [PATCH 23/49] add state_machine updates --- scripts/state_machine.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/state_machine.py b/scripts/state_machine.py index 966ad04f..692da7a9 100644 --- a/scripts/state_machine.py +++ b/scripts/state_machine.py @@ -30,7 +30,7 @@ class StateMachine: >>> StateMachine([('ab','a','b','a')]).final_state('ab') 'b' >>> StateMachine([('ab','a','b','a')]).transition_regex('ab') - re.compile('a$') + re.compile('a$', re.IGNORECASE) >>> StateMachine([('ab','a','b','a')]).function_match('foo_a', transition='ab') ('foo', 'a', 'ab') >>> StateMachine([('ab','a','b',r'ax?')]).function_match('foo_a', transition='ab') @@ -162,8 +162,9 @@ def __setitem__(self, key, value): if len(value) != 3: raise ValueError("Invalid transition ({}), should be of the form (inital_state, final_state, regex).".format(value)) # end if - regex = re.compile(value[2] + r"$") - function = re.compile(FORTRAN_ID + r"_(" + value[2] + r")$") + regex = re.compile(value[2] + r"$", re.IGNORECASE) + function = re.compile(FORTRAN_ID + r"_(" + value[2] + r")$", + re.IGNORECASE) self.__stt__[key] = (value[0], value[1], regex, function) def __delitem__(self, key): From e75e14a842d69cc5df0f6fd57fe1f25dd2c03468 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 25 Sep 2023 13:56:33 -0600 Subject: [PATCH 24/49] simplify workflow --- .github/workflows/capgen_unit_tests.yaml | 7 +------ .github/workflows/python.yaml | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml index 4406ae77..c2d52ee8 100644 --- a/.github/workflows/capgen_unit_tests.yaml +++ b/.github/workflows/capgen_unit_tests.yaml @@ -3,15 +3,10 @@ name: Capgen Unit Tests on: workflow_dispatch: pull_request: - types: [opened, synchronize, reopened] - push: - branches: - #Trigger workflow on push to any branch or branch hierarchy: - - '**' + branches: [feature/capgen, main] jobs: unit_tests: - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 3ab5347c..2f04533e 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -3,15 +3,10 @@ name: Python package on: workflow_dispatch: pull_request: - types: [opened, synchronize, reopened] - push: - branches: - #Trigger workflow on push to any branch or branch hierarchy: - - '**' + branches: [feature/capgen, main] jobs: build: - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' runs-on: ubuntu-latest strategy: matrix: From c8ad82d587227f068e4a4205232374c5af8f2ee8 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 29 Sep 2023 16:55:54 -0600 Subject: [PATCH 25/49] generalize host --- scripts/ccpp_database_obj.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ccpp_database_obj.py b/scripts/ccpp_database_obj.py index 31aa450c..24579750 100644 --- a/scripts/ccpp_database_obj.py +++ b/scripts/ccpp_database_obj.py @@ -18,7 +18,7 @@ def __init__(self, message): super().__init__(message) class CCPPDatabaseObj: - """Ojbect with data and methods to provide information from a run of capgen. + """Object with data and methods to provide information from a run of capgen. """ def __init__(self, run_env, host_model=None, api=None, database_file=None): @@ -56,7 +56,7 @@ def db_from_file(self, run_env, database_file): datatable.xml file created by capgen. """ metadata_tables = {} - host_name = "cam" + host_name = "host" self.__host_model = HostModel(metadata_tables, host_name, run_env) self.__api = API(sdfs, host_model, scheme_headers, run_env) raise CCPPDatabaseObjError("ERROR: not supported") From 44ea46f73c7a226a3b78eb64e0c4b288ac26ffb6 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Fri, 29 Sep 2023 17:08:44 -0600 Subject: [PATCH 26/49] change to fstrings --- scripts/metavar.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/metavar.py b/scripts/metavar.py index 7f852c4c..64067dd1 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -1427,7 +1427,7 @@ def __init__(self, name, run_env, variables=None, self[stdname] = variables[key] # end for elif variables is not None: - raise ParseInternalError('Illegal type for variables, {} in {}'.format(type_name(variables), self.name)) + raise ParseInternalError(f'Illegal type for variables, {type_name(variables)} in {self.name}') # end if @property @@ -1623,15 +1623,13 @@ def add_variable_dimensions(self, var, ignore_sources, to_dict=None, else: ctx = context_string(var.context) # end if - err_ret += "{}: ".format(self.name) - err_ret += "Cannot find variable for dimension, {}, of {}{}" vstdname = var.get_prop_value('standard_name') - err_ret = err_ret.format(dimname, vstdname, ctx) + err_ret += f"{self.name}: " + err_ret += f"Cannot find variable for dimension, {dimname}, of {vstdname}{ctx}" if dvar: - err_ret += "\nFound {} from excluded source, '{}'{}" + err_ret += f"\nFound {lname} from excluded source, '{dvar.source.ptype}'{dctx}" lname = dvar.get_prop_value('local_name') dctx = context_string(dvar.context) - err_ret = err_ret.format(lname, dvar.source.ptype, dctx) # end if # end if # end if From 3f5c06f2a2661b1238973b0c0ef7f516aaf97bea Mon Sep 17 00:00:00 2001 From: Michael Waxmonsky Date: Fri, 29 Sep 2023 17:15:04 -0600 Subject: [PATCH 27/49] Cleaning up units regex. --- scripts/parse_tools/parse_checkers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index ca5f1f51..ba8722d9 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -12,7 +12,13 @@ ######################################################################## -_UNITS_RE = re.compile(r"^[^/!@#$%^&*=()\|<>\[\]{}?,.]+$") +_UNITLESS_REGEX = "1" +_NON_LEADING_ZERO_NUM = "[1-9]\d*" +_NEGATIVE_NON_LEADING_ZERO_NUM = f"[-]{_NON_LEADING_ZERO_NUM}" +_UNIT_EXPONENT = f"({_NEGATIVE_NON_LEADING_ZERO_NUM}|{_NON_LEADING_ZERO_NUM})" +_UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" +_UNITS_REGEX = f"^({_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" +_UNITS_RE = re.compile(_UNITS_REGEX) def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None From d58facedc1be48914b8ab77317ffd6c33a9a7ae0 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Mon, 2 Oct 2023 15:51:43 -0600 Subject: [PATCH 28/49] remove unnecessary logic from workflow --- .github/workflows/python.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 2f04533e..3775a985 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -28,7 +28,6 @@ jobs: pytest -v doctest: - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.repository == 'NCAR/ccpp-framework' runs-on: ubuntu-latest strategy: matrix: From 07828e68ef76ae086a1e0921420f773163cccafe Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Thu, 12 Oct 2023 16:06:59 -0600 Subject: [PATCH 29/49] update metadata parser test with cleanup --- tests/test_metadata_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_metadata_parser.py b/tests/test_metadata_parser.py index 0f7d18d6..febc3d93 100644 --- a/tests/test_metadata_parser.py +++ b/tests/test_metadata_parser.py @@ -51,7 +51,7 @@ def test_MetadataTable_parse_table(tmpdir): assert len(metadata_header.sections()) == 1 metadata_section = metadata_header.sections()[0] assert metadata_section.name == "" - assert metadata_section.type == "scheme" + assert metadata_section.ptype == "scheme" (im_data,) = metadata_section.variable_list() assert isinstance(im_data, Var) assert im_data.get_dimensions() == [] From 1bacf44bad1748e0ab3aa8ea1adfe80e68136dd1 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Thu, 12 Oct 2023 16:11:25 -0600 Subject: [PATCH 30/49] fix merge --- scripts/parse_tools/parse_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/parse_tools/parse_source.py b/scripts/parse_tools/parse_source.py index 6a521a9b..38a72953 100644 --- a/scripts/parse_tools/parse_source.py +++ b/scripts/parse_tools/parse_source.py @@ -375,7 +375,7 @@ class ParseSource(): """ A simple object for providing source information >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")) #doctest: +ELLIPSIS - <__main__.ParseSource object at 0x...> + >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).ptype 'mytype' >>> ParseSource("myname", "mytype", ParseContext(13, "foo.F90")).name From 55608754d9f7d707d409ab8b70fe3e6af75c6d78 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Thu, 12 Oct 2023 16:27:38 -0600 Subject: [PATCH 31/49] fix merge --- scripts/parse_tools/parse_checkers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index b89d62e2..0dbd627a 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -19,6 +19,7 @@ _UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" _UNITS_REGEX = f"^({_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" _UNITS_RE = re.compile(_UNITS_REGEX) +_MAX_MOLAR_MASS = 10000.0 def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None From 1ead68502b710e874a3df93aca05413839a2e34d Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Mon, 16 Oct 2023 20:24:14 -0600 Subject: [PATCH 32/49] Bug fix in metavar.py: optional var props with a default value need to return the default value when variable doesn't have a value --- scripts/metavar.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/metavar.py b/scripts/metavar.py index dc088fdc..6391a9a0 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -116,8 +116,14 @@ class Var: ['Bob', 'Ray'] >>> Var.get_prop('active') #doctest: +ELLIPSIS + >>> Var.get_prop('active').get_default_val({}) + '.true.' >>> Var.get_prop('active').valid_value('flag_for_aerosol_physics') 'flag_for_aerosol_physics' + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'DDT', ParseContext()), _MVAR_DUMMY_RUN_ENV).get_prop_value('active') + '.true.' + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'active' : 'child_is_home==.true.'}, ParseSource('vname', 'DDT', ParseContext()), _MVAR_DUMMY_RUN_ENV).get_prop_value('active') + 'child_is_home==.true.' >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV).get_prop_value('long_name') 'Hi mom' >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV).get_prop_value('intent') @@ -496,7 +502,7 @@ def get_prop_value(self, name): pvalue = self._prop_dict[name] elif name in Var.__var_propdict: vprop = Var.__var_propdict[name] - if vprop.has_default_func: + if vprop.optional: pvalue = vprop.get_default_val(self._prop_dict, context=self.context) else: From 163c397bfef3bd4d0ad9bd58c0b872ba021d1ce0 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Mon, 16 Oct 2023 20:37:37 -0600 Subject: [PATCH 33/49] Trigger CI From 294eecf673a3af7486bb6b3f00f0f6eb80d838b0 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 17 Oct 2023 11:34:23 -0600 Subject: [PATCH 34/49] Bug fix in scripts/metavar.py: use correct boolean default value 'False' for optional var prop 'polymorphic' --- scripts/metavar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/metavar.py b/scripts/metavar.py index 6391a9a0..8c600546 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -202,7 +202,7 @@ class Var: VariableProperty('active', str, optional_in=True, default_in='.true.'), VariableProperty('polymorphic', bool, optional_in=True, - default_in='.false.')] + default_in=False)] # XXgoldyXX: v debug only __to_add = VariableProperty('valid_values', str, From 370e78d5a05d911bc963d0f96ba8bb620e1bbce3 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Tue, 31 Oct 2023 22:27:13 -0600 Subject: [PATCH 35/49] address reviewer comments --- .github/workflows/python.yaml | 4 +- scripts/ccpp_capgen.py | 5 +- scripts/ccpp_suite.py | 12 +- scripts/constituents.py | 68 ++-- scripts/ddt_library.py | 12 +- scripts/fortran_tools/fortran_write.py | 2 +- scripts/fortran_tools/parse_fortran_file.py | 13 +- scripts/framework_env.py | 6 + scripts/host_cap.py | 26 +- scripts/host_model.py | 3 +- scripts/metadata_table.py | 4 +- scripts/metavar.py | 2 - scripts/parse_tools/__init__.py | 2 +- scripts/parse_tools/parse_checkers.py | 6 +- scripts/parse_tools/parse_log.py | 4 + scripts/parse_tools/xml_tools.py | 21 +- scripts/suite_objects.py | 2 +- src/ccpp_constituent_prop_mod.F90 | 367 ++++++++---------- test/advection_test/test_host_data.F90 | 2 +- test/advection_test/test_reports.py | 4 +- test/capgen_test/test_reports.py | 4 +- test/unit_tests/sample_host_files/ddt1.F90 | 2 +- .../sample_host_files/ddt1_plus.F90 | 2 +- test/unit_tests/sample_host_files/ddt2.F90 | 2 +- .../sample_host_files/ddt2_extra_var.F90 | 2 +- .../sample_host_files/ddt_data1_mod.F90 | 2 +- test/unit_tests/test_metadata_scheme_file.py | 3 - test/var_action_test/test_reports.py | 4 +- 28 files changed, 281 insertions(+), 305 deletions(-) diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 3775a985..bf1e7c36 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 406bff14..2c50eed1 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -613,21 +613,20 @@ def capgen(run_env, return_db=False): # First up, handle the host files host_model = parse_host_model_files(host_files, host_name, run_env) # We always need to parse the ccpp_constituent_prop_ptr_t DDT - ##XXgoldyXX: Should this be in framework_env.py? const_prop_mod = os.path.join(src_dir, "ccpp_constituent_prop_mod.meta") if const_prop_mod not in scheme_files: scheme_files = [const_prop_mod] + scheme_files # end if # Next, parse the scheme files scheme_headers, scheme_tdict = parse_scheme_files(scheme_files, run_env) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): ddts = host_model.ddt_lib.keys() if ddts: run_env.logger.debug("DDT definitions = {}".format(ddts)) # end if # end if plist = host_model.prop_list('local_name') - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): run_env.logger.debug("{} variables = {}".format(host_model.name, plist)) run_env.logger.debug("schemes = {}".format([x.title for x in scheme_headers])) diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 49fe0c48..488d4a27 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -427,7 +427,7 @@ def analyze(self, host_model, scheme_library, ddt_library, run_env): phase = RUN_PHASE_NAME # end if lmsg = "Group {}, schemes = {}" - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): run_env.logger.debug(lmsg.format(item.name, [x.name for x in item.schemes()])) @@ -490,7 +490,7 @@ def write(self, output_dir, run_env): (calling the group caps one after another)""" # Set name of module and filename of cap filename = '{module_name}.F90'.format(module_name=self.module) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): run_env.logger.debug('Writing CCPP suite file, {}'.format(filename)) # end if # Retrieve the name of the constituent module for Group use statements @@ -841,11 +841,11 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): else: inout_vars[0].add(stdname) # end if - elif (intent == 'out' and phase != 'initialize' and constituent - and not const_initialized_in_physics[stdname]): + elif constituent and (intent == 'out' and phase != 'initialize' and not + const_initialized_in_physics[stdname]): # constituents HAVE to be initialized in the init phase because the dycore needs to advect them - emsg = "constituent variable '{}' cannot be initialized in the '{}' phase" - raise CCPPError(emsg.format(stdname, phase)) + emsg = f"constituent variable '{stdname}' cannot be initialized in the '{phase}' phase" + raise CCPPError(emsg) elif intent == 'out' and constituent and phase == 'initialize': const_initialized_in_physics[stdname] = True elif intent == 'out': diff --git a/scripts/constituents.py b/scripts/constituents.py index d3fc37be..119c0275 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -310,8 +310,8 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): vertical_dim = '' # end if advect_str = self.TF_string(var.get_prop_value('advected')) - init_args = [f'std_name="{std_name}"', f'long_name="{long_name}"', - f'units="{units}"', f'vertical_dim="{vertical_dim}"', + init_args = [f'{std_name=}', f'{long_name=}', + f'{units=}', f'{vertical_dim=}', f'advected={advect_str}', f'errcode={errvar_names["ccpp_error_code"]}', f'errmsg={errvar_names["ccpp_error_message"]}'] @@ -496,9 +496,9 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna stmt = f"{substmt}({args}, {err_dummy_str})" cap.write(stmt, 1) cap.comment("Create constituent object for suites in ", 2) - cap.write("", 0) + cap.blank_line() ConstituentVarDict.write_constituent_use_statements(cap, suite_list, 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("character(len=*), intent(in) :: suite_list(:)", 2) cap.write(f"type({CONST_PROP_TYPE}), target, intent(in) :: " + \ @@ -513,7 +513,7 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna cap.write("integer{} :: index".format(spc), 2) cap.write("integer{} :: field_ind".format(spc), 2) cap.write(f"type({CONST_PROP_TYPE}), pointer :: const_prop", 2) - cap.write("", 0) + cap.blank_line() cap.write("{} = 0".format(herrcode), 2) cap.write("num_consts = size(host_constituents, 1)", 2) for suite in suite_list: @@ -543,7 +543,7 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna cap.write("end if", 4) cap.write("end do", 3) cap.write("end if", 2) - cap.write("", 0) + cap.blank_line() # Register suite constituents for suite in suite_list: errvar_str = ConstituentVarDict.__errcode_callstr(herrcode, @@ -578,7 +578,7 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna cap.write("end if", 4) cap.write("end do", 3) cap.write("end if", 2) - cap.write("", 0) + cap.blank_line() # end for cap.write(f"if ({herrcode} == 0) then", 2) stmt = "call {}%lock_table({})" @@ -605,33 +605,33 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna cap.write(f"end {substmt}", 1) # Write constituent_init routine substmt = f"subroutine {init_funcname}" - cap.write("", 0) + cap.blank_line() cap.write(f"{substmt}(ncols, num_layers, {err_dummy_str})", 1) cap.comment("Initialize constituent data", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("integer, intent(in) :: ncols", 2) cap.write("integer, intent(in) :: num_layers", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for evar - cap.write("", 0) + cap.blank_line() call_str = f"call {const_obj_name}%lock_data(ncols, num_layers, {obj_err_callstr})" cap.write(call_str, 2) cap.write(f"end {substmt}", 1) # Write num_consts routine substmt = f"subroutine {num_const_funcname}" - cap.write("", 0) + cap.blank_line() cap.write(f"{substmt}(num_flds, advected, {err_dummy_str})", 1) cap.comment("Return the number of constituent fields for this run", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("integer, intent(out) :: num_flds", 2) cap.write("logical, optional, intent(in) :: advected", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for - cap.write("", 0) + cap.blank_line() call_str = "call {}%num_constituents(num_flds, advected=advected, {})" cap.write(call_str.format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) @@ -658,88 +658,88 @@ def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcna cap.write(f"end {substmt}", 1) # Write copy_in routine substmt = "subroutine {}".format(copy_in_funcname) - cap.write("", 0) + cap.blank_line() cap.write("{}(const_array, {})".format(substmt, err_dummy_str), 1) cap.comment("Copy constituent field info into ", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("real(kind_phys), intent(out) :: const_array(:,:,:)", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for - cap.write("", 0) + cap.blank_line() cap.write("call {}%copy_in(const_array, {})".format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) # Write copy_out routine substmt = "subroutine {}".format(copy_out_funcname) - cap.write("", 0) + cap.blank_line() cap.write("{}(const_array, {})".format(substmt, err_dummy_str), 1) cap.comment("Update constituent field info from ", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("real(kind_phys), intent(in) :: const_array(:,:,:)", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for - cap.write("", 0) + cap.blank_line() cap.write("call {}%copy_out(const_array, {})".format(const_obj_name, obj_err_callstr), 2) cap.write("end {}".format(substmt), 1) # Write constituents routine - cap.write("", 0) + cap.blank_line() cap.write(f"function {const_array_func}() result(const_ptr)", 1) - cap.write("", 0) + cap.blank_line() cap.comment("Return pointer to constituent array", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy argument", 2) cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) - cap.write("", 0) + cap.blank_line() cap.write(f"const_ptr => {const_obj_name}%field_data_ptr()", 2) cap.write(f"end function {const_array_func}", 1) # Write advected constituents routine - cap.write("", 0) + cap.blank_line() cap.write(f"function {advect_array_func}() result(const_ptr)", 1) - cap.write("", 0) + cap.blank_line() cap.comment("Return pointer to advected constituent array", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy argument", 2) cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) - cap.write("", 0) + cap.blank_line() cap.write(f"const_ptr => {const_obj_name}%advected_constituents_ptr()", 2) cap.write(f"end function {advect_array_func}", 1) # Write the constituent property array routine - cap.write("", 0) + cap.blank_line() cap.write(f"function {prop_array_func}() result(const_ptr)", 1) cap.write(f"use {CONST_DDT_MOD}, only: {CONST_PROP_PTR_TYPE}", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Return pointer to array of constituent properties", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy argument", 2) cap.write("type(ccpp_constituent_prop_ptr_t), pointer :: const_ptr(:)", 2) - cap.write("", 0) + cap.blank_line() cap.write(f"const_ptr => {const_obj_name}%constituent_props_ptr()", 2) cap.write(f"end function {prop_array_func}", 1) # Write constituent index function substmt = f"subroutine {const_index_func}" - cap.write("", 0) + cap.blank_line() cap.write(f"{substmt}(stdname, const_index, {err_dummy_str})", 1) cap.comment("Set to the constituent array index " + \ "for .", 2) cap.comment("If is not found, set to -1 " + \ "set an error condition", 2) - cap.write("", 0) + cap.blank_line() cap.comment("Dummy arguments", 2) cap.write("character(len=*), intent(in) :: stdname", 2) cap.write("integer, intent(out) :: const_index", 2) for evar in err_vars: evar.write_def(cap, 2, host, dummy=True, add_intent="out") # end for - cap.write("", 0) + cap.blank_line() cap.write(f"call {const_obj_name}%const_index(const_index, " + \ f"stdname, {obj_err_callstr})", 2) cap.write("end {}".format(substmt), 1) diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 118fb3d7..18bc397a 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -46,7 +46,7 @@ def __init__(self, new_field, var_ref, run_env, recur=False): self.__field = VarDDT(new_field, var_ref.field, run_env, recur=True) # end if if ((not recur) and - run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG)): + run_env.debug_on()): run_env.logger.debug('Adding DDT field, {}'.format(self)) # end if @@ -188,7 +188,7 @@ class DDTLibrary(dict): The dictionary holds known standard names. """ - def __init__(self, name, run_env, ddts=None, logger=None): + def __init__(self, name, run_env, ddts=None): "Our dict is DDT definition headers, key is type" self.__name = '{}_ddt_lib'.format(name) # XXgoldyXX: v remove? @@ -218,9 +218,9 @@ def __init__(self, name, run_env, ddts=None, logger=None): octx = context_string(self[ddt.title].source.context) raise CCPPError(errmsg.format(ddt.title, ctx, octx)) # end if - if logger and logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): lmsg = f"Adding DDT {ddt.title} to {self.name}" - logger.debug(lmsg) + run_env.logger.debug(lmsg) # end if self[ddt.title] = ddt dlen = len(ddt.module) @@ -280,10 +280,10 @@ def collect_ddt_fields(self, var_dict, var, run_env, stdname = dvar.get_prop_value('standard_name') pvar = var_dict.find_variable(standard_name=stdname, any_scope=True) if pvar and (not skip_duplicates): - emsg = "Attempt to add duplicate DDT sub-variable, {}{}." - emsg += "\nVariable originally defined{}" ntx = context_string(dvar.context) ctx = context_string(pvar.context) + emsg = f"Attempt to add duplicate DDT sub-variable, {stdname}{ntx}." + emsg += "\nVariable originally defined{ctx}" raise CCPPError(emsg.format(stdname, ntx, ctx)) # end if # Add this intrinsic to diff --git a/scripts/fortran_tools/fortran_write.py b/scripts/fortran_tools/fortran_write.py index f9bcfa3f..5b186c1c 100644 --- a/scripts/fortran_tools/fortran_write.py +++ b/scripts/fortran_tools/fortran_write.py @@ -191,7 +191,7 @@ def write(self, statement, indent_level, continue_line=False): # End while # Before looking for best space, reject any that are on a # comment line but before any significant characters - if outstr.lstrip()[0] == '!': + if outstr.lstrip().startswith('!'): first_space = outstr.index('!') + 1 while ((outstr[first_space] == '!' or outstr[first_space] == ' ') and diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index 7c6493e7..d6ec1f74 100644 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,8 +563,7 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): module=spec_name, var_dict=var_dict) mheaders.append(mheader) - if (run_env.logger and - run_env.logger.isEnabledFor(logging.DEBUG)): + if run_env.debug_on(): ctx = context_string(pobj, nodir=True) msg = 'Adding header {}{}' run_env.logger.debug(msg.format(mheader.table_name, ctx)) @@ -584,7 +583,7 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): # End if mheaders.append(ddt) if (run_env.logger and - run_env.logger.isEnabledFor(logging.DEBUG)): + run_env.debug_on()): ctx = context_string(pobj, nodir=True) msg = 'Adding DDT {}{}' run_env.logger.debug(msg.format(ddt.table_name, ctx)) @@ -626,7 +625,7 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): inpreamble = False insub = True seen_contains = False - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): ctx = context_string(pobj, nodir=True) msg = "Parsing specification of {}{}" run_env.logger.debug(msg.format(table_name, ctx)) @@ -813,7 +812,7 @@ def parse_specification(pobj, statements, run_env, mod_name=None, errmsg = duplicate_header(mtables[title], tbl) raise CCPPError(errmsg) # end if - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): ctx = tbl.start_context() mtype = tbl.table_type msg = "Adding metadata from {}, {}{}" @@ -894,7 +893,7 @@ def parse_module(pobj, statements, run_env): # End if mod_name = pmatch.group(1) pobj.enter_region('MODULE', region_name=mod_name, nested_ok=False) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): ctx = context_string(pobj, nodir=True) msg = "Parsing Fortran module, {}{}" run_env.logger.debug(msg.format(mod_name, ctx)) @@ -930,7 +929,7 @@ def parse_module(pobj, statements, run_env): errmsg = duplicate_header(mtables[title], mheader) raise CCPPError(errmsg) # end if - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): mtype = mheader.table_type ctx = mheader.start_context() msg = "Adding metadata from {}, {}{}" diff --git a/scripts/framework_env.py b/scripts/framework_env.py index 6456134e..af941bd4 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -10,6 +10,7 @@ # Python library imports import argparse import os +from parse_tools import debug_enabled _EPILOG = ''' ''' @@ -275,6 +276,11 @@ def kind_types(self): CCPPFrameworkEnv object.""" return self.__kind_dict.keys() + def debug_on(self): + """Return true if debug enabled for the CCPPFrameworkEnv's + logger object.""" + return (self.logger and debug_enabled(self.logger)) + @property def use_error_obj(self): """Return the property for this diff --git a/scripts/host_cap.py b/scripts/host_cap.py index d2b3066f..268a1855 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -83,7 +83,7 @@ def constituent_num_suite_subname(host_model): ############################################################################### """Return the name of the number of suite constituents for this run Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_num_suite_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_num_suite_constituents" ############################################################################### def constituent_register_subname(host_model): @@ -91,7 +91,7 @@ def constituent_register_subname(host_model): """Return the name of the subroutine used to register the constituent properties for this run. Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_register_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_register_constituents" ############################################################################### def constituent_initialize_subname(host_model): @@ -99,7 +99,7 @@ def constituent_initialize_subname(host_model): """Return the name of the subroutine used to initialize the constituents for this run. Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_initialize_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_initialize_constituents" ############################################################################### def constituent_num_consts_funcname(host_model): @@ -107,7 +107,7 @@ def constituent_num_consts_funcname(host_model): """Return the name of the function to return the number of constituents for this run. Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_number_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_number_constituents" ############################################################################### def query_scheme_constituents_funcname(host_model): @@ -123,7 +123,7 @@ def constituent_copyin_subname(host_model): """Return the name of the subroutine to copy constituent fields to the host model. Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_gather_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_gather_constituents" ############################################################################### def constituent_copyout_subname(host_model): @@ -131,7 +131,7 @@ def constituent_copyout_subname(host_model): """Return the name of the subroutine to update constituent fields from the host model. Because this is a user interface API function, the name is fixed.""" - return "{}_ccpp_update_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_update_constituents" ############################################################################### def unique_local_name(loc_name, host_model): @@ -163,14 +163,14 @@ def constituent_model_object_name(host_model): def constituent_model_const_stdnames(host_model): ############################################################################### """Return the name of the array of constituent standard names""" - hstr = "{}_model_const_stdnames".format(host_model.name) + hstr = f"{host_model.name}_model_const_stdnames" return unique_local_name(hstr, host_model) ############################################################################### def constituent_model_const_indices(host_model): ############################################################################### """Return the name of the array of constituent field array indices""" - hstr = "{}_model_const_indices".format(host_model.name) + hstr = f"{host_model.name}_model_const_indices" return unique_local_name(hstr, host_model) ############################################################################### @@ -178,7 +178,7 @@ def constituent_model_consts(host_model): ############################################################################### """Return the name of the function that will return a pointer to the array of all constituents""" - hstr = "{}_constituents_array".format(host_model.name) + hstr = f"{host_model.name}_constituents_array" return unique_local_name(hstr, host_model) ############################################################################### @@ -186,14 +186,14 @@ def constituent_model_advected_consts(host_model): ############################################################################### """Return the name of the function that will return a pointer to the array of advected constituents""" - hstr = "{}_advected_constituents_array".format(host_model.name) + hstr = f"{host_model.name}_advected_constituents_array" return unique_local_name(hstr, host_model) ############################################################################### def constituent_model_const_props(host_model): ############################################################################### """Return the name of the array of constituent property object pointers""" - hstr = "{}_model_const_properties".format(host_model.name) + hstr = f"{host_model.name}_model_const_properties" return unique_local_name(hstr, host_model) ############################################################################### @@ -201,7 +201,7 @@ def constituent_model_const_index(host_model): ############################################################################### """Return the name of the interface that returns the array index of a constituent array given its standard name""" - hstr = "{}_const_get_index".format(host_model.name) + hstr = f"{host_model.name}_const_get_index" return unique_local_name(hstr, host_model) ############################################################################### @@ -239,7 +239,7 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): # Add entries for each constituent (once per standard name) const_stdnames = set() for suite in suite_list: - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): lmsg = "Adding constituents from {} to {}" run_env.logger.debug(lmsg.format(suite.name, host_model.name)) # end if diff --git a/scripts/host_model.py b/scripts/host_model.py index 68a969e7..c655421b 100644 --- a/scripts/host_model.py +++ b/scripts/host_model.py @@ -19,6 +19,7 @@ class HostModel(VarDictionary): def __init__(self, meta_tables, name_in, run_env): """Initialize this HostModel object. is a dictionary of parsed host metadata tables. + - dictionary key is title of metadata argtable is the name for this host model. is the CCPPFrameworkEnv object for this framework run. """ @@ -150,7 +151,7 @@ def ddt_lib(self): @property def constituent_module(self): """Return the name of host model constituent module""" - return "{}_ccpp_constituents".format(self.name) + return f"{self.name}_ccpp_constituents" def argument_list(self, loop_vars=True): """Return a string representing the host model variable arg list""" diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 49f4ffb1..0afe30d3 100755 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -792,7 +792,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): self.__pobj.add_syntax_err(mismatch) self.__section_valid = False # end if - if run_env.logger and run_env.logger.isEnabledFor(logging.INFO): + if run_env.debug_on(): run_env.logger.info("Parsing {} {}{}".format(self.header_type, self.title, start_ctx)) # end if @@ -812,7 +812,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): newvar, curr_line = self.parse_variable(curr_line, known_ddts) valid_lines = newvar is not None if valid_lines: - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): dmsg = 'Adding {} to {}' lname = newvar.get_prop_value('local_name') run_env.logger.debug(dmsg.format(lname, self.title)) diff --git a/scripts/metavar.py b/scripts/metavar.py index eb1c28f8..b91e42ce 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -792,8 +792,6 @@ def intrinsic_elements(self, check_dict=None, ddt_lib=None): if not element_names: element_names = None # end if - else: - element_names = None # end if # end if children = self.children() diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index fbe05357..736910f0 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -25,7 +25,7 @@ from parse_checkers import check_default_value, check_valid_values, check_molar_mass from parse_log import init_log, set_log_level, flush_log from parse_log import set_log_to_stdout, set_log_to_null -from parse_log import set_log_to_file +from parse_log import set_log_to_file, debug_enabled from preprocess import PreprocStack from xml_tools import find_schema_file, find_schema_version from xml_tools import read_xml_file, validate_xml_file diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index 0dbd627a..f32df45d 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -19,7 +19,7 @@ _UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" _UNITS_REGEX = f"^({_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" _UNITS_RE = re.compile(_UNITS_REGEX) -_MAX_MOLAR_MASS = 10000.0 +_MAX_MOLAR_MASS = 10000.0 def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None @@ -978,7 +978,7 @@ def check_molar_mass(test_val, prop_dict, error): test_val = float(test_val) if test_val < 0.0 or test_val > _MAX_MOLAR_MASS: if error: - raise CCPPError("{} is not a valid molar mass".format(test_val)) + raise CCPPError(f"{test_val} is not a valid molar mass") else: test_val = None # end if @@ -986,7 +986,7 @@ def check_molar_mass(test_val, prop_dict, error): except: # not an int or float, conditionally throw error if error: - raise CCPPError("{} is invalid; not a float or int".format(test_val)) + raise CCPPError(f"{test_val} is invalid; not a float or int") else: test_val=None # end if diff --git a/scripts/parse_tools/parse_log.py b/scripts/parse_tools/parse_log.py index e6561427..9707042a 100644 --- a/scripts/parse_tools/parse_log.py +++ b/scripts/parse_tools/parse_log.py @@ -47,3 +47,7 @@ def flush_log(logger): """Flush all pending output from """ for handler in list(logger.handlers): handler.flush() + +def debug_enabled(logger): + """Return true if debug is enabled for this logger""" + return logger.isEnabledFor(logging.DEBUG) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index d982da45..0ff56b3a 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -176,9 +176,9 @@ def validate_xml_file(filename, schema_root, version, logger, schema_file = find_schema_file(schema_root, version, schema_path) if not (schema_file and os.path.isfile(schema_file)): verstring = '.'.join([str(x) for x in version]) - emsg = """validate_xml_file: Cannot find schema for version {}, - {} does not exist""" - raise CCPPError(emsg.format(verstring, schema_file)) + emsg = f"""validate_xml_file: Cannot find schema for version {verstring}, + {schema_file} does not exist""" + raise CCPPError(emsg) # end if # end if if not os.access(schema_file, os.R_OK): @@ -256,16 +256,11 @@ def write(self, file, encoding="us-ascii", xml_declaration=None, default_namespace=None, method="xml", short_empty_elements=True): """Subclassed write method to format output.""" - if PYSUBVER >= 8: - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method, - xml_declaration=xml_declaration, - default_namespace=default_namespace, - short_empty_elements=short_empty_elements) - else: - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method, - short_empty_elements=short_empty_elements) + et_str = ET.tostring(self.getroot(), + encoding=encoding, method=method, + xml_declaration=xml_declaration, + default_namespace=default_namespace, + short_empty_elements=short_empty_elements) # end if fmode = 'wt' root = str(et_str, encoding="utf-8") diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 5a710548..0be6de15 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1287,7 +1287,7 @@ def __init__(self, index_name, context, parent, run_env, items=None): # self._local_dim_name is the variable name for self._dim_name self._local_dim_name = None super().__init__(index_name, context, parent, run_env) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.debug_on(): lmsg = "Adding VerticalLoop for '{}'" run_env.logger.debug(lmsg.format(index_name)) # end if diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index c9f5322b..7f0637e9 100644 --- a/src/ccpp_constituent_prop_mod.F90 +++ b/src/ccpp_constituent_prop_mod.F90 @@ -184,7 +184,7 @@ module ccpp_constituent_prop_mod ! Private interfaces private to_str private initialize_errvars - private set_errvars + private append_errvars private handle_allocate_error private check_var_bounds @@ -243,19 +243,14 @@ end subroutine initialize_errvars !####################################################################### - subroutine set_errvars(errcode_val, errmsg_val, errcode, errmsg, & - errmsg2, errmsg3, errmsg4, errmsg5) - ! Set error variables, if present + subroutine append_errvars(errcode_val, errmsg_val, errcode, errmsg) + ! Append to error variables, if present ! Dummy arguments - integer, optional, intent(in) :: errcode_val - character(len=*), optional, intent(in) :: errmsg_val + integer, intent(in) :: errcode_val + character(len=*), intent(in) :: errmsg_val integer, optional, intent(inout) :: errcode character(len=*), optional, intent(inout) :: errmsg - character(len=*), optional, intent(in) :: errmsg2 - character(len=*), optional, intent(in) :: errmsg3 - character(len=*), optional, intent(in) :: errmsg4 - character(len=*), optional, intent(in) :: errmsg5 ! Local variable integer :: emsg_len @@ -269,24 +264,8 @@ subroutine set_errvars(errcode_val, errmsg_val, errcode, errmsg, & end if emsg_len = len_trim(errmsg) errmsg(emsg_len+1:) = trim(errmsg_val) - if (present(errmsg2)) then - emsg_len = len_trim(errmsg) - errmsg(emsg_len+1:) = trim(errmsg2) - end if - if (present(errmsg3)) then - emsg_len = len_trim(errmsg) - errmsg(emsg_len+1:) = trim(errmsg3) - end if - if (present(errmsg4)) then - emsg_len = len_trim(errmsg) - errmsg(emsg_len+1:) = trim(errmsg4) - end if - if (present(errmsg5)) then - emsg_len = len_trim(errmsg) - errmsg(emsg_len+1:) = trim(errmsg5) - end if end if - end subroutine set_errvars + end subroutine append_errvars !####################################################################### @@ -301,10 +280,8 @@ subroutine handle_allocate_error(astat, fieldname, errcode, errmsg) call initialize_errvars(errcode, errmsg) if (astat /= 0) then - call set_errvars(astat, "Error allocating ", errcode=errcode, & - errmsg=errmsg, errmsg2="ccpp_constituent_properties_t", & - errmsg3="object component, "//trim(fieldname), & - errmsg4=", error code = ", errmsg5=to_str(astat)) + call append_errvars(astat, "Error allocating ccpp_constituent_properties_t object component " // & + trim(fieldname) // ", error code = " // to_str(astat), errcode=errcode, errmsg=errmsg) end if end subroutine handle_allocate_error @@ -323,8 +300,8 @@ subroutine check_var_bounds(var, var_bound, varname, errcode, errmsg) call initialize_errvars(errcode, errmsg) if (var > var_bound) then - call set_errvars(1, trim(varname)//" exceeds its upper bound, ", & - errcode=errcode, errmsg=errmsg, errmsg2=to_str(var_bound)) + call append_errvars(1, trim(varname)//" exceeds its upper bound, " // & + to_str(var_bound), errcode=errcode, errmsg=errmsg) end if end subroutine check_var_bounds @@ -358,8 +335,8 @@ logical function ccp_is_instantiated(this, errcode, errmsg) ccp_is_instantiated = allocated(this%var_std_name) call initialize_errvars(errcode, errmsg) if (.not. ccp_is_instantiated) then - call set_errvars(1, "ccpp_constituent_properties_t object ", & - errcode=errcode, errmsg=errmsg, errmsg2="is not initialized") + call append_errvars(1, "ccpp_constituent_properties_t object is not initialized", & + errcode=errcode, errmsg=errmsg) end if end function ccp_is_instantiated @@ -402,8 +379,7 @@ subroutine ccp_instantiate(this, std_name, long_name, units, vertical_dim, & if (present(default_value)) then this%const_default_value = default_value end if - end if - if (errcode == 0) then + ! Determine if this is a (moist) mixing ratio or volume mixing ratio if (index(this%var_std_name, "volume_mixing_ratio") > 0) then this%const_type = volume_mixing_ratio else if (index(this%var_std_name, "number_concentration") > 0) then @@ -411,9 +387,6 @@ subroutine ccp_instantiate(this, std_name, long_name, units, vertical_dim, & else this%const_type = mass_mixing_ratio end if - ! Determine if this is a (moist) mixing ratio or volume mixing ratio - end if - if (errcode == 0) then ! Determine if this mixing ratio is dry, moist, or "wet". if (index(this%var_std_name, "wrt_moist_air") > 0) then this%const_water = moist_mixing_ratio @@ -591,9 +564,8 @@ subroutine ccp_set_const_index(this, index, errcode, errmsg) if (this%const_ind == int_unassigned) then this%const_ind = index else - call set_errvars(1, "ccpp_constituent_properties_t ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="const index is already set") + call append_errvars(1, "ccpp_constituent_properties_t const index " // & + "is already set", errcode=errcode, errmsg=errmsg) end if end if @@ -891,13 +863,13 @@ logical function ccp_model_const_locked(this, errcode, errmsg, warn_func) end if else if (present(warn_func)) then - call set_errvars(1, trim(warn_func), & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents not initialized") + call append_errvars(1, trim(warn_func) // & + " WARNING: Model constituents not initialized", & + errcode=errcode, errmsg=errmsg) else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents not initialized") + call append_errvars(1, subname // & + " WARNING: Model constituents not initialized", & + errcode=errcode, errmsg=errmsg) end if end if @@ -930,13 +902,13 @@ logical function ccp_model_const_props_locked(this, errcode, errmsg, warn_func) end if else if (present(warn_func)) then - call set_errvars(1, trim(warn_func), & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent properties not initialized") + call append_errvars(1, trim(warn_func) // & + " WARNING: Model constituent properties not initialized", & + errcode=errcode, errmsg=errmsg) else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent properties not initialized") + call append_errvars(1, subname // & + " WARNING: Model constituent properties not initialized", & + errcode=errcode, errmsg=errmsg) end if end if @@ -969,13 +941,13 @@ logical function ccp_model_const_data_locked(this, errcode, errmsg, warn_func) end if else if (present(warn_func)) then - call set_errvars(1, trim(warn_func), & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent data not initialized") + call append_errvars(1, trim(warn_func) // & + " WARNING: Model constituent data not initialized", & + errcode=errcode, errmsg=errmsg) else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent data not initialized") + call append_errvars(1, subname // & + " WARNING: Model constituent data not initialized", & + errcode=errcode, errmsg=errmsg) end if end if @@ -1004,24 +976,24 @@ logical function ccp_model_const_okay_to_add(this, errcode, errmsg, & errmsg=errmsg, warn_func=subname)) if (.not. ccp_model_const_okay_to_add) then if (present(warn_func)) then - call set_errvars(1, trim(warn_func), & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents are locked") + call append_errvars(1, trim(warn_func) // & + " WARNING: Model constituents are locked", & + errcode=errcode, errmsg=errmsg) else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents are locked") + call append_errvars(1, subname // & + " WARNING: Model constituents are locked", & + errcode=errcode, errmsg=errmsg) end if end if else if (present(warn_func)) then - call set_errvars(1, trim(warn_func), & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents not initialized") + call append_errvars(1, trim(warn_func) // & + " WARNING: Model constituents not initialized", & + errcode=errcode, errmsg=errmsg) else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents not initialized") + call append_errvars(1, subname // & + " WARNING: Model constituents not initialized", & + errcode=errcode, errmsg=errmsg) end if end if @@ -1047,7 +1019,7 @@ subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) !!XXgoldyXX: Add check on key to see if incompatible item already there. call this%hash_table%add_hash_key(field_data, error) if (len_trim(error) > 0) then - call set_errvars(1, trim(error), errcode=errcode, errmsg=errmsg) + call append_errvars(1, trim(error), errcode=errcode, errmsg=errmsg) else ! If we get here we are successful, add to variable count if (field_data%is_layer_var()) then @@ -1056,19 +1028,19 @@ subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) if (present(errmsg)) then call field_data%vertical_dimension(error, & errcode=errcode, errmsg=errmsg) - if (len_trim(errmsg) == 0) then - call set_errvars(1, & - "ERROR: Unknown vertical dimension, '", & - errcode=errcode, errmsg=errmsg, & - errmsg2=trim(error), errmsg3="'") + if (errcode /= 0) then + call append_errvars(1, & + "ERROR: Unknown vertical dimension, '" // & + trim(error) // "'", & + errcode=errcode, errmsg=errmsg) end if end if end if end if else - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituents are locked") + call append_errvars(1, subname // & + "WARNING: Model constituents are locked", & + errcode=errcode, errmsg=errmsg) end if end subroutine ccp_model_const_add_metadata @@ -1119,16 +1091,15 @@ function ccp_model_const_find_const(this, standard_name, errcode, errmsg) & nullify(cprop) hval => this%hash_table%table_value(standard_name, errmsg=error) if (len_trim(error) > 0) then - call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & - errmsg2=": "//trim(error)) + call append_errvars(1, subname // ": "//trim(error), errcode=errcode, & + errmsg=errmsg) else select type(hval) type is (ccpp_constituent_properties_t) cprop => hval class default - call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: Bad hash table value", & - errmsg3=trim(standard_name)) + call append_errvars(1, subname // " ERROR: Bad hash table value " // & + trim(standard_name), errcode=errcode, errmsg=errmsg) end select end if @@ -1148,6 +1119,7 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) integer :: index_advect integer :: num_vars integer :: astat + integer :: errcode_local logical :: check type(ccpp_hash_iterator_t) :: hiter class(ccpp_hashable_t), pointer :: hval @@ -1156,10 +1128,12 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) character(len=*), parameter :: subname = 'ccp_model_const_table_lock' astat = 0 + errcode_local = 0 if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent properites already locked, ignoring") - astat = astat + 1 + call append_errvars(1, subname // & + " WARNING: Model constituents properties already locked, ignoring", & + errcode=errcode, errmsg=errmsg) + errcode_local = 1 else ! Make sure everything is really initialized call this%reset(clear_hash_table=.false.) @@ -1191,17 +1165,18 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) end do ! Sanity check on num_advect if (this%num_advected_vars > num_vars) then - astat = 1 - call set_errvars(astat, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: num_advected_vars index out of bounds") - astat = astat + 1 + call append_errvars(1, subname // & + " ERROR: num_advected_vars index " // & + to_str(this%num_advected_vars) // & + " out of bounds " // to_str(num_vars), & + errcode=errcode, errmsg=errmsg) + errcode_local = 1 end if end if index_advect = 0 index_const = this%num_advected_vars ! Iterate through the hash table to find entries - if (astat == 0) then + if (errcode_local == 0) then call hiter%initialize(this%hash_table) do if (hiter%valid()) then @@ -1213,10 +1188,11 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) if (check) then index_advect = index_advect + 1 if (index_advect > this%num_advected_vars) then - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: const a index out of bounds") - astat = astat + 1 + call append_errvars(1, subname // " ERROR: const a index " // & + to_str(index_advect) // " out of bounds " // & + to_str(this%num_advected_vars), & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 exit end if call cprop%set_const_index(index_advect, & @@ -1225,10 +1201,10 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) else index_const = index_const + 1 if (index_const > num_vars) then - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: const v index out of bounds") - astat = astat + 1 + call append_errvars(1, subname // " ERROR: const v index " // & + to_str(index_const) // " out of bounds " // to_str(num_vars), & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 exit end if call cprop%set_const_index(index_const, & @@ -1239,18 +1215,15 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) if (.not. cprop%is_layer_var()) then call cprop%vertical_dimension(dimname, & errcode=errcode, errmsg=errmsg) - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: Bad vertical dimension, '", & - errmsg3=trim(dimname)) - astat = astat + 1 + call append_errvars(1, subname // " ERROR: Bad vertical dimension, '" // & + trim(dimname), errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 exit end if class default - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2="ERROR: Bad hash table value") - astat = astat + 1 + call append_errvars(1, subname // " ERROR: Bad hash table value", & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 exit end select call hiter%next() @@ -1260,24 +1233,25 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) end do ! Some size sanity checks if (index_const /= this%hash_table%num_values()) then - call set_errvars(errcode + 1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: Too few constituents found in hash table") - astat = astat + 1 + call append_errvars(1, subname // & + " ERROR: Too few constituents " // to_str(index_const) // & + " found in hash table " // to_str(this%hash_table%num_values()), & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 end if if (index_advect /= this%num_advected_vars) then - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=" ERROR: Too few advected constituents found ", & - errmsg3="in hash table") - astat = astat + 1 + call append_errvars(1, subname // & + " ERROR: Too few advected constituents " // to_str(index_const) // & + " found in hash table " // to_str(this%hash_table%num_values()), & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 end if if (present(errcode)) then if (errcode /= 0) then - astat = 1 + errcode_local = 1 end if end if - if (astat == 0) then + if (errcode_local == 0) then this%table_locked = .true. end if end if @@ -1297,30 +1271,35 @@ subroutine ccp_model_const_data_lock(this, ncols, num_layers, errcode, errmsg) integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg ! Local variables - integer :: astat, index + integer :: astat, index, errcode_local real(kind=kind_phys) :: default_value character(len=*), parameter :: subname = 'ccp_model_const_data_lock' + errcode_local = 0 if (this%const_data_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent data already locked, ignoring") - astat = astat + 1 + call append_errvars(1, subname // & + " WARNING: Model constituent data already locked, ignoring", & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 else if (.not. this%const_props_locked(errcode=errcode, errmsg=errmsg, & warn_func=subname)) then - call set_errvars(1, subname, errcode=errcode, errmsg=errmsg, & - errmsg2=" WARNING: Model constituent properties not yet locked, ignoring") - astat = astat + 1 + call append_errvars(1, subname // & + " WARNING: Model constituent properties not yet locked, ignoring", & + errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 else allocate(this%vars_layer(ncols, num_layers, this%hash_table%num_values()), & stat=astat) call handle_allocate_error(astat, 'vars_layer', & errcode=errcode, errmsg=errmsg) + errcode_local = astat if (astat == 0) then allocate(this%vars_minvalue(this%hash_table%num_values()), stat=astat) call handle_allocate_error(astat, 'vars_minvalue', & errcode=errcode, errmsg=errmsg) + errcode_local = astat end if - if (astat == 0) then + if (errcode_local == 0) then this%num_layers = num_layers do index = 1, this%hash_table%num_values() call this%const_metadata(index)%default_value(default_value, & @@ -1331,10 +1310,10 @@ subroutine ccp_model_const_data_lock(this, ncols, num_layers, errcode, errmsg) end if if (present(errcode)) then if (errcode /= 0) then - astat = 1 + errcode_local = 1 end if end if - if (astat == 0) then + if (errcode_local == 0) then this%data_locked = .true. end if end if @@ -1540,40 +1519,39 @@ subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=": Too many constituents for ") + call append_errvars(1, subname // & + ": Too many constituents for ", & + errcode=errcode, errmsg=errmsg) exit end if ! Copy this constituent's field data to call this%const_metadata(index)%const_index(fld_ind) if (fld_ind /= index) then call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname//": ERROR: ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="bad field index, "//to_str(fld_ind), & - errmsg3=" for '"//trim(std_name)//"', ", & - errmsg4="should have been "//to_str(index)) + call append_errvars(1, subname//": ERROR: "// & + "bad field index, "//to_str(fld_ind)// & + " for '"//trim(std_name)//"', should have been "// & + to_str(index), errcode=errcode, errmsg=errmsg) exit else if (this%const_metadata(index)%is_layer_var()) then if (this%num_layers == num_levels) then const_array(:,:,cindex) = this%vars_layer(:,:,fld_ind) else call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=": Wrong number of vertical levels for '", & - errmsg3=trim(std_name)//"', "//to_str(num_levels), & - errmsg4=", expected"//to_str(this%num_layers)) + call append_errvars(1, subname//": ERROR: "// & + "Wrong number of vertical levels for '"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected "//to_str(this%num_layers), & + errcode=errcode, errmsg=errmsg) exit end if else call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname//": Unsupported var type, ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="wrong number of vertical levels for '", & - errmsg3=trim(std_name)//"', "//to_str(num_levels), & - errmsg4=", expected"//to_str(this%num_layers)) + call append_errvars(1, subname//": Unsupported var type,"// & + " wrong number of vertical levels for '"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected"//to_str(this%num_layers), & + errcode=errcode, errmsg=errmsg) exit end if end if @@ -1617,40 +1595,39 @@ subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=": Too many constituents for ") + call append_errvars(1, subname// & + ": Too many constituents for ", & + errcode=errcode, errmsg=errmsg) exit end if ! Copy this field of to to constituent's field data call this%const_metadata(index)%const_index(fld_ind) if (fld_ind /= index) then call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname//": ERROR: ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="bad field index, "//to_str(fld_ind), & - errmsg3=" for '"//trim(std_name)//"', ", & - errmsg4="should have been "//to_str(index)) + call append_errvars(1, subname//": ERROR: "// & + "bad field index, "//to_str(fld_ind)// & + " for '"//trim(std_name)//"', should have been"// & + to_str(index), errcode=errcode, errmsg=errmsg) exit else if (this%const_metadata(index)%is_layer_var()) then if (this%num_layers == num_levels) then this%vars_layer(:,:,fld_ind) = const_array(:,:,cindex) else call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname, & - errcode=errcode, errmsg=errmsg, & - errmsg2=": Wrong number of vertical levels for '", & - errmsg3=trim(std_name)//"', "//to_str(num_levels), & - errmsg4=", expected"//to_str(this%num_layers)) + call append_errvars(1, subname// & + ": Wrong number of vertical levels for '"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected"//to_str(this%num_layers), & + errcode=errcode, errmsg=errmsg) exit end if else call this%const_metadata(index)%standard_name(std_name) - call set_errvars(1, subname//": Unsupported var type, ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="wrong number of vertical levels for '", & - errmsg3=trim(std_name)//"', "//to_str(num_levels), & - errmsg4=", expected"//to_str(this%num_layers)) + call append_errvars(1, subname//": Unsupported var type,"// & + " wrong number of vertical levels for'"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected "//to_str(this%num_layers), & + errcode=errcode, errmsg=errmsg) exit end if end if @@ -1666,7 +1643,7 @@ function ccp_field_data_ptr(this) result(const_ptr) ! Dummy arguments class(ccpp_model_constituents_t), target, intent(inout) :: this - real(kind_phys), pointer :: const_ptr(:,:,:) + real(kind_phys), pointer :: const_ptr(:,:,:) ! Local variables integer :: errcode character(len=errmsg_len) :: errmsg @@ -1749,7 +1726,7 @@ subroutine ccpt_get_standard_name(this, std_name, errcode, errmsg) call this%prop%standard_name(std_name, errcode, errmsg) else std_name = '' - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1772,7 +1749,7 @@ subroutine ccpt_get_long_name(this, long_name, errcode, errmsg) call this%prop%long_name(long_name, errcode, errmsg) else long_name = '' - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1793,11 +1770,11 @@ subroutine ccpt_get_vertical_dimension(this, vert_dim, errcode, errmsg) if (associated(this%prop)) then if (this%prop%is_instantiated(errcode, errmsg)) then - vert_dim = this%prop%vert_dim + call this%prop%vertical_dimension(vert_dim, errcode, errmsg) end if else vert_dim = '' - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1880,7 +1857,7 @@ subroutine ccpt_const_index(this, index, errcode, errmsg) index = this%prop%const_index(errcode, errmsg) else index = int_unassigned - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1902,7 +1879,7 @@ subroutine ccpt_is_thermo_active(this, val_out, errcode, errmsg) call this%prop%is_thermo_active(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1924,7 +1901,7 @@ subroutine ccpt_is_advected(this, val_out, errcode, errmsg) call this%prop%is_advected(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1946,7 +1923,7 @@ subroutine ccpt_is_mass_mixing_ratio(this, val_out, errcode, errmsg) call this%prop%is_mass_mixing_ratio(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1968,7 +1945,7 @@ subroutine ccpt_is_volume_mixing_ratio(this, val_out, errcode, errmsg) call this%prop%is_volume_mixing_ratio(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -1990,7 +1967,7 @@ subroutine ccpt_is_number_concentration(this, val_out, errcode, errmsg) call this%prop%is_number_concentration(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2012,7 +1989,7 @@ subroutine ccpt_is_dry(this, val_out, errcode, errmsg) call this%prop%is_dry(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2034,7 +2011,7 @@ subroutine ccpt_is_moist(this, val_out, errcode, errmsg) call this%prop%is_moist(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2056,7 +2033,7 @@ subroutine ccpt_is_wet(this, val_out, errcode, errmsg) call this%prop%is_wet(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2078,7 +2055,7 @@ subroutine ccpt_min_val(this, val_out, errcode, errmsg) call this%prop%minimum(val_out, errcode, errmsg) else val_out = kphys_unassigned - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2100,7 +2077,7 @@ subroutine ccpt_molec_weight(this, val_out, errcode, errmsg) call this%prop%molec_weight(val_out, errcode, errmsg) else val_out = kphys_unassigned - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2122,7 +2099,7 @@ subroutine ccpt_default_value(this, val_out, errcode, errmsg) call this%prop%default_value(val_out, errcode, errmsg) else val_out = kphys_unassigned - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2144,7 +2121,7 @@ subroutine ccpt_has_default(this, val_out, errcode, errmsg) call this%prop%has_default(val_out, errcode, errmsg) else val_out = .false. - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2173,8 +2150,8 @@ subroutine ccpt_set(this, const_ptr, errcode, errmsg) trim(stdname), "'" end if errcode = errcode + 1 - call set_errvars(1, "ccpt_set: ", errcode=errcode, errmsg=errmsg, & - errmsg2=trim(errmsg2)) + call append_errvars(1, "ccpt_set: "//trim(errmsg2), errcode=errcode, & + errmsg=errmsg) else this%prop => const_ptr end if @@ -2216,13 +2193,13 @@ subroutine ccpt_set_const_index(this, index, errcode, errmsg) if (this%prop%const_ind == int_unassigned) then this%prop%const_ind = index else - call set_errvars(1, "ccpp_constituent_prop_ptr_t ", & - errcode=errcode, errmsg=errmsg, & - errmsg2="const index is already set") + call append_errvars(1, "ccpp_constituent_prop_ptr_t "// & + "const index is already set", & + errcode=errcode, errmsg=errmsg) end if end if else - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if @@ -2248,7 +2225,7 @@ subroutine ccpt_set_thermo_active(this, thermo_flag, errcode, errmsg) this%prop%thermo_active = thermo_flag end if else - call set_errvars(1, subname//": invalid constituent pointer", & + call append_errvars(1, subname//": invalid constituent pointer", & errcode=errcode, errmsg=errmsg) end if diff --git a/test/advection_test/test_host_data.F90 b/test/advection_test/test_host_data.F90 index fce25c66..c2d99798 100644 --- a/test/advection_test/test_host_data.F90 +++ b/test/advection_test/test_host_data.F90 @@ -7,7 +7,7 @@ module test_host_data type physics_state real(kind_phys), allocatable :: ps(:) ! surface pressure real(kind_phys), allocatable :: temp(:,:) ! temperature - real(kind_phys), pointer :: q(:,:,:) => NULL() ! constituent array + real(kind_phys), dimension(:,:,:), pointer :: q => NULL() ! constituent array end type physics_state public allocate_physics_state diff --git a/test/advection_test/test_reports.py b/test/advection_test/test_reports.py index c28fe38a..17e9a2ad 100644 --- a/test/advection_test/test_reports.py +++ b/test/advection_test/test_reports.py @@ -22,8 +22,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR) diff --git a/test/capgen_test/test_reports.py b/test/capgen_test/test_reports.py index 3749e8ac..e8237476 100644 --- a/test/capgen_test/test_reports.py +++ b/test/capgen_test/test_reports.py @@ -23,8 +23,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR) diff --git a/test/unit_tests/sample_host_files/ddt1.F90 b/test/unit_tests/sample_host_files/ddt1.F90 index 966a62a6..71b22b4f 100644 --- a/test/unit_tests/sample_host_files/ddt1.F90 +++ b/test/unit_tests/sample_host_files/ddt1.F90 @@ -9,7 +9,7 @@ module ddt1 !! \htmlinclude ddt1_t.html !! type, public :: ddt1_t - integer, private :: num_vars = 0 + integer, public :: num_vars = 0 real(kind_phys), allocatable :: vars(:,:,:) end type ddt1_t diff --git a/test/unit_tests/sample_host_files/ddt1_plus.F90 b/test/unit_tests/sample_host_files/ddt1_plus.F90 index 90571a73..d1806932 100644 --- a/test/unit_tests/sample_host_files/ddt1_plus.F90 +++ b/test/unit_tests/sample_host_files/ddt1_plus.F90 @@ -16,7 +16,7 @@ module ddt1_plus !! \htmlinclude ddt2_t.html !! type, public :: ddt2_t - integer, private :: num_vars = 0 + integer, public :: num_vars = 0 real(kind_phys), allocatable :: vars(:,:,:) end type ddt2_t diff --git a/test/unit_tests/sample_host_files/ddt2.F90 b/test/unit_tests/sample_host_files/ddt2.F90 index f4ed9cd0..22d5af0e 100644 --- a/test/unit_tests/sample_host_files/ddt2.F90 +++ b/test/unit_tests/sample_host_files/ddt2.F90 @@ -16,7 +16,7 @@ module ddt2 !! \htmlinclude ddt2_t.html !! type, public :: ddt2_t - integer, private :: num_vars = 0 + integer, public :: num_vars = 0 real(kind_phys), allocatable :: vars(:,:,:) end type ddt2_t diff --git a/test/unit_tests/sample_host_files/ddt2_extra_var.F90 b/test/unit_tests/sample_host_files/ddt2_extra_var.F90 index 07bafe4b..00b4c170 100644 --- a/test/unit_tests/sample_host_files/ddt2_extra_var.F90 +++ b/test/unit_tests/sample_host_files/ddt2_extra_var.F90 @@ -16,7 +16,7 @@ module ddt2_extra_var !! \htmlinclude ddt2_t.html !! type, public :: ddt2_t - integer, private :: num_vars = 0 + integer, public :: num_vars = 0 real(kind_phys), allocatable :: vars(:,:,:) contains procedure :: get_num_vars diff --git a/test/unit_tests/sample_host_files/ddt_data1_mod.F90 b/test/unit_tests/sample_host_files/ddt_data1_mod.F90 index 0607f11c..5efe0845 100644 --- a/test/unit_tests/sample_host_files/ddt_data1_mod.F90 +++ b/test/unit_tests/sample_host_files/ddt_data1_mod.F90 @@ -16,7 +16,7 @@ module ddt_data1_mod !! \htmlinclude ddt2_t.html !! type, public :: ddt2_t - integer, private :: num_vars = 0 + integer, public :: num_vars = 0 real(kind_phys), allocatable :: vars(:,:,:) end type ddt2_t diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 6651c2de..595962c0 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -340,9 +340,6 @@ def test_scheme_ddt_only(self): # Exercise scheme_headers, table_dict = parse_scheme_files(scheme_files, self._run_env_ccpp) -# with self.assertRaises(CCPPError) as context: -# _, _ = parse_scheme_files(scheme_files, self._run_env_ccpp) -# # Verify correct error messages returned if __name__ == "__main__": unittest.main() diff --git a/test/var_action_test/test_reports.py b/test/var_action_test/test_reports.py index 3d9a8637..fff8603d 100755 --- a/test/var_action_test/test_reports.py +++ b/test/var_action_test/test_reports.py @@ -23,8 +23,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR) From ac18c448999dbbf2d2fa0f4ac32c0356a5369ea3 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Tue, 31 Oct 2023 22:33:02 -0600 Subject: [PATCH 36/49] fix tests --- scripts/constituents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/constituents.py b/scripts/constituents.py index 119c0275..6b0ac759 100644 --- a/scripts/constituents.py +++ b/scripts/constituents.py @@ -315,7 +315,7 @@ def write_constituent_routines(self, outfile, indent, suite_name, err_vars): f'advected={advect_str}', f'errcode={errvar_names["ccpp_error_code"]}', f'errmsg={errvar_names["ccpp_error_message"]}'] - if default_value is not None: + if default_value is not None and default_value != '': init_args.append(f'default_value={default_value}') stmt = 'call {}(index)%instantiate({})' outfile.write(f'if ({errvar_names["ccpp_error_code"]} == 0) then', indent+1) From 9db947704b718440da68cd284b4781526a6ecb6f Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Wed, 1 Nov 2023 11:51:51 -0600 Subject: [PATCH 37/49] address reviewer comments --- scripts/ddt_library.py | 2 +- scripts/fortran_tools/parse_fortran_file.py | 3 +- scripts/metavar.py | 4 + src/ccpp_constituent_prop_mod.F90 | 283 +++++++++----------- 4 files changed, 137 insertions(+), 155 deletions(-) diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 18bc397a..9644787b 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -283,7 +283,7 @@ def collect_ddt_fields(self, var_dict, var, run_env, ntx = context_string(dvar.context) ctx = context_string(pvar.context) emsg = f"Attempt to add duplicate DDT sub-variable, {stdname}{ntx}." - emsg += "\nVariable originally defined{ctx}" + emsg += f"\nVariable originally defined{ctx}" raise CCPPError(emsg.format(stdname, ntx, ctx)) # end if # Add this intrinsic to diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index d6ec1f74..80b1c486 100644 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -582,8 +582,7 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): raise CCPPError(msg.format(statement, ctx)) # End if mheaders.append(ddt) - if (run_env.logger and - run_env.debug_on()): + if run_env.debug_on(): ctx = context_string(pobj, nodir=True) msg = 'Adding DDT {}{}' run_env.logger.debug(msg.format(ddt.table_name, ctx)) diff --git a/scripts/metavar.py b/scripts/metavar.py index 02a02c2d..c7be0f5c 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -783,6 +783,7 @@ def intrinsic_elements(self, check_dict=None, ddt_lib=None): Currently, an array of DDTs is not processed (return None) since Fortran does not support a way to reference those elements. """ + element_names = None if self.is_ddt(): dtitle = self.get_prop_value('type') if ddt_lib and (dtitle in ddt_lib): @@ -798,6 +799,9 @@ def intrinsic_elements(self, check_dict=None, ddt_lib=None): if not element_names: element_names = None # end if + else: + errmsg = f'No ddt_lib or ddt {dtitle} not in ddt_lib' + raise CCPPError(errmsg) # end if # end if children = self.children() diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index 7f0637e9..41d8213f 100644 --- a/src/ccpp_constituent_prop_mod.F90 +++ b/src/ccpp_constituent_prop_mod.F90 @@ -243,14 +243,16 @@ end subroutine initialize_errvars !####################################################################### - subroutine append_errvars(errcode_val, errmsg_val, errcode, errmsg) + subroutine append_errvars(errcode_val, errmsg_val, subname, errcode, errmsg, caller) ! Append to error variables, if present ! Dummy arguments integer, intent(in) :: errcode_val character(len=*), intent(in) :: errmsg_val + character(len=*), intent(in) :: subname integer, optional, intent(inout) :: errcode character(len=*), optional, intent(inout) :: errmsg + character(len=*), optional, intent(in) :: caller ! Local variable integer :: emsg_len @@ -263,45 +265,51 @@ subroutine append_errvars(errcode_val, errmsg_val, errcode, errmsg) errmsg(emsg_len+1:) = '; ' end if emsg_len = len_trim(errmsg) - errmsg(emsg_len+1:) = trim(errmsg_val) + if (present(caller)) then + errmsg(emsg_len+1:) = trim(caller)//" "//trim(errmsg_val) + else + errmsg(emsg_len+1:) = trim(subname)//" "//trim(errmsg_val) + end if end if end subroutine append_errvars !####################################################################### - subroutine handle_allocate_error(astat, fieldname, errcode, errmsg) + subroutine handle_allocate_error(astat, fieldname, subname, errcode, errmsg) ! Generate an error message if indicates an allocation failure ! Dummy arguments integer, intent(in) :: astat character(len=*), intent(in) :: fieldname + character(len=*), intent(in) :: subname integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg call initialize_errvars(errcode, errmsg) if (astat /= 0) then call append_errvars(astat, "Error allocating ccpp_constituent_properties_t object component " // & - trim(fieldname) // ", error code = " // to_str(astat), errcode=errcode, errmsg=errmsg) + trim(fieldname) // ", error code = " // to_str(astat), subname, errcode=errcode, errmsg=errmsg) end if end subroutine handle_allocate_error !####################################################################### - subroutine check_var_bounds(var, var_bound, varname, errcode, errmsg) + subroutine check_var_bounds(var, var_bound, varname, subname, errcode, errmsg) ! Generate an error message if indicates an allocation failure ! Dummy arguments integer, intent(in) :: var integer, intent(in) :: var_bound character(len=*), intent(in) :: varname + character(len=*), intent(in) :: subname integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg call initialize_errvars(errcode, errmsg) if (var > var_bound) then call append_errvars(1, trim(varname)//" exceeds its upper bound, " // & - to_str(var_bound), errcode=errcode, errmsg=errmsg) + to_str(var_bound), subname, errcode=errcode, errmsg=errmsg) end if end subroutine check_var_bounds @@ -331,12 +339,13 @@ logical function ccp_is_instantiated(this, errcode, errmsg) class(ccpp_constituent_properties_t), intent(in) :: this integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg + character(len=*), parameter :: subname = 'ccp_is_instantiated' ccp_is_instantiated = allocated(this%var_std_name) call initialize_errvars(errcode, errmsg) if (.not. ccp_is_instantiated) then call append_errvars(1, "ccpp_constituent_properties_t object is not initialized", & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) end if end function ccp_is_instantiated @@ -559,13 +568,14 @@ subroutine ccp_set_const_index(this, index, errcode, errmsg) integer, intent(in) :: index integer, optional, intent(out) :: errcode character(len=*), optional, intent(out) :: errmsg + character(len=*), parameter :: subname = 'ccp_set_const_index' if (this%is_instantiated(errcode, errmsg)) then if (this%const_ind == int_unassigned) then this%const_ind = index else call append_errvars(1, "ccpp_constituent_properties_t const index " // & - "is already set", errcode=errcode, errmsg=errmsg) + "is already set", subname, errcode=errcode, errmsg=errmsg) end if end if @@ -854,7 +864,7 @@ logical function ccp_model_const_locked(this, errcode, errmsg, warn_func) ! Use an initialized hash table as double check if (this%hash_table%is_initialized()) then ccp_model_const_locked = this%table_locked .and. this%data_locked - if ( (.not. (this%table_locked .and. this%data_locked)) .and. & + if ( (.not. (this%table_locked .and. this%data_locked)) .and. & present(errmsg) .and. present(warn_func)) then ! Write a warning as a courtesy to calling function but do not set ! errcode (let caller decide). @@ -862,15 +872,8 @@ logical function ccp_model_const_locked(this, errcode, errmsg, warn_func) ' WARNING: Model constituents not ready to use' end if else - if (present(warn_func)) then - call append_errvars(1, trim(warn_func) // & - " WARNING: Model constituents not initialized", & - errcode=errcode, errmsg=errmsg) - else - call append_errvars(1, subname // & - " WARNING: Model constituents not initialized", & - errcode=errcode, errmsg=errmsg) - end if + call append_errvars(1, "WARNING: Model constituents not initialized", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) end if end function ccp_model_const_locked @@ -901,15 +904,9 @@ logical function ccp_model_const_props_locked(this, errcode, errmsg, warn_func) ' WARNING: Model constituent properties not ready to use' end if else - if (present(warn_func)) then - call append_errvars(1, trim(warn_func) // & - " WARNING: Model constituent properties not initialized", & - errcode=errcode, errmsg=errmsg) - else - call append_errvars(1, subname // & - " WARNING: Model constituent properties not initialized", & - errcode=errcode, errmsg=errmsg) - end if + call append_errvars(1, & + "WARNING: Model constituent properties not initialized", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) end if end function ccp_model_const_props_locked @@ -936,19 +933,13 @@ logical function ccp_model_const_data_locked(this, errcode, errmsg, warn_func) present(errmsg) .and. present(warn_func)) then ! Write a warning as a courtesy to calling function but do not set ! errcode (let caller decide). - write(errmsg, *) trim(warn_func), & + write(errmsg, *) trim(warn_func), & ' WARNING: Model constituent data not ready to use' end if else - if (present(warn_func)) then - call append_errvars(1, trim(warn_func) // & - " WARNING: Model constituent data not initialized", & - errcode=errcode, errmsg=errmsg) - else - call append_errvars(1, subname // & - " WARNING: Model constituent data not initialized", & - errcode=errcode, errmsg=errmsg) - end if + call append_errvars(1, & + "WARNING: Model constituent data not initialized", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) end if end function ccp_model_const_data_locked @@ -975,26 +966,14 @@ logical function ccp_model_const_okay_to_add(this, errcode, errmsg, & errmsg=errmsg, warn_func=subname) .or. this%const_data_locked(errcode=errcode, & errmsg=errmsg, warn_func=subname)) if (.not. ccp_model_const_okay_to_add) then - if (present(warn_func)) then - call append_errvars(1, trim(warn_func) // & - " WARNING: Model constituents are locked", & - errcode=errcode, errmsg=errmsg) - else - call append_errvars(1, subname // & - " WARNING: Model constituents are locked", & - errcode=errcode, errmsg=errmsg) - end if + call append_errvars(1, & + "WARNING: Model constituents are locked", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) end if else - if (present(warn_func)) then - call append_errvars(1, trim(warn_func) // & - " WARNING: Model constituents not initialized", & - errcode=errcode, errmsg=errmsg) - else - call append_errvars(1, subname // & - " WARNING: Model constituents not initialized", & - errcode=errcode, errmsg=errmsg) - end if + call append_errvars(1, & + "WARNING: Model constituents not initialized", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) end if end function ccp_model_const_okay_to_add @@ -1019,7 +998,7 @@ subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) !!XXgoldyXX: Add check on key to see if incompatible item already there. call this%hash_table%add_hash_key(field_data, error) if (len_trim(error) > 0) then - call append_errvars(1, trim(error), errcode=errcode, errmsg=errmsg) + call append_errvars(1, trim(error), subname, errcode=errcode, errmsg=errmsg) else ! If we get here we are successful, add to variable count if (field_data%is_layer_var()) then @@ -1031,16 +1010,15 @@ subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) if (errcode /= 0) then call append_errvars(1, & "ERROR: Unknown vertical dimension, '" // & - trim(error) // "'", & + trim(error) // "'", subname, & errcode=errcode, errmsg=errmsg) end if end if end if end if else - call append_errvars(1, subname // & - "WARNING: Model constituents are locked", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, "WARNING: Model constituents are locked", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccp_model_const_add_metadata @@ -1091,15 +1069,15 @@ function ccp_model_const_find_const(this, standard_name, errcode, errmsg) & nullify(cprop) hval => this%hash_table%table_value(standard_name, errmsg=error) if (len_trim(error) > 0) then - call append_errvars(1, subname // ": "//trim(error), errcode=errcode, & - errmsg=errmsg) + call append_errvars(1, trim(error), subname, & + errcode=errcode, errmsg=errmsg) else select type(hval) type is (ccpp_constituent_properties_t) cprop => hval class default - call append_errvars(1, subname // " ERROR: Bad hash table value " // & - trim(standard_name), errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ERROR: Bad hash table value " // & + trim(standard_name), subname, errcode=errcode, errmsg=errmsg) end select end if @@ -1130,9 +1108,9 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) astat = 0 errcode_local = 0 if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - call append_errvars(1, subname // & - " WARNING: Model constituents properties already locked, ignoring", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, & + "WARNING: Model constituents properties already locked, ignoring", & + subname, errcode=errcode, errmsg=errmsg) errcode_local = 1 else ! Make sure everything is really initialized @@ -1142,7 +1120,7 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) num_vars = this%hash_table%num_values() allocate(this%const_metadata(num_vars), stat=astat) call handle_allocate_error(astat, 'const_metadata', & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) ! We want to pack the advected constituents at the beginning of ! the field array so we need to know how many there are if (astat == 0) then @@ -1165,11 +1143,10 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) end do ! Sanity check on num_advect if (this%num_advected_vars > num_vars) then - call append_errvars(1, subname // & - " ERROR: num_advected_vars index " // & + call append_errvars(1, "ERROR: num_advected_vars index " // & to_str(this%num_advected_vars) // & " out of bounds " // to_str(num_vars), & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) errcode_local = 1 end if end if @@ -1188,10 +1165,10 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) if (check) then index_advect = index_advect + 1 if (index_advect > this%num_advected_vars) then - call append_errvars(1, subname // " ERROR: const a index " // & - to_str(index_advect) // " out of bounds " // & - to_str(this%num_advected_vars), & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ERROR: const a index " // & + to_str(index_advect) // " out of bounds " // & + to_str(this%num_advected_vars), & + subname, errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 exit end if @@ -1201,9 +1178,10 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) else index_const = index_const + 1 if (index_const > num_vars) then - call append_errvars(1, subname // " ERROR: const v index " // & - to_str(index_const) // " out of bounds " // to_str(num_vars), & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ERROR: const v index " // & + to_str(index_const) // " out of bounds " // & + to_str(num_vars), subname, errcode=errcode, & + errmsg=errmsg) errcode_local = errcode_local + 1 exit end if @@ -1215,14 +1193,14 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) if (.not. cprop%is_layer_var()) then call cprop%vertical_dimension(dimname, & errcode=errcode, errmsg=errmsg) - call append_errvars(1, subname // " ERROR: Bad vertical dimension, '" // & - trim(dimname), errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ERROR: Bad vertical dimension, '" // & + trim(dimname), subname, errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 exit end if class default - call append_errvars(1, subname // " ERROR: Bad hash table value", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ERROR: Bad hash table value", & + subname, errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 exit end select @@ -1233,16 +1211,16 @@ subroutine ccp_model_const_table_lock(this, errcode, errmsg) end do ! Some size sanity checks if (index_const /= this%hash_table%num_values()) then - call append_errvars(1, subname // & - " ERROR: Too few constituents " // to_str(index_const) // & - " found in hash table " // to_str(this%hash_table%num_values()), & + call append_errvars(1, "ERROR: Too few constituents "// & + to_str(index_const) // " found in hash table " // & + to_str(this%hash_table%num_values()), subname, & errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 end if if (index_advect /= this%num_advected_vars) then - call append_errvars(1, subname // & - " ERROR: Too few advected constituents " // to_str(index_const) // & - " found in hash table " // to_str(this%hash_table%num_values()), & + call append_errvars(1, "ERROR: Too few advected constituents " // & + to_str(index_const) // " found in hash table " // & + to_str(this%hash_table%num_values()), subname, & errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 end if @@ -1277,26 +1255,26 @@ subroutine ccp_model_const_data_lock(this, ncols, num_layers, errcode, errmsg) errcode_local = 0 if (this%const_data_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then - call append_errvars(1, subname // & - " WARNING: Model constituent data already locked, ignoring", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, & + "WARNING: Model constituent data already locked, ignoring", & + subname, errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 else if (.not. this%const_props_locked(errcode=errcode, errmsg=errmsg, & warn_func=subname)) then - call append_errvars(1, subname // & - " WARNING: Model constituent properties not yet locked, ignoring", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, & + "WARNING: Model constituent properties not yet locked, ignoring", & + subname, errcode=errcode, errmsg=errmsg) errcode_local = errcode_local + 1 else allocate(this%vars_layer(ncols, num_layers, this%hash_table%num_values()), & stat=astat) call handle_allocate_error(astat, 'vars_layer', & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) errcode_local = astat if (astat == 0) then allocate(this%vars_minvalue(this%hash_table%num_values()), stat=astat) call handle_allocate_error(astat, 'vars_minvalue', & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) errcode_local = astat end if if (errcode_local == 0) then @@ -1519,39 +1497,39 @@ subroutine ccp_model_const_copy_in_3d(this, const_array, advected, & ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then - call append_errvars(1, subname // & + call append_errvars(1, & ": Too many constituents for ", & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if ! Copy this constituent's field data to call this%const_metadata(index)%const_index(fld_ind) if (fld_ind /= index) then call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname//": ERROR: "// & + call append_errvars(1, ": ERROR: "// & "bad field index, "//to_str(fld_ind)// & " for '"//trim(std_name)//"', should have been "// & - to_str(index), errcode=errcode, errmsg=errmsg) + to_str(index), subname, errcode=errcode, errmsg=errmsg) exit else if (this%const_metadata(index)%is_layer_var()) then if (this%num_layers == num_levels) then const_array(:,:,cindex) = this%vars_layer(:,:,fld_ind) else call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname//": ERROR: "// & + call append_errvars(1, ": ERROR: "// & "Wrong number of vertical levels for '"// & trim(std_name)//"', "//to_str(num_levels)// & ", expected "//to_str(this%num_layers), & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if else call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname//": Unsupported var type,"// & + call append_errvars(1, ": Unsupported var type,"// & " wrong number of vertical levels for '"// & trim(std_name)//"', "//to_str(num_levels)// & ", expected"//to_str(this%num_layers), & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if end if @@ -1595,39 +1573,39 @@ subroutine ccp_model_const_copy_out_3d(this, const_array, advected, & ! See if we have room for another constituent cindex = cindex + 1 if (cindex > max_cind) then - call append_errvars(1, subname// & + call append_errvars(1, & ": Too many constituents for ", & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if ! Copy this field of to to constituent's field data call this%const_metadata(index)%const_index(fld_ind) if (fld_ind /= index) then call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname//": ERROR: "// & + call append_errvars(1, ": ERROR: "// & "bad field index, "//to_str(fld_ind)// & " for '"//trim(std_name)//"', should have been"// & - to_str(index), errcode=errcode, errmsg=errmsg) + to_str(index), subname, errcode=errcode, errmsg=errmsg) exit else if (this%const_metadata(index)%is_layer_var()) then if (this%num_layers == num_levels) then this%vars_layer(:,:,fld_ind) = const_array(:,:,cindex) else call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname// & + call append_errvars(1, & ": Wrong number of vertical levels for '"// & trim(std_name)//"', "//to_str(num_levels)// & ", expected"//to_str(this%num_layers), & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if else call this%const_metadata(index)%standard_name(std_name) - call append_errvars(1, subname//": Unsupported var type,"// & + call append_errvars(1, ": Unsupported var type,"// & " wrong number of vertical levels for'"// & trim(std_name)//"', "//to_str(num_levels)// & ", expected "//to_str(this%num_layers), & - errcode=errcode, errmsg=errmsg) + subname, errcode=errcode, errmsg=errmsg) exit end if end if @@ -1726,8 +1704,8 @@ subroutine ccpt_get_standard_name(this, std_name, errcode, errmsg) call this%prop%standard_name(std_name, errcode, errmsg) else std_name = '' - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_get_standard_name @@ -1749,8 +1727,8 @@ subroutine ccpt_get_long_name(this, long_name, errcode, errmsg) call this%prop%long_name(long_name, errcode, errmsg) else long_name = '' - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_get_long_name @@ -1774,8 +1752,8 @@ subroutine ccpt_get_vertical_dimension(this, vert_dim, errcode, errmsg) end if else vert_dim = '' - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_get_vertical_dimension @@ -1857,8 +1835,8 @@ subroutine ccpt_const_index(this, index, errcode, errmsg) index = this%prop%const_index(errcode, errmsg) else index = int_unassigned - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_const_index @@ -1879,8 +1857,8 @@ subroutine ccpt_is_thermo_active(this, val_out, errcode, errmsg) call this%prop%is_thermo_active(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_thermo_active @@ -1901,8 +1879,8 @@ subroutine ccpt_is_advected(this, val_out, errcode, errmsg) call this%prop%is_advected(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_advected @@ -1923,8 +1901,8 @@ subroutine ccpt_is_mass_mixing_ratio(this, val_out, errcode, errmsg) call this%prop%is_mass_mixing_ratio(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_mass_mixing_ratio @@ -1945,8 +1923,8 @@ subroutine ccpt_is_volume_mixing_ratio(this, val_out, errcode, errmsg) call this%prop%is_volume_mixing_ratio(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_volume_mixing_ratio @@ -1967,8 +1945,8 @@ subroutine ccpt_is_number_concentration(this, val_out, errcode, errmsg) call this%prop%is_number_concentration(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_number_concentration @@ -1989,8 +1967,8 @@ subroutine ccpt_is_dry(this, val_out, errcode, errmsg) call this%prop%is_dry(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_dry @@ -2011,8 +1989,8 @@ subroutine ccpt_is_moist(this, val_out, errcode, errmsg) call this%prop%is_moist(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_moist @@ -2033,8 +2011,8 @@ subroutine ccpt_is_wet(this, val_out, errcode, errmsg) call this%prop%is_wet(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_is_wet @@ -2055,8 +2033,8 @@ subroutine ccpt_min_val(this, val_out, errcode, errmsg) call this%prop%minimum(val_out, errcode, errmsg) else val_out = kphys_unassigned - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_min_val @@ -2077,8 +2055,8 @@ subroutine ccpt_molec_weight(this, val_out, errcode, errmsg) call this%prop%molec_weight(val_out, errcode, errmsg) else val_out = kphys_unassigned - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_molec_weight @@ -2099,8 +2077,8 @@ subroutine ccpt_default_value(this, val_out, errcode, errmsg) call this%prop%default_value(val_out, errcode, errmsg) else val_out = kphys_unassigned - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_default_value @@ -2121,8 +2099,8 @@ subroutine ccpt_has_default(this, val_out, errcode, errmsg) call this%prop%has_default(val_out, errcode, errmsg) else val_out = .false. - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_has_default @@ -2141,6 +2119,7 @@ subroutine ccpt_set(this, const_ptr, errcode, errmsg) ! Local variables character(len=stdname_len) :: stdname character(len=errmsg_len) :: errmsg2 + character(len=*), parameter :: subname = 'ccpt_set' call initialize_errvars(errcode, errmsg) if (associated(this%prop)) then @@ -2150,7 +2129,7 @@ subroutine ccpt_set(this, const_ptr, errcode, errmsg) trim(stdname), "'" end if errcode = errcode + 1 - call append_errvars(1, "ccpt_set: "//trim(errmsg2), errcode=errcode, & + call append_errvars(1, trim(errmsg2), subname, errcode=errcode, & errmsg=errmsg) else this%prop => const_ptr @@ -2193,14 +2172,14 @@ subroutine ccpt_set_const_index(this, index, errcode, errmsg) if (this%prop%const_ind == int_unassigned) then this%prop%const_ind = index else - call append_errvars(1, "ccpp_constituent_prop_ptr_t "// & - "const index is already set", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, "ccpp_constituent_prop_ptr_t "// & + "const index is already set", & + subname, errcode=errcode, errmsg=errmsg) end if end if else - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_set_const_index @@ -2225,8 +2204,8 @@ subroutine ccpt_set_thermo_active(this, thermo_flag, errcode, errmsg) this%prop%thermo_active = thermo_flag end if else - call append_errvars(1, subname//": invalid constituent pointer", & - errcode=errcode, errmsg=errmsg) + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if end subroutine ccpt_set_thermo_active From 4c1807cdcccce884c0df3c88e463e7157173cb17 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Tue, 28 Nov 2023 20:44:58 -0700 Subject: [PATCH 38/49] change debug_on subroutine in framework_env to verbose property --- scripts/ccpp_capgen.py | 4 ++-- scripts/ccpp_suite.py | 4 ++-- scripts/ddt_library.py | 4 ++-- scripts/fortran_tools/parse_fortran_file.py | 16 ++++++++-------- scripts/framework_env.py | 7 ++++--- scripts/host_cap.py | 2 +- scripts/metadata_table.py | 4 ++-- scripts/parse_tools/__init__.py | 2 +- scripts/parse_tools/parse_log.py | 2 +- scripts/suite_objects.py | 2 +- 10 files changed, 24 insertions(+), 23 deletions(-) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 2c50eed1..cbd46599 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -619,14 +619,14 @@ def capgen(run_env, return_db=False): # end if # Next, parse the scheme files scheme_headers, scheme_tdict = parse_scheme_files(scheme_files, run_env) - if run_env.debug_on(): + if run_env.verbose: ddts = host_model.ddt_lib.keys() if ddts: run_env.logger.debug("DDT definitions = {}".format(ddts)) # end if # end if plist = host_model.prop_list('local_name') - if run_env.debug_on(): + if run_env.verbose: run_env.logger.debug("{} variables = {}".format(host_model.name, plist)) run_env.logger.debug("schemes = {}".format([x.title for x in scheme_headers])) diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 488d4a27..c5c6f4ab 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -427,7 +427,7 @@ def analyze(self, host_model, scheme_library, ddt_library, run_env): phase = RUN_PHASE_NAME # end if lmsg = "Group {}, schemes = {}" - if run_env.debug_on(): + if run_env.verbose: run_env.logger.debug(lmsg.format(item.name, [x.name for x in item.schemes()])) @@ -490,7 +490,7 @@ def write(self, output_dir, run_env): (calling the group caps one after another)""" # Set name of module and filename of cap filename = '{module_name}.F90'.format(module_name=self.module) - if run_env.debug_on(): + if run_env.verbose: run_env.logger.debug('Writing CCPP suite file, {}'.format(filename)) # end if # Retrieve the name of the constituent module for Group use statements diff --git a/scripts/ddt_library.py b/scripts/ddt_library.py index 9644787b..72fe48b8 100644 --- a/scripts/ddt_library.py +++ b/scripts/ddt_library.py @@ -46,7 +46,7 @@ def __init__(self, new_field, var_ref, run_env, recur=False): self.__field = VarDDT(new_field, var_ref.field, run_env, recur=True) # end if if ((not recur) and - run_env.debug_on()): + run_env.verbose): run_env.logger.debug('Adding DDT field, {}'.format(self)) # end if @@ -218,7 +218,7 @@ def __init__(self, name, run_env, ddts=None): octx = context_string(self[ddt.title].source.context) raise CCPPError(errmsg.format(ddt.title, ctx, octx)) # end if - if run_env.debug_on(): + if run_env.verbose: lmsg = f"Adding DDT {ddt.title} to {self.name}" run_env.logger.debug(lmsg) # end if diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index 80b1c486..a67816e4 100644 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,7 +563,7 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): module=spec_name, var_dict=var_dict) mheaders.append(mheader) - if run_env.debug_on(): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = 'Adding header {}{}' run_env.logger.debug(msg.format(mheader.table_name, ctx)) @@ -580,13 +580,13 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, run_env): ctx = context_string(pobj, nodir=True) msg = "No DDT found at '{}'{}" raise CCPPError(msg.format(statement, ctx)) - # End if + # end if mheaders.append(ddt) - if run_env.debug_on(): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = 'Adding DDT {}{}' run_env.logger.debug(msg.format(ddt.table_name, ctx)) - # End if + # end if active_table = None else: # We found a type definition but it is not one with @@ -624,7 +624,7 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): inpreamble = False insub = True seen_contains = False - if run_env.debug_on(): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = "Parsing specification of {}{}" run_env.logger.debug(msg.format(table_name, ctx)) @@ -811,7 +811,7 @@ def parse_specification(pobj, statements, run_env, mod_name=None, errmsg = duplicate_header(mtables[title], tbl) raise CCPPError(errmsg) # end if - if run_env.debug_on(): + if run_env.verbose: ctx = tbl.start_context() mtype = tbl.table_type msg = "Adding metadata from {}, {}{}" @@ -892,7 +892,7 @@ def parse_module(pobj, statements, run_env): # End if mod_name = pmatch.group(1) pobj.enter_region('MODULE', region_name=mod_name, nested_ok=False) - if run_env.debug_on(): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = "Parsing Fortran module, {}{}" run_env.logger.debug(msg.format(mod_name, ctx)) @@ -928,7 +928,7 @@ def parse_module(pobj, statements, run_env): errmsg = duplicate_header(mtables[title], mheader) raise CCPPError(errmsg) # end if - if run_env.debug_on(): + if run_env.verbose: mtype = mheader.table_type ctx = mheader.start_context() msg = "Adding metadata from {}, {}{}" diff --git a/scripts/framework_env.py b/scripts/framework_env.py index af941bd4..c0bb4139 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -10,7 +10,7 @@ # Python library imports import argparse import os -from parse_tools import debug_enabled +from parse_tools import verbose _EPILOG = ''' ''' @@ -276,10 +276,11 @@ def kind_types(self): CCPPFrameworkEnv object.""" return self.__kind_dict.keys() - def debug_on(self): + @property + def verbose(self): """Return true if debug enabled for the CCPPFrameworkEnv's logger object.""" - return (self.logger and debug_enabled(self.logger)) + return (self.logger and verbose(self.logger)) @property def use_error_obj(self): diff --git a/scripts/host_cap.py b/scripts/host_cap.py index 268a1855..d2c4ed7e 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -239,7 +239,7 @@ def add_constituent_vars(cap, host_model, suite_list, run_env): # Add entries for each constituent (once per standard name) const_stdnames = set() for suite in suite_list: - if run_env.debug_on(): + if run_env.verbose: lmsg = "Adding constituents from {} to {}" run_env.logger.debug(lmsg.format(suite.name, host_model.name)) # end if diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 0afe30d3..946e9782 100755 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -792,7 +792,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): self.__pobj.add_syntax_err(mismatch) self.__section_valid = False # end if - if run_env.debug_on(): + if run_env.verbose: run_env.logger.info("Parsing {} {}{}".format(self.header_type, self.title, start_ctx)) # end if @@ -812,7 +812,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): newvar, curr_line = self.parse_variable(curr_line, known_ddts) valid_lines = newvar is not None if valid_lines: - if run_env.debug_on(): + if run_env.verbose: dmsg = 'Adding {} to {}' lname = newvar.get_prop_value('local_name') run_env.logger.debug(dmsg.format(lname, self.title)) diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 736910f0..4990da88 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -25,7 +25,7 @@ from parse_checkers import check_default_value, check_valid_values, check_molar_mass from parse_log import init_log, set_log_level, flush_log from parse_log import set_log_to_stdout, set_log_to_null -from parse_log import set_log_to_file, debug_enabled +from parse_log import set_log_to_file, verbose from preprocess import PreprocStack from xml_tools import find_schema_file, find_schema_version from xml_tools import read_xml_file, validate_xml_file diff --git a/scripts/parse_tools/parse_log.py b/scripts/parse_tools/parse_log.py index 9707042a..f85a5d09 100644 --- a/scripts/parse_tools/parse_log.py +++ b/scripts/parse_tools/parse_log.py @@ -48,6 +48,6 @@ def flush_log(logger): for handler in list(logger.handlers): handler.flush() -def debug_enabled(logger): +def verbose(logger): """Return true if debug is enabled for this logger""" return logger.isEnabledFor(logging.DEBUG) diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 0be6de15..c52c7c4d 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1287,7 +1287,7 @@ def __init__(self, index_name, context, parent, run_env, items=None): # self._local_dim_name is the variable name for self._dim_name self._local_dim_name = None super().__init__(index_name, context, parent, run_env) - if run_env.debug_on(): + if run_env.verbose: lmsg = "Adding VerticalLoop for '{}'" run_env.logger.debug(lmsg.format(index_name)) # end if From 800ea0786a0b0196266c71bbd7d9edf8b6c64926 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Tue, 16 Jan 2024 08:49:29 -0700 Subject: [PATCH 39/49] Add capability to do unit conversations to capgen (#504) This PR adds automatic conversions for supported unit and type transforms. Also, included is a new ccpp variable property to indicate a scheme variables vertical orientation vertical, top_at_one. As default, top_at_one is set to .false.(GFS ordering convention). Adding top_at_one = .true. to a variable in a schemes metadata file will trigger automatic array flipping: Addresses #329 and #403 Conversions supported: https://github.com/NCAR/ccpp-framework/blob/main/scripts/conversion_tools/unit_conversion.py --------- Co-authored-by: dustinswales Co-authored-by: Grant Firl Co-authored-by: Dom Heinzeller --- .github/workflows/prebuild.yaml | 47 ++++++ CMakeLists.txt | 2 +- scripts/ccpp_datafile.py | 2 +- scripts/metavar.py | 8 +- scripts/suite_objects.py | 117 ++++++++++++-- scripts/var_props.py | 116 +++++++------- test/run_fortran_tests.sh | 16 +- test/unit_tests/test_var_transforms.py | 55 ++----- .../.gitignore | 0 .../CMakeLists.txt | 4 +- .../README.md | 4 +- .../effr_calc.F90 | 2 +- .../effr_calc.meta | 4 +- .../run_test | 50 +++--- .../test_host.F90 | 4 +- .../test_host.meta | 0 .../test_host_data.F90 | 0 .../test_host_data.meta | 0 .../test_host_mod.F90 | 2 +- .../test_host_mod.meta | 0 .../test_reports.py | 14 +- .../var_compatability_files.txt} | 0 .../var_compatability_suite.xml} | 2 +- .../test_blocked_data/CMakeLists.txt | 90 +++++++++++ test_prebuild/test_blocked_data/README.md | 13 ++ .../test_blocked_data/blocked_data_scheme.F90 | 118 ++++++++++++++ .../blocked_data_scheme.meta | 147 ++++++++++++++++++ .../test_blocked_data/ccpp_prebuild_config.py | 81 ++++++++++ test_prebuild/test_blocked_data/data.F90 | 41 +++++ test_prebuild/test_blocked_data/data.meta | 69 ++++++++ test_prebuild/test_blocked_data/main.F90 | 107 +++++++++++++ .../suite_blocked_data_suite.xml | 9 ++ .../test_metadata_parser.py | 0 {tests => test_prebuild}/test_mkstatic.py | 0 34 files changed, 956 insertions(+), 168 deletions(-) create mode 100644 .github/workflows/prebuild.yaml mode change 100644 => 100755 test/unit_tests/test_var_transforms.py rename test/{var_action_test => var_compatability_test}/.gitignore (100%) rename test/{var_action_test => var_compatability_test}/CMakeLists.txt (98%) rename test/{var_action_test => var_compatability_test}/README.md (58%) rename test/{var_action_test => var_compatability_test}/effr_calc.F90 (95%) rename test/{var_action_test => var_compatability_test}/effr_calc.meta (97%) rename test/{var_action_test => var_compatability_test}/run_test (72%) rename test/{var_action_test => var_compatability_test}/test_host.F90 (99%) rename test/{var_action_test => var_compatability_test}/test_host.meta (100%) rename test/{var_action_test => var_compatability_test}/test_host_data.F90 (100%) rename test/{var_action_test => var_compatability_test}/test_host_data.meta (100%) rename test/{var_action_test => var_compatability_test}/test_host_mod.F90 (97%) rename test/{var_action_test => var_compatability_test}/test_host_mod.meta (100%) rename test/{var_action_test => var_compatability_test}/test_reports.py (94%) rename test/{var_action_test/var_action_files.txt => var_compatability_test/var_compatability_files.txt} (100%) rename test/{var_action_test/var_action_suite.xml => var_compatability_test/var_compatability_suite.xml} (69%) create mode 100644 test_prebuild/test_blocked_data/CMakeLists.txt create mode 100644 test_prebuild/test_blocked_data/README.md create mode 100644 test_prebuild/test_blocked_data/blocked_data_scheme.F90 create mode 100644 test_prebuild/test_blocked_data/blocked_data_scheme.meta create mode 100644 test_prebuild/test_blocked_data/ccpp_prebuild_config.py create mode 100644 test_prebuild/test_blocked_data/data.F90 create mode 100644 test_prebuild/test_blocked_data/data.meta create mode 100644 test_prebuild/test_blocked_data/main.F90 create mode 100644 test_prebuild/test_blocked_data/suite_blocked_data_suite.xml rename {tests => test_prebuild}/test_metadata_parser.py (100%) rename {tests => test_prebuild}/test_mkstatic.py (100%) diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml new file mode 100644 index 00000000..80e34608 --- /dev/null +++ b/.github/workflows/prebuild.yaml @@ -0,0 +1,47 @@ +name: ccpp-prebuild + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + unit-tests: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: ccpp-prebuild unit tests + run: | + export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools + cd test_prebuild + python3 test_metadata_parser.py + python3 test_mkstatic.py + - name: ccpp-prebuild blocked data tests + run: | + cd test_prebuild/test_blocked_data + python3 ../../scripts/ccpp_prebuild.py --config=ccpp_prebuild_config.py --builddir=build + cd build + cmake .. + make + ./test_blocked_data.x diff --git a/CMakeLists.txt b/CMakeLists.txt index 4daf855b..41e163ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ project(ccpp_framework #------------------------------------------------------------------------------ # Set package definitions set(PACKAGE "ccpp-framework") -set(AUTHORS "Dom Heinzeller" "Grant Firl" "Mike Kavulich" "Steve Goldhaber") +set(AUTHORS "Dom Heinzeller" "Grant Firl" "Mike Kavulich" "Dustin Swales" "Courtney Peverley") string(TIMESTAMP YEAR "%Y") #------------------------------------------------------------------------------ diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 7a4c3d8d..1fd5f830 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -654,7 +654,7 @@ def _new_var_entry(parent, var, full_entry=True): "diagnostic_name", "diagnostic_name_fixed", "kind", "persistence", "polymorphic", "protected", "state_variable", "type", "units", "molar_mass", - "advected"]) + "advected", "top_at_one"]) prop_list.extend(Var.constituent_property_names()) # end if ventry = ET.SubElement(parent, "var") diff --git a/scripts/metavar.py b/scripts/metavar.py index c7be0f5c..0c5a87d2 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -204,6 +204,8 @@ class Var: default_in='.true.'), VariableProperty('polymorphic', bool, optional_in=True, default_in=False), + VariableProperty('top_at_one', bool, optional_in=True, + default_in=False), VariableProperty('target', bool, optional_in=True, default_in=False)] @@ -376,15 +378,17 @@ def compatible(self, other, run_env): sunits = self.get_prop_value('units') sstd_name = self.get_prop_value('standard_name') sloc_name = self.get_prop_value('local_name') + stopp = self.get_prop_value('top_at_one') sdims = self.get_dimensions() otype = other.get_prop_value('type') okind = other.get_prop_value('kind') ounits = other.get_prop_value('units') ostd_name = other.get_prop_value('standard_name') oloc_name = other.get_prop_value('local_name') + otopp = other.get_prop_value('top_at_one') odims = other.get_dimensions() - compat = VarCompatObj(sstd_name, stype, skind, sunits, sdims, sloc_name, - ostd_name, otype, okind, ounits, odims, oloc_name, + compat = VarCompatObj(sstd_name, stype, skind, sunits, sdims, sloc_name, stopp, + ostd_name, otype, okind, ounits, odims, oloc_name, otopp, run_env, v1_context=self.context, v2_context=other.context) if (not compat) and (run_env.logger is not None): diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index c52c7c4d..b97c9ce6 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -20,6 +20,7 @@ from parse_tools import init_log, set_log_to_null from var_props import is_horizontal_dimension, find_horizontal_dimension from var_props import find_vertical_dimension +from var_props import VarCompatObj # pylint: disable=too-many-lines @@ -816,7 +817,7 @@ def find_variable(self, standard_name=None, source_var=None, # end if return found_var - def match_variable(self, var, vstdname=None, vdims=None): + def match_variable(self, var, run_env): """Try to find a source for in this SuiteObject's dictionary tree. Several items are returned: found_var: True if a match was found @@ -825,21 +826,19 @@ def match_variable(self, var, vstdname=None, vdims=None): missing_vert: Vertical dim in parent but not in perm: Permutation (XXgoldyXX: Not yet implemented) """ - if vstdname is None: - vstdname = var.get_prop_value('standard_name') - # end if - if vdims is None: - vdims = var.get_dimensions() - # end if + vstdname = var.get_prop_value('standard_name') + vdims = var.get_dimensions() if (not vdims) and self.run_phase(): vmatch = VarDictionary.loop_var_match(vstdname) else: vmatch = None # end if + found_var = False missing_vert = None new_vdims = list() var_vdim = var.has_vertical_dimension(dims=vdims) + compat_obj = None # Does this variable exist in the calling tree? dict_var = self.find_variable(source_var=var, any_scope=True) if dict_var is None: @@ -872,6 +871,10 @@ def match_variable(self, var, vstdname=None, vdims=None): new_vdims = list() new_dict_dims = dict_dims match = True + # Create compatability object, containing any necessary forward/reverse + # transforms from and + compat_obj = var.compatible(dict_var, run_env) + # end if # Add the variable to the parent call tree if dict_dims == new_dict_dims: @@ -894,7 +897,7 @@ def match_variable(self, var, vstdname=None, vdims=None): # end if # end if # end if - return found_var, var_vdim, new_vdims, missing_vert + return found_var, var_vdim, new_vdims, missing_vert, compat_obj def in_process_split(self): """Find out if we are in a process-split region""" @@ -1077,6 +1080,8 @@ def __init__(self, scheme_xml, context, parent, run_env): self.__lib = scheme_xml.get('lib', None) self.__has_vertical_dimension = False self.__group = None + self.__forward_transforms = list() + self.__reverse_transforms = list() super().__init__(name, context, parent, run_env, active_call_list=True) def update_group_call_list_variable(self, var): @@ -1144,8 +1149,8 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): def_val = var.get_prop_value('default_value') vdims = var.get_dimensions() vintent = var.get_prop_value('intent') - args = self.match_variable(var, vstdname=vstdname, vdims=vdims) - found, vert_dim, new_dims, missing_vert = args + args = self.match_variable(var, self.run_env) + found, vert_dim, new_dims, missing_vert, compat_obj = args if found: if not self.has_vertical_dim: self.__has_vertical_dimension = vert_dim is not None @@ -1200,6 +1205,12 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): vstdname)) # end if # end if + # Are there any forward/reverse transforms for this variable? + if compat_obj is not None and (compat_obj.has_vert_transforms or + compat_obj.has_unit_transforms or + compat_obj.has_kind_transforms): + self.add_var_transform(var, compat_obj, vert_dim) + # end for if self.needs_vertical is not None: self.parent.add_part(self, replace=True) # Should add a vloop @@ -1211,6 +1222,81 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end if return scheme_mods + def add_var_transform(self, var, compat_obj, vert_dim): + """Register any variable transformation needed by for this Scheme. + For any transformation identified in , create dummy variable + from to perform the transformation. Determine the indices needed + for the transform and save for use during write stage""" + + # Add dummy variable (_local) needed for transformation. + dummy = var.clone(var.get_prop_value('local_name')+'_local') + self.__group.manage_variable(dummy) + + # Create indices (default) for transform. + lindices = [':']*var.get_rank() + rindices = [':']*var.get_rank() + + # If needed, modify vertical dimension for vertical orientation flipping + _, vdim = find_vertical_dimension(var.get_dimensions()) + vdim_name = vert_dim.split(':')[-1] + group_vvar = self.__group.call_list.find_variable(vdim_name) + vname = group_vvar.get_prop_value('local_name') + lindices[vdim] = '1:'+vname + rindices[vdim] = '1:'+vname + if compat_obj.has_vert_transforms: + rindices[vdim] = vname+':1:-1' + + # If needed, modify horizontal dimension for loop substitution. + # NOT YET IMPLEMENTED + #hdim = find_horizontal_dimension(var.get_dimensions()) + #if compat_obj.has_dim_transforms: + + # + # Register any reverse (pre-Scheme) transforms. + # + if (var.get_prop_value('intent') != 'out'): + self.__reverse_transforms.append([dummy.get_prop_value('local_name'), + var.get_prop_value('local_name'), + rindices, lindices, compat_obj]) + + # + # Register any forward (post-Scheme) transforms. + # + if (var.get_prop_value('intent') != 'in'): + self.__forward_transforms.append([var.get_prop_value('local_name'), + dummy.get_prop_value('local_name'), + lindices, rindices, compat_obj]) + + def write_var_transform(self, var, dummy, rindices, lindices, compat_obj, + outfile, indent, forward): + """Write variable transformation needed to call this Scheme in . + is the varaible that needs transformation before and after calling Scheme. + is the local variable needed for the transformation.. + are the LHS indices of for reverse transforms (before Scheme). + are the RHS indices of for reverse transforms (before Scheme). + are the LHS indices of for forward transforms (after Scheme). + are the RHS indices of for forward transforms (after Scheme). + """ + # + # Write reverse (pre-Scheme) transform. + # + if not forward: + # dummy(lindices) = var(rindices) + stmt = compat_obj.reverse_transform(lvar_lname=dummy, + rvar_lname=var, + lvar_indices=lindices, + rvar_indices=rindices) + # + # Write forward (post-Scheme) transform. + # + else: + # var(lindices) = dummy(rindices) + stmt = compat_obj.forward_transform(lvar_lname=var, + rvar_lname=dummy, + lvar_indices=rindices, + rvar_indices=lindices) + outfile.write(stmt, indent+1) + def write(self, outfile, errcode, indent): # Unused arguments are for consistent write interface # pylint: disable=unused-argument @@ -1222,9 +1308,18 @@ def write(self, outfile, errcode, indent): my_args = self.call_list.call_string(cldicts=cldicts, is_func_call=True, subname=self.subroutine_name) - stmt = 'call {}({})' + outfile.write('if ({} == 0) then'.format(errcode), indent) + # Write any reverse (pre-Scheme) transforms. + for (dummy, var, rindices, lindices, compat_obj) in self.__reverse_transforms: + tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent, False) + # Write the scheme call. + stmt = 'call {}({})' outfile.write(stmt.format(self.subroutine_name, my_args), indent+1) + # Write any forward (post-Scheme) transforms. + for (var, dummy, lindices, rindices, compat_obj) in self.__forward_transforms: + tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent, True) + # outfile.write('end if', indent) def schemes(self): diff --git a/scripts/var_props.py b/scripts/var_props.py index 78656df8..dc16fd0f 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -791,62 +791,54 @@ class VarCompatObj: kind_types=["kind_phys=REAL64", \ "kind_dyn=REAL32", \ "kind_host=REAL64"]) - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", [], \ - "var1_lname", "var_stdname", "real", "kind_phys", \ - "m", [], "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", [], "var1_lname", False,\ + "var_stdname", "real", "kind_phys", "m", [], "var2_lname", False,\ + _DOCTEST_RUNENV) #doctest: +ELLIPSIS # Test that a 2-D var with no horizontal transform works - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ - ['horizontal_dimension'], "var1_lname", "var_stdname", \ - "real", "kind_phys", "m", ['horizontal_dimension'], \ - "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var2_lname", False, \ + _DOCTEST_RUNENV) #doctest: +ELLIPSIS # Test that a 2-D var with a horizontal transform works - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ - ['horizontal_dimension'], "var1_lname", "var_stdname", \ - "real", "kind_phys", "m", ['horizontal_loop_extent'], \ - "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "m", ['horizontal_loop_extent'], "var2_lname", False, \ + _DOCTEST_RUNENV) #doctest: +ELLIPSIS # Test that a 2-D var with unit conversion m->km works - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ - ['horizontal_dimension'], "var1_lname", "var_stdname", \ - "real", "kind_phys", "km", ['horizontal_dimension'], \ - "var2_lname", _DOCTEST_RUNENV) #doctest: +ELLIPSIS + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "km", ['horizontal_dimension'], "var2_lname", False, \ + _DOCTEST_RUNENV) #doctest: +ELLIPSIS # Test that a 2-D var with unit conversion m->km works and that it # produces the correct forward transformation - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ - ['horizontal_dimension'], "var1_lname", "var_stdname", \ - "real", "kind_phys", "km", ['horizontal_dimension'], \ - "var2_lname", _DOCTEST_RUNENV).forward_transform( \ - "var1_lname", "var2_lname", ('i')) + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "km", ['horizontal_dimension'], "var2_lname", False, \ + _DOCTEST_RUNENV).forward_transform("var1_lname", "var2_lname", 'i', 'i') 'var1_lname(i) = 1.0E-3_kind_phys*var2_lname(i)' # Test that a 3-D var with unit conversion m->km and vertical flipping # works and that it produces the correct reverse transformation - >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", \ - ['horizontal_dimension', 'vertical_layer_dimension'], \ - "var1_lname", "var_stdname", "real", "kind_phys", "km",\ - ['horizontal_dimension', 'vertical_layer_dimension'], \ - "var2_lname", _DOCTEST_RUNENV).reverse_transform( \ - "var1_lname", "var2_lname", ('i','k'), flip_vdim='nk') + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension', 'vertical_layer_dimension'], "var1_lname", False,\ + "var_stdname", "real", "kind_phys", "km",['horizontal_dimension', 'vertical_layer_dimension'], "var2_lname", True, \ + _DOCTEST_RUNENV).reverse_transform("var1_lname", "var2_lname", ('i','k'), ('i','nk-k+1')) 'var1_lname(i,nk-k+1) = 1.0E+3_kind_phys*var2_lname(i,k)' """ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, - var1_dims, var1_lname, var2_stdname, var2_type, var2_kind, - var2_units, var2_dims, var2_lname, run_env, v1_context=None, + var1_dims, var1_lname, var1_top, var2_stdname, var2_type, var2_kind, + var2_units, var2_dims, var2_lname, var2_top, run_env, v1_context=None, v2_context=None): """Initialize this object with information on the equivalence and/or conformability of two variables. variable 1 is described by , , , - , , , and . + , , , , and . variable 2 is described by , , , - , , , and . + , , , , and . is the CCPPFrameworkEnv object used here to verify kind equivalence or to produce kind transformations. """ @@ -861,6 +853,7 @@ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, self.__dim_transforms = None self.__kind_transforms = None self.__unit_transforms = None + self.has_vert_transforms = False incompat_reason = list() # First, check for fatal incompatibilities if var1_stdname != var2_stdname: @@ -926,6 +919,13 @@ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, var2_units) # end if # end if + if self.__compat: + # Check for vertical array flipping (do later) + if var1_top != var2_top: + self.__compat = True + self.has_vert_transforms = True + # end if + # end if if self.__compat: # Check dimensions ##XXgoldyXX: For now, we always have to create a dimension @@ -945,13 +945,15 @@ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, # end if self.__incompat_reason = " and ".join([x for x in incompat_reason if x]) - def forward_transform(self, lvar_lname, rvar_lname, indices, + def forward_transform(self, lvar_lname, rvar_lname, rvar_indices, lvar_indices, adjust_hdim=None, flip_vdim=None): """Compute and return the the forward transform from "var1" to "var2". is the local name of "var2". is the local name of "var1". - is a tuple of the loop indices for "var1" (i.e., "var1" - will show up in the RHS of the transform as "var1(indices)". + is a tuple of the loop indices for "var1" (i.e., "var1" + will show up in the RHS of the transform as "var1(rvar_indices)". + is a tuple of the loop indices for "var2" (i.e., "var2" + will show up in the LHS of the transform as "var2(lvar_indices)". If is not None, it should be a string containing the local name of the "horizontal_loop_begin" variable. This is used to compute the offset in the horizontal axis index between one and @@ -963,16 +965,10 @@ def forward_transform(self, lvar_lname, rvar_lname, indices, "var2" (i.e., "vertical_layer_dimension" or "vertical_interface_dimension"). """ - # Grab any dimension transform - if self.has_dim_transforms: - dtrans = self.__dim_transforms - lhs_term = dtrans.forward_transform(lvar_lname, indices, - adjust_hdim=adjust_hdim, - flip_vdim=flip_vdim) - else: - lhs_term = f"{lvar_lname}({','.join(indices)})" - # end if - rhs_term = f"{rvar_lname}({','.join(indices)})" + # Dimension transform (Indices handled externally) + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + if self.has_kind_transforms: kind = self.__kind_transforms[1] rhs_term = f"real({rhs_term}, {kind})" @@ -989,13 +985,15 @@ def forward_transform(self, lvar_lname, rvar_lname, indices, # end if return f"{lhs_term} = {rhs_term}" - def reverse_transform(self, lvar_lname, rvar_lname, indices, + def reverse_transform(self, lvar_lname, rvar_lname, rvar_indices, lvar_indices, adjust_hdim=None, flip_vdim=None): """Compute and return the the reverse transform from "var2" to "var1". is the local name of "var1". is the local name of "var2". - is a tuple of the loop indices for "var2" (i.e., "var2" - will show up in the RHS of the transform as "var2(indices)". + is a tuple of the loop indices for "var1" (i.e., "var1" + will show up in the RHS of the transform as "var1(rvar_indices)". + is a tuple of the loop indices for "var2" (i.e., "var2" + will show up in the LHS of the transform as "var2(lvar_indices)". If is not None, it should be a string containing the local name of the "horizontal_loop_begin" variable. This is used to compute the offset in the horizontal axis index between one and @@ -1007,16 +1005,10 @@ def reverse_transform(self, lvar_lname, rvar_lname, indices, "var2" (i.e., "vertical_layer_dimension" or "vertical_interface_dimension"). """ - # Grab any dimension transform - if self.has_dim_transforms: - dtrans = self.__dim_transforms - lhs_term = dtrans.reverse_transform(lvar_lname, indices, - adjust_hdim=adjust_hdim, - flip_vdim=flip_vdim) - else: - lhs_term = f"{lvar_lname}({','.join(indices)})" - # end if - rhs_term = f"{rvar_lname}({','.join(indices)})" + # Dimension transforms (Indices handled externally) + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + if self.has_kind_transforms: kind = self.__kind_transforms[0] rhs_term = f"real({rhs_term}, {kind})" @@ -1054,9 +1046,9 @@ def _get_kind_convstrs(self, var1_kind, var2_kind, run_env): >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ - "m", [], "var1_lname", "var_stdname", \ + "m", [], "var1_lname", False, "var_stdname", \ "real", "kind_phys", "m", [], \ - "var2_lname", _DOCTEST_RUNENV, \ + "var2_lname", False, _DOCTEST_RUNENV, \ v1_context=_DOCTEST_CONTEXT1, \ v2_context=_DOCTEST_CONTEXT2) @@ -1108,9 +1100,9 @@ def _get_unit_convstrs(self, var1_units, var2_units): >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ - "m", [], "var1_lname", "var_stdname", \ + "m", [], "var1_lname", False, "var_stdname", \ "real", "kind_phys", "m", [], \ - "var2_lname", _DOCTEST_RUNENV, \ + "var2_lname", False, _DOCTEST_RUNENV, \ v1_context=_DOCTEST_CONTEXT1, \ v2_context=_DOCTEST_CONTEXT2) @@ -1180,9 +1172,9 @@ def _get_dim_transforms(self, var1_dims, var2_dims): >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') >>> _DOCTEST_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", \ - "m", [], "var1_lname", "var_stdname", \ + "m", [], "var1_lname", False, "var_stdname", \ "real", "kind_phys", "m", [], \ - "var2_lname", _DOCTEST_RUNENV, \ + "var2_lname", False, _DOCTEST_RUNENV, \ v1_context=_DOCTEST_CONTEXT1, \ v2_context=_DOCTEST_CONTEXT2) diff --git a/test/run_fortran_tests.sh b/test/run_fortran_tests.sh index 8b0f5bcb..cd6436fb 100755 --- a/test/run_fortran_tests.sh +++ b/test/run_fortran_tests.sh @@ -37,15 +37,13 @@ if [ $res -ne 0 ]; then echo "Failure running advection test" fi -# Run var_action test -# TODO: Re-enable after feature fully implemented. -# ./var_action_test/run_test -# res=$? -# errcnt=$((errcnt + res)) -# if [ $res -ne 0 ]; then -# echo "Failure running var_action test" -# fi -echo "Skipping var_action_test/run_test until feature is fully implemented" +# Run var_compatability test + ./var_compatability_test/run_test + res=$? + errcnt=$((errcnt + res)) + if [ $res -ne 0 ]; then + echo "Failure running var_compatability test" + fi if [ $errcnt -eq 0 ]; then echo "All tests PASSed!" diff --git a/test/unit_tests/test_var_transforms.py b/test/unit_tests/test_var_transforms.py old mode 100644 new mode 100755 index ae71008f..94a6c4a0 --- a/test/unit_tests/test_var_transforms.py +++ b/test/unit_tests/test_var_transforms.py @@ -282,13 +282,12 @@ def test_valid_dim_transforms(self): self.assertIsInstance(compat, VarCompatObj, msg=self.__inst_emsg.format(type(compat))) rindices = ("hind", "vind") - fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, - adjust_hdim=None, flip_vdim=None) + lindices = rindices + fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, lindices) ind_str = ','.join(rindices) expected = f"{v2_lname}({ind_str}) = {v1_lname}({ind_str})" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, - adjust_hdim=None, flip_vdim=None) + rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, lindices) expected = f"{v1_lname}({ind_str}) = {v2_lname}({ind_str})" self.assertEqual(rev_stmt, expected) @@ -298,17 +297,13 @@ def test_valid_dim_transforms(self): msg=self.__inst_emsg.format(type(compat))) rindices = ("hind", "vind") lindices = ("hind-col_start+1", "vind") - fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) + fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) expected = f"{v2_lname}({lind_str}) = {v1_lname}({rind_str})" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) lindices = ("hind+col_start-1", "vind") + rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, lindices) lind_str = ','.join(lindices) expected = f"{v1_lname}({lind_str}) = {v2_lname}({rind_str})" self.assertEqual(rev_stmt, expected) @@ -320,17 +315,13 @@ def test_valid_dim_transforms(self): msg=self.__inst_emsg.format(type(compat))) rindices = ("hind", "vind") lindices = ("hind-col_start+1", "pver-vind+1") - fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, - adjust_hdim='col_start', - flip_vdim='pver') + fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) expected = f"{v2_lname}({lind_str}) = {v1_lname}({rind_str})" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, - adjust_hdim='col_start', - flip_vdim='pver') lindices = ("hind+col_start-1", "pver-vind+1") + rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, lindices) lind_str = ','.join(lindices) expected = f"{v1_lname}({lind_str}) = {v2_lname}({rind_str})" self.assertEqual(rev_stmt, expected) @@ -342,17 +333,13 @@ def test_valid_dim_transforms(self): rindices = ("hind", "vind") lindices = ("hind-col_start+1", "vind") conv = f"273.15_{real_array1.get_prop_value('kind')}" - fwd_stmt = compat.forward_transform(v3_lname, v1_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) + fwd_stmt = compat.forward_transform(v3_lname, v1_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) expected = f"{v3_lname}({lind_str}) = {v1_lname}({rind_str})+{conv}" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v1_lname, v3_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) lindices = ("hind+col_start-1", "vind") + rev_stmt = compat.reverse_transform(v1_lname, v3_lname, rindices, lindices) lind_str = ','.join(lindices) conv = f"273.15_{real_array2.get_prop_value('kind')}" expected = f"{v1_lname}({lind_str}) = {v3_lname}({rind_str})-{conv}" @@ -364,18 +351,14 @@ def test_valid_dim_transforms(self): msg=self.__inst_emsg.format(type(compat))) rindices = ("hind", "vind") lindices = ("hind", "vind") - fwd_stmt = compat.forward_transform(v4_lname, v3_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) + fwd_stmt = compat.forward_transform(v4_lname, v3_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) rkind = real_array3.get_prop_value('kind') expected = f"{v4_lname}({lind_str}) = real({v3_lname}({rind_str}), {rkind})" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v3_lname, v4_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) lindices = ("hind", "vind") + rev_stmt = compat.reverse_transform(v3_lname, v4_lname, rindices, lindices) lind_str = ','.join(lindices) rkind = real_array4.get_prop_value('kind') expected = f"{v3_lname}({lind_str}) = real({v4_lname}({rind_str}), {rkind})" @@ -389,17 +372,13 @@ def test_valid_dim_transforms(self): lindices = ("hind-col_start+1", "vind") rkind = real_array4.get_prop_value('kind') conv = f"273.15_{rkind}" - fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) + fwd_stmt = compat.forward_transform(v2_lname, v1_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) expected = f"{v2_lname}({lind_str}) = real({v1_lname}({rind_str}), {rkind})+{conv}" self.assertEqual(fwd_stmt, expected) - rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, - adjust_hdim='col_start', - flip_vdim=None) lindices = ("hind+col_start-1", "vind") + rev_stmt = compat.reverse_transform(v1_lname, v2_lname, rindices, lindices) lind_str = ','.join(lindices) rkind = real_array1.get_prop_value('kind') conv = f"273.15_{rkind}" @@ -413,9 +392,7 @@ def test_valid_dim_transforms(self): msg=self.__inst_emsg.format(type(compat))) rindices = ("hind", "vind") lindices = ("pver-vind+1", "hind-col_start+1") - fwd_stmt = compat.forward_transform(v4_lname, v5_lname, rindices, - adjust_hdim='col_start', - flip_vdim='pver') + fwd_stmt = compat.forward_transform(v4_lname, v5_lname, rindices, lindices) lind_str = ','.join(lindices) rind_str = ','.join(rindices) rkind = real_array3.get_prop_value('kind') @@ -423,10 +400,8 @@ def test_valid_dim_transforms(self): self.assertEqual(fwd_stmt, expected) rindices = ("vind", "hind") rind_str = ','.join(rindices) - rev_stmt = compat.reverse_transform(v5_lname, v4_lname, rindices, - adjust_hdim='col_start', - flip_vdim='pver') lindices = ("hind+col_start-1", "pver-vind+1") + rev_stmt = compat.reverse_transform(v5_lname, v4_lname, rindices, lindices) lind_str = ','.join(lindices) rkind = real_array4.get_prop_value('kind') expected = f"{v5_lname}({lind_str}) = {v4_lname}({rind_str})" diff --git a/test/var_action_test/.gitignore b/test/var_compatability_test/.gitignore similarity index 100% rename from test/var_action_test/.gitignore rename to test/var_compatability_test/.gitignore diff --git a/test/var_action_test/CMakeLists.txt b/test/var_compatability_test/CMakeLists.txt similarity index 98% rename from test/var_action_test/CMakeLists.txt rename to test/var_compatability_test/CMakeLists.txt index 4b9daab8..7e3590a6 100644 --- a/test/var_action_test/CMakeLists.txt +++ b/test/var_compatability_test/CMakeLists.txt @@ -19,9 +19,9 @@ get_filename_component(CCPP_ROOT "${TEST_ROOT}" DIRECTORY) # Paths should be relative to CMAKE_SOURCE_DIR (this file's directory) # #------------------------------------------------------------------------------ -LIST(APPEND SCHEME_FILES "var_action_files.txt") +LIST(APPEND SCHEME_FILES "var_compatability_files.txt") LIST(APPEND HOST_FILES "test_host_data" "test_host_mod") -LIST(APPEND SUITE_FILES "var_action_suite.xml") +LIST(APPEND SUITE_FILES "var_compatability_suite.xml") # HOST is the name of the executable we will build. # We assume there are files ${HOST}.meta and ${HOST}.F90 in CMAKE_SOURCE_DIR SET(HOST "${CMAKE_PROJECT_NAME}") diff --git a/test/var_action_test/README.md b/test/var_compatability_test/README.md similarity index 58% rename from test/var_action_test/README.md rename to test/var_compatability_test/README.md index b593a1f9..066cf771 100644 --- a/test/var_action_test/README.md +++ b/test/var_compatability_test/README.md @@ -1,6 +1,6 @@ -var_action test +var_compatability test ================ -To build and run the var_action test, run ./run_test +To build and run the var_compatability test, run ./run_test This script will build and run the test. The exit code is zero (0) on PASS and non-zero on FAIL. diff --git a/test/var_action_test/effr_calc.F90 b/test/var_compatability_test/effr_calc.F90 similarity index 95% rename from test/var_action_test/effr_calc.F90 rename to test/var_compatability_test/effr_calc.F90 index 50804a24..d01408c6 100644 --- a/test/var_action_test/effr_calc.F90 +++ b/test/var_compatability_test/effr_calc.F90 @@ -23,7 +23,7 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrl_inout, & real(kind_phys), intent(in) :: effrr_in(:,:) real(kind_phys), intent(inout) :: effrl_inout(:,:) real(kind_phys), intent(out) :: effri_out(:,:) - real(kind_phys), intent(inout) :: effrs_inout(:,:) + real(8),intent(inout) :: effrs_inout(:,:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- diff --git a/test/var_action_test/effr_calc.meta b/test/var_compatability_test/effr_calc.meta similarity index 97% rename from test/var_action_test/effr_calc.meta rename to test/var_compatability_test/effr_calc.meta index 12e3dac6..78243ce9 100644 --- a/test/var_action_test/effr_calc.meta +++ b/test/var_compatability_test/effr_calc.meta @@ -25,6 +25,7 @@ type = real kind = kind_phys intent = in + top_at_one = .true. [effrl_inout] standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle long_name = effective radius of cloud liquid water particle in micrometer @@ -47,8 +48,9 @@ units = um dimensions = (horizontal_loop_extent,vertical_layer_dimension) type = real - kind = kind_phys + kind = 8 intent = inout + top_at_one = .true. [ errmsg ] standard_name = ccpp_error_message long_name = Error message for error handling in CCPP diff --git a/test/var_action_test/run_test b/test/var_compatability_test/run_test similarity index 72% rename from test/var_action_test/run_test rename to test/var_compatability_test/run_test index 2b4db0ac..ca185966 100755 --- a/test/var_action_test/run_test +++ b/test/var_compatability_test/run_test @@ -118,36 +118,36 @@ frame_src="${framework}/src" ## NB: This has to be after build_dir is finalized ## host_files="${build_dir}/ccpp/test_host_ccpp_cap.F90" -suite_files="${build_dir}/ccpp/ccpp_var_action_suite_cap.F90" +suite_files="${build_dir}/ccpp/ccpp_var_compatability_suite_cap.F90" utility_files="${build_dir}/ccpp/ccpp_kinds.F90" utility_files="${utility_files},${frame_src}/ccpp_constituent_prop_mod.F90" utility_files="${utility_files},${frame_src}/ccpp_hashable.F90" utility_files="${utility_files},${frame_src}/ccpp_hash_table.F90" ccpp_files="${utility_files}" ccpp_files="${ccpp_files},${build_dir}/ccpp/test_host_ccpp_cap.F90" -ccpp_files="${ccpp_files},${build_dir}/ccpp/ccpp_var_action_suite_cap.F90" +ccpp_files="${ccpp_files},${build_dir}/ccpp/ccpp_var_compatability_suite_cap.F90" #process_list="" module_list="effr_calc" #dependencies="" -suite_list="var_action_suite" -required_vars_var_action="ccpp_error_code,ccpp_error_message" -required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_ice_particle" -required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_liquid_water_particle" -required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_rain_particle" -required_vars_var_action="${required_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" -required_vars_var_action="${required_vars_var_action},horizontal_loop_begin" -required_vars_var_action="${required_vars_var_action},horizontal_loop_end" -required_vars_var_action="${required_vars_var_action},vertical_layer_dimension" -input_vars_var_action="effective_radius_of_stratiform_cloud_liquid_water_particle" -input_vars_var_action="${input_vars_var_action},effective_radius_of_stratiform_cloud_rain_particle" -input_vars_var_action="${input_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" -input_vars_var_action="${input_vars_var_action},horizontal_loop_begin" -input_vars_var_action="${input_vars_var_action},horizontal_loop_end" -input_vars_var_action="${input_vars_var_action},vertical_layer_dimension" -output_vars_var_action="ccpp_error_code,ccpp_error_message" -output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_ice_particle" -output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_liquid_water_particle" -output_vars_var_action="${output_vars_var_action},effective_radius_of_stratiform_cloud_snow_particle" +suite_list="var_compatability_suite" +required_vars_var_compatability="ccpp_error_code,ccpp_error_message" +required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_ice_particle" +required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_liquid_water_particle" +required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_rain_particle" +required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" +required_vars_var_compatability="${required_vars_var_compatability},horizontal_loop_begin" +required_vars_var_compatability="${required_vars_var_compatability},horizontal_loop_end" +required_vars_var_compatability="${required_vars_var_compatability},vertical_layer_dimension" +input_vars_var_compatability="effective_radius_of_stratiform_cloud_liquid_water_particle" +input_vars_var_compatability="${input_vars_var_compatability},effective_radius_of_stratiform_cloud_rain_particle" +input_vars_var_compatability="${input_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" +input_vars_var_compatability="${input_vars_var_compatability},horizontal_loop_begin" +input_vars_var_compatability="${input_vars_var_compatability},horizontal_loop_end" +input_vars_var_compatability="${input_vars_var_compatability},vertical_layer_dimension" +output_vars_var_compatability="ccpp_error_code,ccpp_error_message" +output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_ice_particle" +output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_liquid_water_particle" +output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" ## ## Run a database report and check the return string @@ -215,13 +215,13 @@ check_datatable ${report_prog} ${datafile} "--module-list" ${module_list} #check_datatable ${report_prog} ${datafile} "--dependencies" ${dependencies} check_datatable ${report_prog} ${datafile} "--suite-list" ${suite_list} \ --sep ";" -echo -e "\nChecking variables for var_action suite from command line" +echo -e "\nChecking variables for var_compatability suite from command line" check_datatable ${report_prog} ${datafile} "--required-variables" \ - ${required_vars_var_action} "var_action_suite" + ${required_vars_var_compatability} "var_compatability_suite" check_datatable ${report_prog} ${datafile} "--input-variables" \ - ${input_vars_var_action} "var_action_suite" + ${input_vars_var_compatability} "var_compatability_suite" check_datatable ${report_prog} ${datafile} "--output-variables" \ - ${output_vars_var_action} "var_action_suite" + ${output_vars_var_compatability} "var_compatability_suite" # Run make make res=$? diff --git a/test/var_action_test/test_host.F90 b/test/var_compatability_test/test_host.F90 similarity index 99% rename from test/var_action_test/test_host.F90 rename to test/var_compatability_test/test_host.F90 index 885c2e62..6de9597c 100644 --- a/test/var_action_test/test_host.F90 +++ b/test/var_compatability_test/test_host.F90 @@ -8,7 +8,7 @@ module test_prog public test_host ! Public data and interfaces - integer, public, parameter :: cs = 16 + integer, public, parameter :: cs = 32 integer, public, parameter :: cm = 60 !> \section arg_table_suite_info Argument Table @@ -375,7 +375,7 @@ program test logical :: run_okay ! Setup expected test suite info - test_suites(1)%suite_name = 'var_action_suite' + test_suites(1)%suite_name = 'var_compatability_suite' test_suites(1)%suite_parts => test_parts1 test_suites(1)%suite_input_vars => test_invars1 test_suites(1)%suite_output_vars => test_outvars1 diff --git a/test/var_action_test/test_host.meta b/test/var_compatability_test/test_host.meta similarity index 100% rename from test/var_action_test/test_host.meta rename to test/var_compatability_test/test_host.meta diff --git a/test/var_action_test/test_host_data.F90 b/test/var_compatability_test/test_host_data.F90 similarity index 100% rename from test/var_action_test/test_host_data.F90 rename to test/var_compatability_test/test_host_data.F90 diff --git a/test/var_action_test/test_host_data.meta b/test/var_compatability_test/test_host_data.meta similarity index 100% rename from test/var_action_test/test_host_data.meta rename to test/var_compatability_test/test_host_data.meta diff --git a/test/var_action_test/test_host_mod.F90 b/test/var_compatability_test/test_host_mod.F90 similarity index 97% rename from test/var_action_test/test_host_mod.F90 rename to test/var_compatability_test/test_host_mod.F90 index d6191814..b0501c0c 100644 --- a/test/var_action_test/test_host_mod.F90 +++ b/test/var_compatability_test/test_host_mod.F90 @@ -36,7 +36,7 @@ logical function compare_data() real(kind_phys), parameter :: effrl_expected = 5.0E-5 ! 50 microns, in meter real(kind_phys), parameter :: effri_expected = 7.5E-5 ! 75 microns, in meter real(kind_phys), parameter :: effrs_expected = 5.1E-4 ! 510 microns, in meter - real(kind_phys), parameter :: tolerance = 1.0E-10 ! used as scaling factor for expected value + real(kind_phys), parameter :: tolerance = 1.0E-6 ! used as scaling factor for expected value compare_data = .true. diff --git a/test/var_action_test/test_host_mod.meta b/test/var_compatability_test/test_host_mod.meta similarity index 100% rename from test/var_action_test/test_host_mod.meta rename to test/var_compatability_test/test_host_mod.meta diff --git a/test/var_action_test/test_reports.py b/test/var_compatability_test/test_reports.py similarity index 94% rename from test/var_action_test/test_reports.py rename to test/var_compatability_test/test_reports.py index fff8603d..a8dc6eea 100755 --- a/test/var_action_test/test_reports.py +++ b/test/var_compatability_test/test_reports.py @@ -57,16 +57,16 @@ def usage(errmsg=None): # Check data _HOST_FILES = [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90")] -_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_action_suite_cap.F90")] +_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatability_suite_cap.F90")] _UTILITY_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_kinds.F90"), os.path.join(_SRC_DIR, "ccpp_constituent_prop_mod.F90"), os.path.join(_SRC_DIR, "ccpp_hashable.F90"), os.path.join(_SRC_DIR, "ccpp_hash_table.F90")] _CCPP_FILES = _UTILITY_FILES + \ [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), - os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_action_suite_cap.F90")] + os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatability_suite_cap.F90")] _MODULE_LIST = ["effr_calc"] -_SUITE_LIST = ["var_action_suite"] +_SUITE_LIST = ["var_compatability_suite"] _INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "vertical_layer_dimension", "effective_radius_of_stratiform_cloud_liquid_water_particle", "effective_radius_of_stratiform_cloud_rain_particle", @@ -140,15 +140,15 @@ def check_datatable(database, report_type, check_list, _MODULE_LIST) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_list"), _SUITE_LIST) -print("\nChecking variables for var_action suite from python") +print("\nChecking variables for var_compatability suite from python") NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("required_variables", - value="var_action_suite"), + value="var_compatability_suite"), _REQUIRED_VARS_VAR_ACTION) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("input_variables", - value="var_action_suite"), + value="var_compatability_suite"), _INPUT_VARS_VAR_ACTION) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("output_variables", - value="var_action_suite"), + value="var_compatability_suite"), _OUTPUT_VARS_VAR_ACTION) sys.exit(NUM_ERRORS) diff --git a/test/var_action_test/var_action_files.txt b/test/var_compatability_test/var_compatability_files.txt similarity index 100% rename from test/var_action_test/var_action_files.txt rename to test/var_compatability_test/var_compatability_files.txt diff --git a/test/var_action_test/var_action_suite.xml b/test/var_compatability_test/var_compatability_suite.xml similarity index 69% rename from test/var_action_test/var_action_suite.xml rename to test/var_compatability_test/var_compatability_suite.xml index 72a7b80f..ae75d9f7 100644 --- a/test/var_action_test/var_action_suite.xml +++ b/test/var_compatability_test/var_compatability_suite.xml @@ -1,6 +1,6 @@ - + effr_calc diff --git a/test_prebuild/test_blocked_data/CMakeLists.txt b/test_prebuild/test_blocked_data/CMakeLists.txt new file mode 100644 index 00000000..921c87e6 --- /dev/null +++ b/test_prebuild/test_blocked_data/CMakeLists.txt @@ -0,0 +1,90 @@ +#------------------------------------------------------------------------------ +cmake_minimum_required(VERSION 3.0) + +project(ccpp_blocked_data + VERSION 1.0.0 + LANGUAGES Fortran) + +#------------------------------------------------------------------------------ +# Request a static build +option(BUILD_SHARED_LIBS "Build a shared library" OFF) + +#------------------------------------------------------------------------------ +# Set the sources: physics type definitions +set(TYPEDEFS $ENV{CCPP_TYPEDEFS}) +if(TYPEDEFS) + message(STATUS "Got CCPP TYPEDEFS from environment variable: ${TYPEDEFS}") +else(TYPEDEFS) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_TYPEDEFS.cmake) + message(STATUS "Got CCPP TYPEDEFS from cmakefile include file: ${TYPEDEFS}") +endif(TYPEDEFS) + +# Generate list of Fortran modules from the CCPP type +# definitions that need need to be installed +foreach(typedef_module ${TYPEDEFS}) + list(APPEND MODULES_F90 ${CMAKE_CURRENT_BINARY_DIR}/${typedef_module}) +endforeach() + +#------------------------------------------------------------------------------ +# Set the sources: physics schemes +set(SCHEMES $ENV{CCPP_SCHEMES}) +if(SCHEMES) + message(STATUS "Got CCPP SCHEMES from environment variable: ${SCHEMES}") +else(SCHEMES) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_SCHEMES.cmake) + message(STATUS "Got CCPP SCHEMES from cmakefile include file: ${SCHEMES}") +endif(SCHEMES) + +# Set the sources: physics scheme caps +set(CAPS $ENV{CCPP_CAPS}) +if(CAPS) + message(STATUS "Got CCPP CAPS from environment variable: ${CAPS}") +else(CAPS) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_CAPS.cmake) + message(STATUS "Got CCPP CAPS from cmakefile include file: ${CAPS}") +endif(CAPS) + +# Set the sources: physics scheme caps +set(API $ENV{CCPP_API}) +if(API) + message(STATUS "Got CCPP API from environment variable: ${API}") +else(API) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_API.cmake) + message(STATUS "Got CCPP API from cmakefile include file: ${API}") +endif(API) + +set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} -O0 -fno-unsafe-math-optimizations -frounding-math -fsignaling-nans -ffpe-trap=invalid,zero,overflow -fbounds-check -ggdb -fbacktrace -ffree-line-length-none") + +#------------------------------------------------------------------------------ +add_library(ccpp_blocked_data STATIC ${SCHEMES} ${CAPS} ${API}) +# Generate list of Fortran modules from defined sources +foreach(source_f90 ${CAPS} ${API}) + get_filename_component(tmp_source_f90 ${source_f90} NAME) + string(REGEX REPLACE ".F90" ".mod" tmp_module_f90 ${tmp_source_f90}) + string(TOLOWER ${tmp_module_f90} module_f90) + list(APPEND MODULES_F90 ${CMAKE_CURRENT_BINARY_DIR}/${module_f90}) +endforeach() + +set_target_properties(ccpp_blocked_data PROPERTIES VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) + +add_executable(test_blocked_data.x main.F90) +add_dependencies(test_blocked_data.x ccpp_blocked_data) +target_link_libraries(test_blocked_data.x ccpp_blocked_data) +set_target_properties(test_blocked_data.x PROPERTIES LINKER_LANGUAGE Fortran) + +# Define where to install the library +install(TARGETS ccpp_blocked_data + EXPORT ccpp_blocked_data-targets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib +) +# Export our configuration +install(EXPORT ccpp_blocked_data-targets + FILE ccpp_blocked_data-config.cmake + DESTINATION lib/cmake +) +# Define where to install the C headers and Fortran modules +#install(FILES ${HEADERS_C} DESTINATION include) +install(FILES ${MODULES_F90} DESTINATION include) diff --git a/test_prebuild/test_blocked_data/README.md b/test_prebuild/test_blocked_data/README.md new file mode 100644 index 00000000..ad977913 --- /dev/null +++ b/test_prebuild/test_blocked_data/README.md @@ -0,0 +1,13 @@ +# How to build the blocked data test + +1. Set compiler environment as appropriate for your system +2. Run the following commands: +``` +cd test_prebuild/test_blocked_data/ +rm -fr build +mkdir build +../../scripts/ccpp_prebuild.py --config=ccpp_prebuild_config.py --builddir=build +cd build +cmake .. 2>&1 | tee log.cmake +make 2>&1 | tee log.make +``` diff --git a/test_prebuild/test_blocked_data/blocked_data_scheme.F90 b/test_prebuild/test_blocked_data/blocked_data_scheme.F90 new file mode 100644 index 00000000..eeda2206 --- /dev/null +++ b/test_prebuild/test_blocked_data/blocked_data_scheme.F90 @@ -0,0 +1,118 @@ +!>\file blocked_data_scheme.F90 +!! This file contains a blocked_data_scheme CCPP scheme that does nothing +!! except requesting the minimum, mandatory variables. + +module blocked_data_scheme + + use, intrinsic :: iso_fortran_env, only: error_unit + implicit none + + private + public :: blocked_data_scheme_init, & + blocked_data_scheme_timestep_init, & + blocked_data_scheme_run, & + blocked_data_scheme_timestep_finalize, & + blocked_data_scheme_finalize + + ! This is for unit testing only + integer, parameter, dimension(4) :: data_array_sizes = (/6,6,6,3/) + + contains + +!! \section arg_table_blocked_data_scheme_init Argument Table +!! \htmlinclude blocked_data_scheme_init.html +!! + subroutine blocked_data_scheme_init(data_array, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: data_array(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Check size of data array + write(error_unit,'(a,i3)') 'In blocked_data_scheme_init: checking size of data array to be', sum(data_array_sizes) + if (size(data_array)/=sum(data_array_sizes)) then + write(errmsg,'(2(a,i3))') "Error, expected size(data_array)==", sum(data_array_sizes), "but got ", size(data_array) + errflg = 1 + return + end if + end subroutine blocked_data_scheme_init + +!! \section arg_table_blocked_data_scheme_timestep_init Argument Table +!! \htmlinclude blocked_data_scheme_timestep_init.html +!! + subroutine blocked_data_scheme_timestep_init(data_array, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: data_array(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Check size of data array + write(error_unit,'(a,i3)') 'In blocked_data_scheme_timestep_init: checking size of data array to be', sum(data_array_sizes) + if (size(data_array)/=sum(data_array_sizes)) then + write(errmsg,'(2(a,i3))') "Error, expected size(data_array)==", sum(data_array_sizes), " but got ", size(data_array) + errflg = 1 + return + end if + end subroutine blocked_data_scheme_timestep_init + +!! \section arg_table_blocked_data_scheme_run Argument Table +!! \htmlinclude blocked_data_scheme_run.html +!! + subroutine blocked_data_scheme_run(nb, data_array, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: nb + integer, intent(in) :: data_array(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Check size of data array + write(error_unit,'(2(a,i3))') 'In blocked_data_scheme_run: checking size of data array for block', nb, ' to be', data_array_sizes(nb) + if (size(data_array)/=data_array_sizes(nb)) then + write(errmsg,'(a,i4)') "Error in blocked_data_scheme_run, expected size(data_array)==6, got ", size(data_array) + errflg = 1 + return + end if + end subroutine blocked_data_scheme_run + + !! \section arg_table_blocked_data_scheme_timestep_finalize Argument Table + !! \htmlinclude blocked_data_scheme_timestep_finalize.html + !! + subroutine blocked_data_scheme_timestep_finalize(data_array, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: data_array(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Check size of data array + write(error_unit,'(a,i3)') 'In blocked_data_scheme_timestep_finalize: checking size of data array to be', sum(data_array_sizes) + if (size(data_array)/=sum(data_array_sizes)) then + write(errmsg,'(2(a,i3))') "Error, expected size(data_array)==", sum(data_array_sizes), "but got ", size(data_array) + errflg = 1 + return + end if + end subroutine blocked_data_scheme_timestep_finalize + +!! \section arg_table_blocked_data_scheme_finalize Argument Table +!! \htmlinclude blocked_data_scheme_finalize.html +!! + subroutine blocked_data_scheme_finalize(data_array, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: data_array(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Check size of data array + write(error_unit,'(a,i3)') 'In blocked_data_scheme_finalize: checking size of data array to be', sum(data_array_sizes) + if (size(data_array)/=sum(data_array_sizes)) then + write(errmsg,'(2(a,i3))') "Error, expected size(data_array)==", sum(data_array_sizes), "but got ", size(data_array) + errflg = 1 + return + end if + end subroutine blocked_data_scheme_finalize + +end module blocked_data_scheme diff --git a/test_prebuild/test_blocked_data/blocked_data_scheme.meta b/test_prebuild/test_blocked_data/blocked_data_scheme.meta new file mode 100644 index 00000000..d92b0da6 --- /dev/null +++ b/test_prebuild/test_blocked_data/blocked_data_scheme.meta @@ -0,0 +1,147 @@ +[ccpp-table-properties] + name = blocked_data_scheme + type = scheme + dependencies = + +######################################################################## +[ccpp-arg-table] + name = blocked_data_scheme_init + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[data_array] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_dimension) + type = integer + intent = in + +######################################################################## +[ccpp-arg-table] + name = blocked_data_scheme_timestep_init + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[data_array] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_dimension) + type = integer + intent = in + +######################################################################## +[ccpp-arg-table] + name = blocked_data_scheme_run + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[nb] + standard_name = ccpp_block_number + long_name = number of block for explicit data blocking in CCPP + units = index + dimensions = () + type = integer + intent = in +[data_array] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_loop_extent) + type = integer + intent = in + +######################################################################## +[ccpp-arg-table] + name = blocked_data_scheme_timestep_finalize + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[data_array] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_dimension) + type = integer + intent = in + +######################################################################## +[ccpp-arg-table] + name = blocked_data_scheme_finalize + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[data_array] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_dimension) + type = integer + intent = in + diff --git a/test_prebuild/test_blocked_data/ccpp_prebuild_config.py b/test_prebuild/test_blocked_data/ccpp_prebuild_config.py new file mode 100644 index 00000000..700d9f76 --- /dev/null +++ b/test_prebuild/test_blocked_data/ccpp_prebuild_config.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# CCPP prebuild config for GFDL Finite-Volume Cubed-Sphere Model (FV3) + +############################################################################### +# Definitions # +############################################################################### + +HOST_MODEL_IDENTIFIER = "FV3" + +# Add all files with metadata tables on the host model side and in CCPP, +# relative to basedir = top-level directory of host model. This includes +# kind and type definitions used in CCPP physics. Also add any internal +# dependencies of these files to the list. +VARIABLE_DEFINITION_FILES = [ + # actual variable definition files + '../../src/ccpp_types.F90', + 'data.F90', + ] + +TYPEDEFS_NEW_METADATA = { + 'ccpp_types' : { + 'ccpp_t' : 'cdata', + 'ccpp_types' : '', + }, + 'data' : { + 'blocked_data_type' : 'blocked_data_instance(cdata%blk_no)', + 'data' : '', + }, + } + +# Add all physics scheme files relative to basedir +SCHEME_FILES = [ + 'blocked_data_scheme.F90', + ] + +# Default build dir, relative to current working directory, +# if not specified as command-line argument +DEFAULT_BUILD_DIR = 'build' + +# Auto-generated makefile/cmakefile snippets that contain all type definitions +TYPEDEFS_MAKEFILE = '{build_dir}/CCPP_TYPEDEFS.mk' +TYPEDEFS_CMAKEFILE = '{build_dir}/CCPP_TYPEDEFS.cmake' +TYPEDEFS_SOURCEFILE = '{build_dir}/CCPP_TYPEDEFS.sh' + +# Auto-generated makefile/cmakefile snippets that contain all schemes +SCHEMES_MAKEFILE = '{build_dir}/CCPP_SCHEMES.mk' +SCHEMES_CMAKEFILE = '{build_dir}/CCPP_SCHEMES.cmake' +SCHEMES_SOURCEFILE = '{build_dir}/CCPP_SCHEMES.sh' + +# Auto-generated makefile/cmakefile snippets that contain all caps +CAPS_MAKEFILE = '{build_dir}/CCPP_CAPS.mk' +CAPS_CMAKEFILE = '{build_dir}/CCPP_CAPS.cmake' +CAPS_SOURCEFILE = '{build_dir}/CCPP_CAPS.sh' + +# Directory where to put all auto-generated physics caps +CAPS_DIR = '{build_dir}' + +# Directory where the suite definition files are stored +SUITES_DIR = '.' + +# Optional arguments - only required for schemes that use +# optional arguments. ccpp_prebuild.py will throw an exception +# if it encounters a scheme subroutine with optional arguments +# if no entry is made here. Possible values are: 'all', 'none', +# or a list of standard_names: [ 'var1', 'var3' ]. +OPTIONAL_ARGUMENTS = {} + +# Directory where to write static API to +STATIC_API_DIR = '{build_dir}' +STATIC_API_CMAKEFILE = '{build_dir}/CCPP_API.cmake' +STATIC_API_SOURCEFILE = '{build_dir}/CCPP_API.sh' + +# Directory for writing HTML pages generated from metadata files +METADATA_HTML_OUTPUT_DIR = '{build_dir}' + +# HTML document containing the model-defined CCPP variables +HTML_VARTABLE_FILE = '{build_dir}/CCPP_VARIABLES_BLOCKED_DATA.html' + +# LaTeX document containing the provided vs requested CCPP variables +LATEX_VARTABLE_FILE = '{build_dir}/CCPP_VARIABLES_BLOCKED_DATA.tex' diff --git a/test_prebuild/test_blocked_data/data.F90 b/test_prebuild/test_blocked_data/data.F90 new file mode 100644 index 00000000..97ad051e --- /dev/null +++ b/test_prebuild/test_blocked_data/data.F90 @@ -0,0 +1,41 @@ +module data + +!! \section arg_table_data Argument Table +!! \htmlinclude data.html +!! + use ccpp_types, only: ccpp_t + + implicit none + + private + + public nblks, blksz, ncols + public ccpp_data_domain, ccpp_data_blocks, blocked_data_type, blocked_data_instance + + integer, parameter :: nblks = 4 + type(ccpp_t), target :: ccpp_data_domain + type(ccpp_t), dimension(nblks), target :: ccpp_data_blocks + + integer, parameter, dimension(nblks) :: blksz = (/6,6,6,3/) + integer, parameter :: ncols = sum(blksz) + +!! \section arg_table_blocked_data_type +!! \htmlinclude blocked_data_type.html +!! + type blocked_data_type + integer, dimension(:), allocatable :: array_data + contains + procedure :: create => blocked_data_create + end type blocked_data_type + + type(blocked_data_type), dimension(nblks) :: blocked_data_instance + +contains + + subroutine blocked_data_create(blocked_data_instance, ncol) + class(blocked_data_type), intent(inout) :: blocked_data_instance + integer, intent(in) :: ncol + allocate(blocked_data_instance%array_data(ncol)) + end subroutine blocked_data_create + +end module data diff --git a/test_prebuild/test_blocked_data/data.meta b/test_prebuild/test_blocked_data/data.meta new file mode 100644 index 00000000..c5fa2842 --- /dev/null +++ b/test_prebuild/test_blocked_data/data.meta @@ -0,0 +1,69 @@ +[ccpp-table-properties] + name = blocked_data_type + type = ddt + dependencies = +[ccpp-arg-table] + name = blocked_data_type + type = ddt +[array_data] + standard_name = blocked_data_array + long_name = blocked data array + units = 1 + dimensions = (horizontal_loop_extent) + type = integer + +[ccpp-table-properties] + name = data + type = module + dependencies = +[ccpp-arg-table] + name = data + type = module +[cdata] + standard_name = ccpp_t_instance + long_name = instance of derived data type ccpp_t + units = DDT + dimensions = () + type = ccpp_t +[nblks] + standard_name = ccpp_block_count + long_name = for explicit data blocking: number of blocks + units = count + dimensions = () + type = integer +[blksz] + standard_name = ccpp_block_sizes + long_name = for explicit data blocking: block sizes of all blocks + units = count + dimensions = (ccpp_block_count) + type = integer +[blksz(ccpp_block_number)] + standard_name = horizontal_loop_extent + long_name = horizontal loop extent + units = count + dimensions = () + type = integer +[ncols] + standard_name = horizontal_dimension + long_name = horizontal dimension + units = count + dimensions = () + type = integer +[blocked_data_type] + standard_name = blocked_data_type + long_name = definition of type blocked_data_type + units = DDT + dimensions = () + type = blocked_data_type +[blocked_data_instance(ccpp_block_number)] + standard_name = blocked_data_type_instance + long_name = instance of derived data type blocked_data_type + units = DDT + dimensions = () + type = blocked_data_type +[blocked_data_instance] + standard_name = blocked_data_type_instance_all_blocks + long_name = instance of derived data type blocked_data_type + units = DDT + dimensions = (ccpp_block_count) + type = blocked_data_type diff --git a/test_prebuild/test_blocked_data/main.F90 b/test_prebuild/test_blocked_data/main.F90 new file mode 100644 index 00000000..cc57f618 --- /dev/null +++ b/test_prebuild/test_blocked_data/main.F90 @@ -0,0 +1,107 @@ +program test_blocked_data + + use, intrinsic :: iso_fortran_env, only: error_unit + + use ccpp_types, only: ccpp_t + use data, only: nblks, blksz, ncols + use data, only: ccpp_data_domain, ccpp_data_blocks, & + blocked_data_type, blocked_data_instance + + use ccpp_static_api, only: ccpp_physics_init, & + ccpp_physics_timestep_init, & + ccpp_physics_run, & + ccpp_physics_timestep_finalize, & + ccpp_physics_finalize + + implicit none + + character(len=*), parameter :: ccpp_suite = 'blocked_data_suite' + integer :: ib, ierr + type(ccpp_t), pointer :: cdata => null() + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + ! For physics running over the entire domain, block and thread + ! number are not used; set to safe values + ccpp_data_domain%blk_no = 1 + ccpp_data_domain%thrd_no = 1 + + ! Loop over all blocks and threads for ccpp_data_blocks + do ib=1,nblks + ! Assign the correct block numbers, only one thread + ccpp_data_blocks(ib)%blk_no = ib + ccpp_data_blocks(ib)%thrd_no = 1 + end do + + do ib=1,size(blocked_data_instance) + allocate(blocked_data_instance(ib)%array_data(blksz(ib))) + write(error_unit,'(2(a,i3))') "Allocated array_data for block", ib, " to size", size(blocked_data_instance(ib)%array_data) + end do + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + cdata => ccpp_data_domain + call ccpp_physics_init(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics timestep init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + cdata => ccpp_data_domain + call ccpp_physics_timestep_init(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics run step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + do ib=1,nblks + cdata => ccpp_data_blocks(ib) + call ccpp_physics_run(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a,i3,a)') "An error occurred in ccpp_physics_run for block", ib, ":" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + end do + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics timestep finalize step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + cdata => ccpp_data_domain + call ccpp_physics_timestep_finalize(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics finalize step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + cdata => ccpp_data_domain + call ccpp_physics_finalize(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + +contains + +end program test_blocked_data \ No newline at end of file diff --git a/test_prebuild/test_blocked_data/suite_blocked_data_suite.xml b/test_prebuild/test_blocked_data/suite_blocked_data_suite.xml new file mode 100644 index 00000000..cf4fe9a4 --- /dev/null +++ b/test_prebuild/test_blocked_data/suite_blocked_data_suite.xml @@ -0,0 +1,9 @@ + + + + + + blocked_data_scheme + + + diff --git a/tests/test_metadata_parser.py b/test_prebuild/test_metadata_parser.py similarity index 100% rename from tests/test_metadata_parser.py rename to test_prebuild/test_metadata_parser.py diff --git a/tests/test_mkstatic.py b/test_prebuild/test_mkstatic.py similarity index 100% rename from tests/test_mkstatic.py rename to test_prebuild/test_mkstatic.py From a4e174fdee886da355e9c1aff2d4d7681666fb2c Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Fri, 19 Jan 2024 07:04:50 -0700 Subject: [PATCH 40/49] Add debug switch to capgen (perform variable allocation checks etc) + fix spelling: var_compatability --> var_compatibility (#512) * Add capability to parse Fortran pointer variables to fortran_tools/parse_fortran.py * Add --debug option to capgen (framework_env.py) * For advection tests, add --debug flag to capgen and update test reports * Add active attribute to variable water_vapor_specific_humidity in capgen_test, add --debug flag to capgen and update test reports * For var_action tests, add --debug flag to capgen and update test reports * Add docstring documentation for add_var_debug_check and write_var_debug_check in scripts/suite_objects.py * Bug fix in scripts/suite_objects.py: also check variable allocations for variables local to the group and assign correct dimensions for local (group) and module (suite) variables * Fix spelling: compatability --> compatibility --------- Co-authored-by: mwaxmonsky <137746677+mwaxmonsky@users.noreply.github.com> --- scripts/framework_env.py | 20 +- scripts/metavar.py | 50 +++ scripts/parse_tools/__init__.py | 5 +- scripts/parse_tools/fortran_conditional.py | 13 + scripts/suite_objects.py | 304 +++++++++++++++++- scripts/var_props.py | 2 +- test/advection_test/CMakeLists.txt | 1 + test/advection_test/run_test | 7 +- test/advection_test/test_reports.py | 10 +- test/capgen_test/CMakeLists.txt | 1 + test/capgen_test/run_test | 4 + test/capgen_test/test_host_data.meta | 1 + test/capgen_test/test_reports.py | 5 +- test/run_fortran_tests.sh | 6 +- .../.gitignore | 0 .../CMakeLists.txt | 5 +- .../README.md | 4 +- .../effr_calc.F90 | 0 .../effr_calc.meta | 0 .../run_test | 52 +-- .../test_host.F90 | 2 +- .../test_host.meta | 0 .../test_host_data.F90 | 0 .../test_host_data.meta | 0 .../test_host_mod.F90 | 0 .../test_host_mod.meta | 0 .../test_reports.py | 16 +- .../var_compatibility_files.txt} | 0 .../var_compatibility_suite.xml} | 2 +- test_prebuild/test_blocked_data/README.md | 1 + 30 files changed, 451 insertions(+), 60 deletions(-) create mode 100755 scripts/parse_tools/fortran_conditional.py mode change 100644 => 100755 scripts/suite_objects.py rename test/{var_compatability_test => var_compatibility_test}/.gitignore (100%) rename test/{var_compatability_test => var_compatibility_test}/CMakeLists.txt (98%) rename test/{var_compatability_test => var_compatibility_test}/README.md (58%) rename test/{var_compatability_test => var_compatibility_test}/effr_calc.F90 (100%) rename test/{var_compatability_test => var_compatibility_test}/effr_calc.meta (100%) rename test/{var_compatability_test => var_compatibility_test}/run_test (79%) rename test/{var_compatability_test => var_compatibility_test}/test_host.F90 (99%) rename test/{var_compatability_test => var_compatibility_test}/test_host.meta (100%) rename test/{var_compatability_test => var_compatibility_test}/test_host_data.F90 (100%) rename test/{var_compatability_test => var_compatibility_test}/test_host_data.meta (100%) rename test/{var_compatability_test => var_compatibility_test}/test_host_mod.F90 (100%) rename test/{var_compatability_test => var_compatibility_test}/test_host_mod.meta (100%) rename test/{var_compatability_test => var_compatibility_test}/test_reports.py (95%) rename test/{var_compatability_test/var_compatability_files.txt => var_compatibility_test/var_compatibility_files.txt} (100%) rename test/{var_compatability_test/var_compatability_suite.xml => var_compatibility_test/var_compatibility_suite.xml} (69%) diff --git a/scripts/framework_env.py b/scripts/framework_env.py index c0bb4139..88c9c204 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -25,7 +25,8 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, host_files=None, scheme_files=None, suites=None, preproc_directives=[], generate_docfiles=False, host_name='', kind_types=[], use_error_obj=False, force_overwrite=False, - output_root=os.getcwd(), ccpp_datafile="datatable.xml"): + output_root=os.getcwd(), ccpp_datafile="datatable.xml", + debug=False): """Initialize a new CCPPFrameworkEnv object from the input arguments. is a dict with the parsed command-line arguments (or a dictionary created with the necessary arguments). @@ -198,6 +199,13 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, self.__datatable_file = os.path.join(self.output_dir, self.datatable_file) # end if + # Enable or disable variable allocation checks + if ndict and ('debug' in ndict): + self.__debug = ndict['debug'] + del ndict['debug'] + else: + self.__debug = debug + # end if self.__logger = logger ## Check to see if anything is left in dictionary if ndict: @@ -311,6 +319,12 @@ def datatable_file(self): CCPPFrameworkEnv object.""" return self.__datatable_file + @property + def debug(self): + """Return the property for this + CCPPFrameworkEnv object.""" + return self.__debug + @property def logger(self): """Return the property for this CCPPFrameworkEnv object.""" @@ -386,7 +400,11 @@ def parse_command_line(args, description, logger=None): help="""Overwrite all CCPP-generated files, even if unmodified""") + parser.add_argument("--debug", action='store_true', default=False, + help="Add variable allocation checks to assist debugging") + parser.add_argument("--verbose", action='count', default=0, help="Log more activity, repeat for increased output") + pargs = parser.parse_args(args) return CCPPFrameworkEnv(logger, vars(pargs)) diff --git a/scripts/metavar.py b/scripts/metavar.py index 0c5a87d2..eb1973e1 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -23,6 +23,7 @@ from parse_tools import check_molar_mass from parse_tools import ParseContext, ParseSource, type_name from parse_tools import ParseInternalError, ParseSyntaxError, CCPPError +from parse_tools import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX from var_props import CCPP_LOOP_DIM_SUBSTS, VariableProperty, VarCompatObj from var_props import find_horizontal_dimension, find_vertical_dimension from var_props import standard_name_to_long_name, default_kind_val @@ -302,6 +303,8 @@ def __init__(self, prop_dict, source, run_env, context=None, if 'units' not in prop_dict: prop_dict['units'] = "" # end if + # DH* To investigate later: Why is the DDT type + # copied into the kind attribute? Can we remove this? prop_dict['kind'] = prop_dict['ddt_type'] del prop_dict['ddt_type'] self.__intrinsic = False @@ -975,6 +978,53 @@ def has_vertical_dimension(self, dims=None): # end if return find_vertical_dimension(vdims)[0] + def conditional(self, vdicts): + """Convert conditional expression from active attribute + (i.e. in standard name format) to local names based on vdict. + Return conditional and a list of variables needed to evaluate + the conditional. + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real',}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV).conditional([{}]) + ('.true.', []) + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'active' : 'False'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV).conditional([VarDictionary('bar', _MVAR_DUMMY_RUN_ENV, variables={})]) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + Exception: Cannot find variable 'false' for generating conditional for 'False' + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'active' : 'mom_gone'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV).conditional([ VarDictionary('bar', _MVAR_DUMMY_RUN_ENV, variables=[Var({'local_name' : 'bar', 'standard_name' : 'mom_home', 'units' : '', 'dimensions' : '()', 'type' : 'logical'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV)]) ]) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + Exception: Cannot find variable 'mom_gone' for generating conditional for 'mom_gone' + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'active' : 'mom_home'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV).conditional([ VarDictionary('bar', _MVAR_DUMMY_RUN_ENV, variables=[Var({'local_name' : 'bar', 'standard_name' : 'mom_home', 'units' : '', 'dimensions' : '()', 'type' : 'logical'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV)]) ])[0] + 'bar' + >>> len(Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'active' : 'mom_home'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV).conditional([ VarDictionary('bar', _MVAR_DUMMY_RUN_ENV, variables=[Var({'local_name' : 'bar', 'standard_name' : 'mom_home', 'units' : '', 'dimensions' : '()', 'type' : 'logical'}, ParseSource('vname', 'HOST', ParseContext()), _MVAR_DUMMY_RUN_ENV)]) ])[1]) + 1 + """ + + active = self.get_prop_value('active') + conditional = '' + vars_needed = [] + + # Find all words in the conditional, for each of them look + # for a matching standard name in the list of known variables + items = FORTRAN_CONDITIONAL_REGEX.findall(active) + for item in items: + item = item.lower() + if item in FORTRAN_CONDITIONAL_REGEX_WORDS: + conditional += item + else: + # Keep integers + try: + int(item) + conditional += item + except ValueError: + dvar = None + for vdict in vdicts: + dvar = vdict.find_variable(standard_name=item, any_scope=True) # or any_scope=False ? + if dvar: + break + if not dvar: + raise Exception(f"Cannot find variable '{item}' for generating conditional for '{active}'") + conditional += dvar.get_prop_value('local_name') + vars_needed.append(dvar) + return (conditional, vars_needed) + def write_def(self, outfile, indent, wdict, allocatable=False, dummy=False, add_intent=None, extra_space=0, public=False): """Write the definition line for the variable to . diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 4990da88..309bbda0 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -30,6 +30,7 @@ from xml_tools import find_schema_file, find_schema_version from xml_tools import read_xml_file, validate_xml_file from xml_tools import PrettyElementTree +from fortran_conditional import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX # pylint: enable=wrong-import-position __all__ = [ @@ -74,5 +75,7 @@ 'set_log_to_stdout', 'type_name', 'unique_standard_name', - 'validate_xml_file' + 'validate_xml_file', + 'FORTRAN_CONDITIONAL_REGEX_WORDS', + 'FORTRAN_CONDITIONAL_REGEX' ] diff --git a/scripts/parse_tools/fortran_conditional.py b/scripts/parse_tools/fortran_conditional.py new file mode 100755 index 00000000..b1ec7eeb --- /dev/null +++ b/scripts/parse_tools/fortran_conditional.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# + +"""Definitions to convert a conditional statement in the metadata, expressed in standard names, +into a Fortran conditional (used in an if statement), expressed in local names. +""" + +import re + +FORTRAN_CONDITIONAL_REGEX_WORDS = [' ', '(', ')', '==', '/=', '<=', '>=', '<', '>', '.eqv.', '.neqv.', + '.true.', '.false.', '.lt.', '.le.', '.eq.', '.ge.', '.gt.', '.ne.', + '.not.', '.and.', '.or.', '.xor.'] +FORTRAN_CONDITIONAL_REGEX = re.compile(r"[\w']+|" + "|".join([word.replace('(','\(').replace(')', '\)') for word in FORTRAN_CONDITIONAL_REGEX_WORDS])) diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py old mode 100644 new mode 100755 index b97c9ce6..96d6649c --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -897,7 +897,7 @@ def match_variable(self, var, run_env): # end if # end if # end if - return found_var, var_vdim, new_vdims, missing_vert, compat_obj + return found_var, dict_var, var_vdim, new_vdims, missing_vert, compat_obj def in_process_split(self): """Find out if we are in a process-split region""" @@ -1080,6 +1080,7 @@ def __init__(self, scheme_xml, context, parent, run_env): self.__lib = scheme_xml.get('lib', None) self.__has_vertical_dimension = False self.__group = None + self.__var_debug_checks = list() self.__forward_transforms = list() self.__reverse_transforms = list() super().__init__(name, context, parent, run_env, active_call_list=True) @@ -1150,8 +1151,13 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): vdims = var.get_dimensions() vintent = var.get_prop_value('intent') args = self.match_variable(var, self.run_env) - found, vert_dim, new_dims, missing_vert, compat_obj = args + found, dict_var, vert_dim, new_dims, missing_vert, compat_obj = args if found: + if self.__group.run_env.debug: + # Add variable allocation checks for group, suite and host variables + if dict_var: + self.add_var_debug_check(dict_var) + # end if if not self.has_vertical_dim: self.__has_vertical_dimension = vert_dim is not None # end if @@ -1222,6 +1228,276 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end if return scheme_mods + def add_var_debug_check(self, var): + """Add a debug check for a given variable var (host model variable, + suite variable or group module variable) for this scheme. + Return the variable and an associated dummy variable that is + managed by the group subroutine that calls the scheme, and + which is used to assign the scalar or the lower and upper bounds + of the array to if the intent is 'inout' or 'out'. + """ + # Get the basic attributes that decide whether we need + # to check the variable when we write the group + standard_name = var.get_prop_value('standard_name') + dimensions = var.get_dimensions() + active = var.get_prop_value('active') + var_dicts = [ self.__group.call_list ] + self.__group.suite_dicts() + + # If the variable isn't active, skip it + if active.lower() =='.false.': + return + # Also, if the variable is one of the CCPP error handling messages, skip it + # since it is defined as intent(out) and we can't do meaningful checks on it + elif standard_name == 'ccpp_error_code' or standard_name == 'ccpp_error_message': + return + # To perform allocation checks, we need to know all variables + # that are part of the 'active' attribute conditional and add + # it to the group's call list. + else: + (_, vars_needed) = var.conditional(var_dicts) + for var_needed in vars_needed: + self.update_group_call_list_variable(var_needed) + + # For scalars and arrays, need an internal_var variable (same kind and type) + # that we can assign the scalar or the lbound/ubound of the array to. + # We need to treat DDTs and variables with kind attributes slightly + # differently, and make sure there are no duplicate variables. We + # also need to assign a bogus standard name to these local variables. + vtype = var.get_prop_value('type') + if var.is_ddt(): + vkind = '' + units = '' + else: + vkind = var.get_prop_value('kind') + units = var.get_prop_value('units') + if vkind: + internal_var_lname = f'internal_var_{vtype.replace("=","_")}_{vkind.replace("=","_")}' + else: + internal_var_lname = f'internal_var_{vtype.replace("=","_")}' + if var.is_ddt(): + internal_var = Var({'local_name':internal_var_lname, 'standard_name':f'{internal_var_lname}_local', + 'ddt_type':vtype, 'kind':vkind, 'units':units, 'dimensions':'()'}, + _API_LOCAL, self.run_env) + else: + internal_var = Var({'local_name':internal_var_lname, 'standard_name':f'{internal_var_lname}_local', + 'type':vtype, 'kind':vkind, 'units':units, 'dimensions':'()'}, + _API_LOCAL, self.run_env) + found = self.__group.find_variable(source_var=internal_var, any_scope=False) + if not found: + self.__group.manage_variable(internal_var) + + # For arrays, we need to get information on the dimensions and add it to + # the group's call list so that we can test for the correct size later on + if dimensions: + for dim in dimensions: + if not ':' in dim: + dim_var = self.find_variable(standard_name=dim) + if not dim_var: + raise Exception(f"No dimension with standard name '{dim}'") + self.update_group_call_list_variable(dim_var) + else: + (ldim, udim) = dim.split(":") + ldim_var = self.find_variable(standard_name=ldim) + if not ldim_var: + raise Exception(f"No dimension with standard name '{ldim}'") + self.update_group_call_list_variable(ldim_var) + udim_var = self.find_variable(standard_name=udim) + if not udim_var: + raise Exception(f"No dimension with standard name '{udim}'") + self.update_group_call_list_variable(udim_var) + + # Add the variable to the list of variables to check. Record which internal_var to use. + self.__var_debug_checks.append([var, internal_var]) + + def replace_horiz_dim_debug_check(self, dim, cldicts, var_in_call_list): + """Determine the correct horizontal dimension to use for a given variable, + depending on the CCPP phase and origin of the variable (from the host/suite + or defined as a module variable for the parent group, or local to the group. + Return the dimension length and other properties needed for debug checks.""" + if not is_horizontal_dimension(dim): + raise Exception(f"Dimension {dim} is not a horizontal dimension") + if self.run_phase(): + if var_in_call_list and \ + self.find_variable(standard_name="horizontal_loop_extent"): + ldim = "ccpp_constant_one" + udim = "horizontal_loop_extent" + else: + ldim = "horizontal_loop_begin" + udim = "horizontal_loop_end" + else: + ldim = "ccpp_constant_one" + udim = "horizontal_dimension" + # Get dimension for lower bound + for var_dict in cldicts: + dvar = var_dict.find_variable(standard_name=ldim, any_scope=False) + if dvar is not None: + break + if not dvar: + raise Exception(f"No variable with standard name '{ldim}' in cldicts") + ldim_lname = dvar.get_prop_value('local_name') + # Get dimension for upper bound + for var_dict in cldicts: + dvar = var_dict.find_variable(standard_name=udim, any_scope=False) + if dvar is not None: + break + if not dvar: + raise Exception(f"No variable with standard name '{udim}' in cldicts") + udim_lname = dvar.get_prop_value('local_name') + # Assemble dimensions and bounds for size checking + dim_length = f'{udim_lname}-{ldim_lname}+1' + # If the variable that uses these dimensions is not in the group's call + # list, then it is defined as a module variable for this group and the + # dimensions run from ldim to udim, otherwise from 1:dim_length. + if not var_in_call_list: + dim_string = f"{ldim_lname}:{udim_lname}" + lbound_string = ldim_lname + ubound_string = udim_lname + else: + dim_string = ":" + lbound_string = '1' + ubound_string = f'{udim_lname}-{ldim_lname}+1' + return (dim_length, dim_string, lbound_string, ubound_string) + + def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, errmsg, indent): + """Write the variable debug check for the given variable, as determined + in a previous step (add_var_debug_check). Assign the scalar or lower and + upper bounds of the array to the internal_var variable, and for arrays also check + that the size of the array matches the dimensions from the metadata. + """ + # Get the basic attributes for writing the check + standard_name = var.get_prop_value('standard_name') + dimensions = var.get_dimensions() + active = var.get_prop_value('active') + allocatable = var.get_prop_value('allocatable') + + # Need the local name from the group call list, + # from the locally-defined variables of the group, + # or from the suite, not how it is called in the scheme (var) + # First, check if the variable is in the call list. + dvar = self.__group.call_list.find_variable(standard_name=standard_name, any_scope=False) + if dvar: + var_in_call_list = True + else: + var_in_call_list = False + # If it is not in the call list, try to find it + # in the local variables of this group subroutine. + dvar = self.__group.find_variable(standard_name=standard_name, any_scope=False) + if not dvar: + # This variable is handled by the group + # and is declared as a module variable + for var_dict in self.__group.suite_dicts(): + dvar = var_dict.find_variable(standard_name=standard_name, any_scope=False) + if dvar: + break + if not dvar: + raise Exception(f"No variable with standard name '{standard_name}' in cldicts") + local_name = dvar.get_prop_value('local_name') + + # If the variable is allocatable and the intent for the scheme is 'out', + # then we can't test anything because the scheme is going to allocate + # the variable. We don't have this information earlier in + # add_var_debug_check, therefore need to back out here, + # using the information from the scheme variable (call list). + svar = self.call_list.find_variable(standard_name=standard_name, any_scope=False) + intent = svar.get_prop_value('intent') + if intent == 'out' and allocatable: + return + + # Get the condition on which the variable is active + (conditional, _) = var.conditional(cldicts) + + # For scalars, assign to internal_var variable if the variable intent is in/inout + if not dimensions: + if not intent == 'out': + internal_var_lname = internal_var.get_prop_value('local_name') + outfile.write(f"if ({conditional}) then", indent) + outfile.write(f"! Assign value of {local_name} to internal_var", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}", indent+1) + outfile.write(f"end if", indent) + # For arrays, check size of array against dimensions in metadata, then assign + # the lower and upper bounds to the internal_var variable if the intent is in/inout + else: + array_size = 1 + dim_strings = [] + lbound_strings = [] + ubound_strings = [] + for dim in dimensions: + if not ':' in dim: + # In capgen, any true dimension (that is not a single index) does + # have a colon (:) in the dimension, therefore this is an index + for var_dict in cldicts: + dvar = var_dict.find_variable(standard_name=dim, any_scope=False) + if dvar is not None: + break + if not dvar: + raise Exception(f"No variable with standard name '{dim}' in cldicts") + dim_lname = dvar.get_prop_value('local_name') + dim_length = 1 + dim_strings.append(dim_lname) + lbound_strings.append(dim_lname) + ubound_strings.append(dim_lname) + else: + # Horizontal dimension needs to be dealt with separately, because it + # depends on the CCPP phase, whether the variable is a host/suite + # variable or locally defined on the group level. + if is_horizontal_dimension(dim): + (dim_length, dim_string, lbound_string, ubound_string) = \ + self.replace_horiz_dim_debug_check(dim, cldicts, var_in_call_list) + else: + (ldim, udim) = dim.split(":") + # Get dimension for lower bound + for var_dict in cldicts: + dvar = var_dict.find_variable(standard_name=ldim, any_scope=False) + if dvar is not None: + break + if not dvar: + raise Exception(f"No variable with standard name '{ldim}' in cldicts") + ldim_lname = dvar.get_prop_value('local_name') + # Get dimension for upper bound + for var_dict in cldicts: + dvar = var_dict.find_variable(standard_name=udim, any_scope=False) + if dvar is not None: + break + if not dvar: + raise Exception(f"No variable with standard name '{udim}' in cldicts") + udim_lname = dvar.get_prop_value('local_name') + # Assemble dimensions and bounds for size checking + dim_length = f'{udim_lname}-{ldim_lname}+1' + dim_string = ":" + lbound_string = ldim_lname + ubound_string = udim_lname + # end if + dim_strings.append(dim_string) + lbound_strings.append(lbound_string) + ubound_strings.append(ubound_string) + array_size = f'{array_size}*({dim_length})' + + # Various strings needed to get the right size + # and lower/upper bound of the array + dim_string = '(' + ','.join(dim_strings) + ')' + lbound_string = '(' + ','.join(lbound_strings) + ')' + ubound_string = '(' + ','.join(ubound_strings) + ')' + + # Write size check + outfile.write(f"if ({conditional}) then", indent) + outfile.write(f"! Check size of array {local_name}", indent+1) + outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent+1) + outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+2) + outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+2) + outfile.write(f"{errcode} = 1", indent+2) + outfile.write(f"return", indent+2) + outfile.write(f"end if", indent+1) + outfile.write(f"end if", indent) + + # Assign lower/upper bounds to internal_var (scalar) if intent is not out + if not intent == 'out': + internal_var_lname = internal_var.get_prop_value('local_name') + outfile.write(f"if ({conditional}) then", indent) + outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent+1) + outfile.write(f"end if", indent) + def add_var_transform(self, var, compat_obj, vert_dim): """Register any variable transformation needed by for this Scheme. For any transformation identified in , create dummy variable @@ -1297,7 +1573,7 @@ def write_var_transform(self, var, dummy, rindices, lindices, compat_obj, rvar_indices=lindices) outfile.write(stmt, indent+1) - def write(self, outfile, errcode, indent): + def write(self, outfile, errcode, errmsg, indent): # Unused arguments are for consistent write interface # pylint: disable=unused-argument """Write code to call this Scheme to """ @@ -1310,6 +1586,12 @@ def write(self, outfile, errcode, indent): subname=self.subroutine_name) outfile.write('if ({} == 0) then'.format(errcode), indent) + + # Write debug checks (operating on variables + # coming from the group's call list) + for (var, internal_var) in self.__var_debug_checks: + stmt = self.write_var_debug_check(var, internal_var, cldicts, outfile, errcode, errmsg, indent) + # Write any reverse (pre-Scheme) transforms. for (dummy, var, rindices, lindices, compat_obj) in self.__reverse_transforms: tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent, False) @@ -1435,13 +1717,13 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end for return scheme_mods - def write(self, outfile, errcode, indent): + def write(self, outfile, errcode, errmsg, indent): """Write code for the vertical loop, including contents, to """ outfile.write('do {} = 1, {}'.format(self.name, self.dimension_name), indent) # Note that 'scheme' may be a sybcycle or other construct for item in self.parts: - item.write(outfile, errcode, indent+1) + item.write(outfile, errcode, errmsg, indent+1) # end for outfile.write('end do', 2) @@ -1500,12 +1782,12 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end for return scheme_mods - def write(self, outfile, errcode, indent): + def write(self, outfile, errcode, errmsg, indent): """Write code for the subcycle loop, including contents, to """ outfile.write('do {} = 1, {}'.format(self.name, self.loop), indent) # Note that 'scheme' may be a sybcycle or other construct for item in self.parts: - item.write(outfile, errcode, indent+1) + item.write(outfile, errcode, errmsg, indent+1) # end for outfile.write('end do', 2) @@ -1550,11 +1832,11 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end for return scheme_mods - def write(self, outfile, errcode, indent): + def write(self, outfile, errcode, errmsg, indent): """Write code for this TimeSplit section, including contents, to """ for item in self.parts: - item.write(outfile, errcode, indent) + item.write(outfile, errcode, errmsg, indent) # end for ############################################################################### @@ -1579,7 +1861,7 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # Handle all the suite objects inside of this group raise CCPPError('ProcessSplit not yet implemented') - def write(self, outfile, errcode, indent): + def write(self, outfile, errcode, errmsg, indent): """Write code for this ProcessSplit section, including contents, to """ raise CCPPError('ProcessSplit not yet implemented') @@ -1978,7 +2260,7 @@ def write(self, outfile, host_arglist, indent, const_mod, # end for # Write the scheme and subcycle calls for item in self.parts: - item.write(outfile, errcode, indent + 1) + item.write(outfile, errcode, errmsg, indent + 1) # end for # Deallocate local arrays for lname in allocatable_var_set: diff --git a/scripts/var_props.py b/scripts/var_props.py index dc16fd0f..c61d9ae4 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -151,7 +151,7 @@ def standard_name_to_long_name(prop_dict, context=None): ... parse_source.CCPPError: No standard name to convert to long name, at foo.F90:4 """ - # We assume that standar_name has been checked for validity + # We assume that standard_name has been checked for validity # Make the first char uppercase and replace each underscore with a space if 'standard_name' in prop_dict: standard_name = prop_dict['standard_name'] diff --git a/test/advection_test/CMakeLists.txt b/test/advection_test/CMakeLists.txt index 10ada283..4aacdc18 100644 --- a/test/advection_test/CMakeLists.txt +++ b/test/advection_test/CMakeLists.txt @@ -149,6 +149,7 @@ while (VERBOSITY GREATER 0) list(APPEND CAPGEN_CMD "--verbose") MATH(EXPR VERBOSITY "${VERBOSITY} - 1") endwhile () +list(APPEND CAPGEN_CMD "--debug") string(REPLACE ";" " " CAPGEN_STRING "${CAPGEN_CMD}") MESSAGE(STATUS "Running: ${CAPGEN_STRING}") EXECUTE_PROCESS(COMMAND ${CAPGEN_CMD} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} diff --git a/test/advection_test/run_test b/test/advection_test/run_test index b014470a..9c75aa08 100755 --- a/test/advection_test/run_test +++ b/test/advection_test/run_test @@ -131,18 +131,23 @@ suite_list="cld_suite" required_vars="ccpp_error_code,ccpp_error_message" required_vars="${required_vars},cloud_ice_dry_mixing_ratio" required_vars="${required_vars},cloud_liquid_dry_mixing_ratio" +required_vars="${required_vars},horizontal_dimension" required_vars="${required_vars},horizontal_loop_begin" required_vars="${required_vars},horizontal_loop_end" required_vars="${required_vars},surface_air_pressure" required_vars="${required_vars},temperature" required_vars="${required_vars},time_step_for_physics" +required_vars="${required_vars},vertical_layer_dimension" required_vars="${required_vars},water_temperature_at_freezing" required_vars="${required_vars},water_vapor_specific_humidity" input_vars="cloud_ice_dry_mixing_ratio,cloud_liquid_dry_mixing_ratio" +input_vars="${input_vars},horizontal_dimension" input_vars="${input_vars},horizontal_loop_begin" input_vars="${input_vars},horizontal_loop_end" input_vars="${input_vars},surface_air_pressure,temperature" -input_vars="${input_vars},time_step_for_physics,water_temperature_at_freezing" +input_vars="${input_vars},time_step_for_physics" +input_vars="${input_vars},vertical_layer_dimension" +input_vars="${input_vars},water_temperature_at_freezing" input_vars="${input_vars},water_vapor_specific_humidity" output_vars="ccpp_error_code,ccpp_error_message" output_vars="${output_vars},cloud_ice_dry_mixing_ratio" diff --git a/test/advection_test/test_reports.py b/test/advection_test/test_reports.py index 17e9a2ad..6d84616b 100644 --- a/test/advection_test/test_reports.py +++ b/test/advection_test/test_reports.py @@ -72,13 +72,19 @@ def usage(errmsg=None): "time_step_for_physics", "water_temperature_at_freezing", "water_vapor_specific_humidity", "cloud_ice_dry_mixing_ratio", - "cloud_liquid_dry_mixing_ratio"] + "cloud_liquid_dry_mixing_ratio", + # Added by --debug option + "horizontal_dimension", + "vertical_layer_dimension"] _INPUT_VARS_CLD = ["surface_air_pressure", "temperature", "horizontal_loop_begin", "horizontal_loop_end", "time_step_for_physics", "water_temperature_at_freezing", "water_vapor_specific_humidity", "cloud_ice_dry_mixing_ratio", - "cloud_liquid_dry_mixing_ratio"] + "cloud_liquid_dry_mixing_ratio", + # Added by --debug option + "horizontal_dimension", + "vertical_layer_dimension"] _OUTPUT_VARS_CLD = ["ccpp_error_code", "ccpp_error_message", "water_vapor_specific_humidity", "temperature", "cloud_ice_dry_mixing_ratio", diff --git a/test/capgen_test/CMakeLists.txt b/test/capgen_test/CMakeLists.txt index f02213a0..ccae4f08 100644 --- a/test/capgen_test/CMakeLists.txt +++ b/test/capgen_test/CMakeLists.txt @@ -149,6 +149,7 @@ while (VERBOSITY GREATER 0) list(APPEND CAPGEN_CMD "--verbose") MATH(EXPR VERBOSITY "${VERBOSITY} - 1") endwhile () +list(APPEND CAPGEN_CMD "--debug") string(REPLACE ";" " " CAPGEN_STRING "${CAPGEN_CMD}") MESSAGE(STATUS "Running: ${CAPGEN_STRING}") EXECUTE_PROCESS(COMMAND ${CAPGEN_CMD} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} diff --git a/test/capgen_test/run_test b/test/capgen_test/run_test index 4dfd490f..0d5d44f7 100755 --- a/test/capgen_test/run_test +++ b/test/capgen_test/run_test @@ -148,20 +148,24 @@ output_vars_ddt="${output_vars_ddt},model_times,number_of_model_times" required_vars_temp="ccpp_error_code,ccpp_error_message,horizontal_dimension" required_vars_temp="${required_vars_temp},horizontal_loop_begin" required_vars_temp="${required_vars_temp},horizontal_loop_end" +required_vars_temp="${required_vars_temp},index_of_water_vapor_specific_humidity" required_vars_temp="${required_vars_temp},potential_temperature" required_vars_temp="${required_vars_temp},potential_temperature_at_interface" required_vars_temp="${required_vars_temp},potential_temperature_increment" required_vars_temp="${required_vars_temp},surface_air_pressure" required_vars_temp="${required_vars_temp},time_step_for_physics" +required_vars_temp="${required_vars_temp},vertical_interface_dimension" required_vars_temp="${required_vars_temp},vertical_layer_dimension" required_vars_temp="${required_vars_temp},water_vapor_specific_humidity" input_vars_temp="horizontal_dimension" input_vars_temp="${input_vars_temp},horizontal_loop_begin" input_vars_temp="${input_vars_temp},horizontal_loop_end" +input_vars_temp="${input_vars_temp},index_of_water_vapor_specific_humidity" input_vars_temp="${input_vars_temp},potential_temperature" input_vars_temp="${input_vars_temp},potential_temperature_at_interface" input_vars_temp="${input_vars_temp},potential_temperature_increment" input_vars_temp="${input_vars_temp},surface_air_pressure,time_step_for_physics" +input_vars_temp="${input_vars_temp},vertical_interface_dimension" input_vars_temp="${input_vars_temp},vertical_layer_dimension" input_vars_temp="${input_vars_temp},water_vapor_specific_humidity" output_vars_temp="ccpp_error_code,ccpp_error_message,potential_temperature" diff --git a/test/capgen_test/test_host_data.meta b/test/capgen_test/test_host_data.meta index 8a54a3ed..df4b92b4 100644 --- a/test/capgen_test/test_host_data.meta +++ b/test/capgen_test/test_host_data.meta @@ -49,3 +49,4 @@ kind = kind_phys units = kg kg-1 dimensions = (horizontal_dimension, vertical_layer_dimension) + active = (index_of_water_vapor_specific_humidity > 0) diff --git a/test/capgen_test/test_reports.py b/test/capgen_test/test_reports.py index e8237476..da02aeea 100644 --- a/test/capgen_test/test_reports.py +++ b/test/capgen_test/test_reports.py @@ -78,7 +78,10 @@ def usage(errmsg=None): "number_of_model_times"] _REQUIRED_VARS_DDT = _INPUT_VARS_DDT + _OUTPUT_VARS_DDT _PROT_VARS_TEMP = ["horizontal_loop_begin", "horizontal_loop_end", - "horizontal_dimension", "vertical_layer_dimension"] + "horizontal_dimension", "vertical_layer_dimension", + # Added for --debug + "index_of_water_vapor_specific_humidity", + "vertical_interface_dimension"] _REQUIRED_VARS_TEMP = ["ccpp_error_code", "ccpp_error_message", "potential_temperature", "potential_temperature_at_interface", diff --git a/test/run_fortran_tests.sh b/test/run_fortran_tests.sh index cd6436fb..ccfc85f9 100755 --- a/test/run_fortran_tests.sh +++ b/test/run_fortran_tests.sh @@ -37,12 +37,12 @@ if [ $res -ne 0 ]; then echo "Failure running advection test" fi -# Run var_compatability test - ./var_compatability_test/run_test +# Run var_compatibility test + ./var_compatibility_test/run_test res=$? errcnt=$((errcnt + res)) if [ $res -ne 0 ]; then - echo "Failure running var_compatability test" + echo "Failure running var_compatibility test" fi if [ $errcnt -eq 0 ]; then diff --git a/test/var_compatability_test/.gitignore b/test/var_compatibility_test/.gitignore similarity index 100% rename from test/var_compatability_test/.gitignore rename to test/var_compatibility_test/.gitignore diff --git a/test/var_compatability_test/CMakeLists.txt b/test/var_compatibility_test/CMakeLists.txt similarity index 98% rename from test/var_compatability_test/CMakeLists.txt rename to test/var_compatibility_test/CMakeLists.txt index 7e3590a6..8cbd7e44 100644 --- a/test/var_compatability_test/CMakeLists.txt +++ b/test/var_compatibility_test/CMakeLists.txt @@ -19,9 +19,9 @@ get_filename_component(CCPP_ROOT "${TEST_ROOT}" DIRECTORY) # Paths should be relative to CMAKE_SOURCE_DIR (this file's directory) # #------------------------------------------------------------------------------ -LIST(APPEND SCHEME_FILES "var_compatability_files.txt") +LIST(APPEND SCHEME_FILES "var_compatibility_files.txt") LIST(APPEND HOST_FILES "test_host_data" "test_host_mod") -LIST(APPEND SUITE_FILES "var_compatability_suite.xml") +LIST(APPEND SUITE_FILES "var_compatibility_suite.xml") # HOST is the name of the executable we will build. # We assume there are files ${HOST}.meta and ${HOST}.F90 in CMAKE_SOURCE_DIR SET(HOST "${CMAKE_PROJECT_NAME}") @@ -149,6 +149,7 @@ while (VERBOSITY GREATER 0) list(APPEND CAPGEN_CMD "--verbose") MATH(EXPR VERBOSITY "${VERBOSITY} - 1") endwhile () +list(APPEND CAPGEN_CMD "--debug") string(REPLACE ";" " " CAPGEN_STRING "${CAPGEN_CMD}") MESSAGE(STATUS "Running: ${CAPGEN_STRING}") EXECUTE_PROCESS(COMMAND ${CAPGEN_CMD} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} diff --git a/test/var_compatability_test/README.md b/test/var_compatibility_test/README.md similarity index 58% rename from test/var_compatability_test/README.md rename to test/var_compatibility_test/README.md index 066cf771..9b56ec9c 100644 --- a/test/var_compatability_test/README.md +++ b/test/var_compatibility_test/README.md @@ -1,6 +1,6 @@ -var_compatability test +var_compatibility test ================ -To build and run the var_compatability test, run ./run_test +To build and run the var_compatibility test, run ./run_test This script will build and run the test. The exit code is zero (0) on PASS and non-zero on FAIL. diff --git a/test/var_compatability_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 similarity index 100% rename from test/var_compatability_test/effr_calc.F90 rename to test/var_compatibility_test/effr_calc.F90 diff --git a/test/var_compatability_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta similarity index 100% rename from test/var_compatability_test/effr_calc.meta rename to test/var_compatibility_test/effr_calc.meta diff --git a/test/var_compatability_test/run_test b/test/var_compatibility_test/run_test similarity index 79% rename from test/var_compatability_test/run_test rename to test/var_compatibility_test/run_test index ca185966..ac86bf6f 100755 --- a/test/var_compatability_test/run_test +++ b/test/var_compatibility_test/run_test @@ -118,36 +118,38 @@ frame_src="${framework}/src" ## NB: This has to be after build_dir is finalized ## host_files="${build_dir}/ccpp/test_host_ccpp_cap.F90" -suite_files="${build_dir}/ccpp/ccpp_var_compatability_suite_cap.F90" +suite_files="${build_dir}/ccpp/ccpp_var_compatibility_suite_cap.F90" utility_files="${build_dir}/ccpp/ccpp_kinds.F90" utility_files="${utility_files},${frame_src}/ccpp_constituent_prop_mod.F90" utility_files="${utility_files},${frame_src}/ccpp_hashable.F90" utility_files="${utility_files},${frame_src}/ccpp_hash_table.F90" ccpp_files="${utility_files}" ccpp_files="${ccpp_files},${build_dir}/ccpp/test_host_ccpp_cap.F90" -ccpp_files="${ccpp_files},${build_dir}/ccpp/ccpp_var_compatability_suite_cap.F90" +ccpp_files="${ccpp_files},${build_dir}/ccpp/ccpp_var_compatibility_suite_cap.F90" #process_list="" module_list="effr_calc" #dependencies="" -suite_list="var_compatability_suite" -required_vars_var_compatability="ccpp_error_code,ccpp_error_message" -required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_ice_particle" -required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_liquid_water_particle" -required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_rain_particle" -required_vars_var_compatability="${required_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" -required_vars_var_compatability="${required_vars_var_compatability},horizontal_loop_begin" -required_vars_var_compatability="${required_vars_var_compatability},horizontal_loop_end" -required_vars_var_compatability="${required_vars_var_compatability},vertical_layer_dimension" -input_vars_var_compatability="effective_radius_of_stratiform_cloud_liquid_water_particle" -input_vars_var_compatability="${input_vars_var_compatability},effective_radius_of_stratiform_cloud_rain_particle" -input_vars_var_compatability="${input_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" -input_vars_var_compatability="${input_vars_var_compatability},horizontal_loop_begin" -input_vars_var_compatability="${input_vars_var_compatability},horizontal_loop_end" -input_vars_var_compatability="${input_vars_var_compatability},vertical_layer_dimension" -output_vars_var_compatability="ccpp_error_code,ccpp_error_message" -output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_ice_particle" -output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_liquid_water_particle" -output_vars_var_compatability="${output_vars_var_compatability},effective_radius_of_stratiform_cloud_snow_particle" +suite_list="var_compatibility_suite" +required_vars_var_compatibility="ccpp_error_code,ccpp_error_message" +required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" +required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" +required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" +required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" +required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_dimension" +required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_begin" +required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_end" +required_vars_var_compatibility="${required_vars_var_compatibility},vertical_layer_dimension" +input_vars_var_compatibility="effective_radius_of_stratiform_cloud_liquid_water_particle" +input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" +input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" +input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_dimension" +input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_begin" +input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_end" +input_vars_var_compatibility="${input_vars_var_compatibility},vertical_layer_dimension" +output_vars_var_compatibility="ccpp_error_code,ccpp_error_message" +output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" +output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" +output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" ## ## Run a database report and check the return string @@ -215,13 +217,13 @@ check_datatable ${report_prog} ${datafile} "--module-list" ${module_list} #check_datatable ${report_prog} ${datafile} "--dependencies" ${dependencies} check_datatable ${report_prog} ${datafile} "--suite-list" ${suite_list} \ --sep ";" -echo -e "\nChecking variables for var_compatability suite from command line" +echo -e "\nChecking variables for var_compatibility suite from command line" check_datatable ${report_prog} ${datafile} "--required-variables" \ - ${required_vars_var_compatability} "var_compatability_suite" + ${required_vars_var_compatibility} "var_compatibility_suite" check_datatable ${report_prog} ${datafile} "--input-variables" \ - ${input_vars_var_compatability} "var_compatability_suite" + ${input_vars_var_compatibility} "var_compatibility_suite" check_datatable ${report_prog} ${datafile} "--output-variables" \ - ${output_vars_var_compatability} "var_compatability_suite" + ${output_vars_var_compatibility} "var_compatibility_suite" # Run make make res=$? diff --git a/test/var_compatability_test/test_host.F90 b/test/var_compatibility_test/test_host.F90 similarity index 99% rename from test/var_compatability_test/test_host.F90 rename to test/var_compatibility_test/test_host.F90 index 6de9597c..25129405 100644 --- a/test/var_compatability_test/test_host.F90 +++ b/test/var_compatibility_test/test_host.F90 @@ -375,7 +375,7 @@ program test logical :: run_okay ! Setup expected test suite info - test_suites(1)%suite_name = 'var_compatability_suite' + test_suites(1)%suite_name = 'var_compatibility_suite' test_suites(1)%suite_parts => test_parts1 test_suites(1)%suite_input_vars => test_invars1 test_suites(1)%suite_output_vars => test_outvars1 diff --git a/test/var_compatability_test/test_host.meta b/test/var_compatibility_test/test_host.meta similarity index 100% rename from test/var_compatability_test/test_host.meta rename to test/var_compatibility_test/test_host.meta diff --git a/test/var_compatability_test/test_host_data.F90 b/test/var_compatibility_test/test_host_data.F90 similarity index 100% rename from test/var_compatability_test/test_host_data.F90 rename to test/var_compatibility_test/test_host_data.F90 diff --git a/test/var_compatability_test/test_host_data.meta b/test/var_compatibility_test/test_host_data.meta similarity index 100% rename from test/var_compatability_test/test_host_data.meta rename to test/var_compatibility_test/test_host_data.meta diff --git a/test/var_compatability_test/test_host_mod.F90 b/test/var_compatibility_test/test_host_mod.F90 similarity index 100% rename from test/var_compatability_test/test_host_mod.F90 rename to test/var_compatibility_test/test_host_mod.F90 diff --git a/test/var_compatability_test/test_host_mod.meta b/test/var_compatibility_test/test_host_mod.meta similarity index 100% rename from test/var_compatability_test/test_host_mod.meta rename to test/var_compatibility_test/test_host_mod.meta diff --git a/test/var_compatability_test/test_reports.py b/test/var_compatibility_test/test_reports.py similarity index 95% rename from test/var_compatability_test/test_reports.py rename to test/var_compatibility_test/test_reports.py index a8dc6eea..4c8f9da6 100755 --- a/test/var_compatability_test/test_reports.py +++ b/test/var_compatibility_test/test_reports.py @@ -57,17 +57,17 @@ def usage(errmsg=None): # Check data _HOST_FILES = [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90")] -_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatability_suite_cap.F90")] +_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] _UTILITY_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_kinds.F90"), os.path.join(_SRC_DIR, "ccpp_constituent_prop_mod.F90"), os.path.join(_SRC_DIR, "ccpp_hashable.F90"), os.path.join(_SRC_DIR, "ccpp_hash_table.F90")] _CCPP_FILES = _UTILITY_FILES + \ [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), - os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatability_suite_cap.F90")] + os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] _MODULE_LIST = ["effr_calc"] -_SUITE_LIST = ["var_compatability_suite"] -_INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "vertical_layer_dimension", +_SUITE_LIST = ["var_compatibility_suite"] +_INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "horizontal_dimension", "vertical_layer_dimension", "effective_radius_of_stratiform_cloud_liquid_water_particle", "effective_radius_of_stratiform_cloud_rain_particle", "effective_radius_of_stratiform_cloud_snow_particle"] @@ -140,15 +140,15 @@ def check_datatable(database, report_type, check_list, _MODULE_LIST) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_list"), _SUITE_LIST) -print("\nChecking variables for var_compatability suite from python") +print("\nChecking variables for var_compatibility suite from python") NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("required_variables", - value="var_compatability_suite"), + value="var_compatibility_suite"), _REQUIRED_VARS_VAR_ACTION) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("input_variables", - value="var_compatability_suite"), + value="var_compatibility_suite"), _INPUT_VARS_VAR_ACTION) NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("output_variables", - value="var_compatability_suite"), + value="var_compatibility_suite"), _OUTPUT_VARS_VAR_ACTION) sys.exit(NUM_ERRORS) diff --git a/test/var_compatability_test/var_compatability_files.txt b/test/var_compatibility_test/var_compatibility_files.txt similarity index 100% rename from test/var_compatability_test/var_compatability_files.txt rename to test/var_compatibility_test/var_compatibility_files.txt diff --git a/test/var_compatability_test/var_compatability_suite.xml b/test/var_compatibility_test/var_compatibility_suite.xml similarity index 69% rename from test/var_compatability_test/var_compatability_suite.xml rename to test/var_compatibility_test/var_compatibility_suite.xml index ae75d9f7..4b70fe48 100644 --- a/test/var_compatability_test/var_compatability_suite.xml +++ b/test/var_compatibility_test/var_compatibility_suite.xml @@ -1,6 +1,6 @@ - + effr_calc diff --git a/test_prebuild/test_blocked_data/README.md b/test_prebuild/test_blocked_data/README.md index ad977913..8802e812 100644 --- a/test_prebuild/test_blocked_data/README.md +++ b/test_prebuild/test_blocked_data/README.md @@ -10,4 +10,5 @@ mkdir build cd build cmake .. 2>&1 | tee log.cmake make 2>&1 | tee log.make +./test_blocked_data.x ``` From 73b61387205d09a829ff8502c9a5059cb57c8c30 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Thu, 25 Jan 2024 22:18:31 +0000 Subject: [PATCH 41/49] Expand var compatibility test for active attribute --- test/var_compatibility_test/effr_calc.F90 | 5 ++++- test/var_compatibility_test/effr_calc.meta | 8 ++++++++ test/var_compatibility_test/run_test | 6 +++++- test/var_compatibility_test/test_host.F90 | 12 ++++++++---- test/var_compatibility_test/test_host_data.F90 | 13 +++++++++++-- test/var_compatibility_test/test_host_data.meta | 8 ++++++++ test/var_compatibility_test/test_host_mod.F90 | 6 +++++- test/var_compatibility_test/test_host_mod.meta | 6 ++++++ test/var_compatibility_test/test_reports.py | 4 +++- 9 files changed, 58 insertions(+), 10 deletions(-) diff --git a/test/var_compatibility_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 index d01408c6..4d5086ec 100644 --- a/test/var_compatibility_test/effr_calc.F90 +++ b/test/var_compatibility_test/effr_calc.F90 @@ -15,12 +15,13 @@ module effr_calc !> \section arg_table_effr_calc_run Argument Table !! \htmlinclude arg_table_effr_calc_run.html !! - subroutine effr_calc_run(ncol, nlev, effrr_in, effrl_inout, & + subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & effri_out, effrs_inout, errmsg, errflg) integer, intent(in) :: ncol integer, intent(in) :: nlev real(kind_phys), intent(in) :: effrr_in(:,:) + real(kind_phys), intent(in) :: effrg_in(:,:) real(kind_phys), intent(inout) :: effrl_inout(:,:) real(kind_phys), intent(out) :: effri_out(:,:) real(8),intent(inout) :: effrs_inout(:,:) @@ -32,11 +33,13 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrl_inout, & real(kind_phys), parameter :: re_qc_max = 50. ! microns real(kind_phys), parameter :: re_qi_avg = 75. ! microns real(kind_phys) :: effrr_local(ncol,nlev) + real(kind_phys) :: effrg_local(ncol,nlev) errmsg = '' errflg = 0 effrr_local = effrr_in + effrg_local = effrg_in effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) effri_out = re_qi_avg effrs_inout = effrs_inout + 10.0 ! in micrometer diff --git a/test/var_compatibility_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta index 78243ce9..19e19dff 100644 --- a/test/var_compatibility_test/effr_calc.meta +++ b/test/var_compatibility_test/effr_calc.meta @@ -26,6 +26,14 @@ kind = kind_phys intent = in top_at_one = .true. +[effrg_in] + standard_name = effective_radius_of_stratiform_cloud_graupel + long_name = effective radius of cloud graupel in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in [effrl_inout] standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle long_name = effective radius of cloud liquid water particle in micrometer diff --git a/test/var_compatibility_test/run_test b/test/var_compatibility_test/run_test index ac86bf6f..9239c5d1 100755 --- a/test/var_compatibility_test/run_test +++ b/test/var_compatibility_test/run_test @@ -131,17 +131,21 @@ module_list="effr_calc" #dependencies="" suite_list="var_compatibility_suite" required_vars_var_compatibility="ccpp_error_code,ccpp_error_message" +required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_graupel" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" +required_vars_var_compatibility="${required_vars_var_compatibility},flag_indicating_cloud_microphysics_has_graupel" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_dimension" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_begin" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_end" required_vars_var_compatibility="${required_vars_var_compatibility},vertical_layer_dimension" -input_vars_var_compatibility="effective_radius_of_stratiform_cloud_liquid_water_particle" +input_vars_var_compatibility="effective_radius_of_stratiform_cloud_graupel" +input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" +input_vars_var_compatibility="${input_vars_var_compatibility},flag_indicating_cloud_microphysics_has_graupel" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_dimension" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_begin" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_end" diff --git a/test/var_compatibility_test/test_host.F90 b/test/var_compatibility_test/test_host.F90 index 25129405..12215bd1 100644 --- a/test/var_compatibility_test/test_host.F90 +++ b/test/var_compatibility_test/test_host.F90 @@ -351,10 +351,12 @@ program test character(len=cs), target :: test_parts1(1) = (/ 'radiation ' /) - character(len=cm), target :: test_invars1(3) = (/ & + character(len=cm), target :: test_invars1(5) = (/ & 'effective_radius_of_stratiform_cloud_rain_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & - 'effective_radius_of_stratiform_cloud_snow_particle ' /) + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'effective_radius_of_stratiform_cloud_graupel ', & + 'flag_indicating_cloud_microphysics_has_graupel '/) character(len=cm), target :: test_outvars1(5) = (/ & 'ccpp_error_code ', & @@ -363,13 +365,15 @@ program test 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ' /) - character(len=cm), target :: test_reqvars1(6) = (/ & + character(len=cm), target :: test_reqvars1(8) = (/ & 'ccpp_error_code ', & 'ccpp_error_message ', & 'effective_radius_of_stratiform_cloud_rain_particle ', & 'effective_radius_of_stratiform_cloud_ice_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & - 'effective_radius_of_stratiform_cloud_snow_particle ' /) + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'effective_radius_of_stratiform_cloud_graupel ', & + 'flag_indicating_cloud_microphysics_has_graupel '/) type(suite_info) :: test_suites(1) logical :: run_okay diff --git a/test/var_compatibility_test/test_host_data.F90 b/test/var_compatibility_test/test_host_data.F90 index 9a17eb4f..b17fb916 100644 --- a/test/var_compatibility_test/test_host_data.F90 +++ b/test/var_compatibility_test/test_host_data.F90 @@ -8,17 +8,19 @@ module test_host_data real(kind_phys), dimension(:,:), allocatable :: & effrr, & ! effective radius of cloud rain effrl, & ! effective radius of cloud liquid water - effri ! effective radius of cloud ice + effri, & ! effective radius of cloud ice + effrg ! effective radius of cloud graupel end type physics_state public allocate_physics_state contains - subroutine allocate_physics_state(cols, levels, state) + subroutine allocate_physics_state(cols, levels, state, has_graupel) integer, intent(in) :: cols integer, intent(in) :: levels type(physics_state), intent(out) :: state + logical, intent(in) :: has_graupel if (allocated(state%effrr)) then deallocate(state%effrr) @@ -35,6 +37,13 @@ subroutine allocate_physics_state(cols, levels, state) end if allocate(state%effri(cols, levels)) + if (has_graupel) then + if (allocated(state%effrg)) then + deallocate(state%effrg) + end if + allocate(state%effrg(cols, levels)) + endif + end subroutine allocate_physics_state end module test_host_data diff --git a/test/var_compatibility_test/test_host_data.meta b/test/var_compatibility_test/test_host_data.meta index 4a98245f..9916d478 100644 --- a/test/var_compatibility_test/test_host_data.meta +++ b/test/var_compatibility_test/test_host_data.meta @@ -25,3 +25,11 @@ dimensions = (horizontal_dimension,vertical_layer_dimension) type = real kind = kind_phys +[effrg] + standard_name = effective_radius_of_stratiform_cloud_graupel + long_name = effective radius of cloud graupel in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + active = (flag_indicating_cloud_microphysics_has_graupel) \ No newline at end of file diff --git a/test/var_compatibility_test/test_host_mod.F90 b/test/var_compatibility_test/test_host_mod.F90 index b0501c0c..2761d9fa 100644 --- a/test/var_compatibility_test/test_host_mod.F90 +++ b/test/var_compatibility_test/test_host_mod.F90 @@ -13,6 +13,7 @@ module test_host_mod integer, parameter :: pver = 4 type(physics_state) :: phys_state real(kind_phys) :: effrs(ncols, pver) + logical, parameter :: mp_has_graupel = .true. public :: init_data public :: compare_data @@ -22,11 +23,14 @@ module test_host_mod subroutine init_data() ! Allocate and initialize state - call allocate_physics_state(ncols, pver, phys_state) + call allocate_physics_state(ncols, pver, phys_state, mp_has_graupel) phys_state%effrr = 1.0E-3 ! 1000 microns, in meter phys_state%effrl = 1.0E-4 ! 100 microns, in meter phys_state%effri = 5.0E-5 ! 50 microns, in meter effrs = 5.0E-4 ! 500 microns, in meter + if (mp_has_graupel) then + phys_state%effrg = 2.5E-4 ! 250 microns, in meter + endif end subroutine init_data diff --git a/test/var_compatibility_test/test_host_mod.meta b/test/var_compatibility_test/test_host_mod.meta index 0aff8e2f..852b95a0 100644 --- a/test/var_compatibility_test/test_host_mod.meta +++ b/test/var_compatibility_test/test_host_mod.meta @@ -28,3 +28,9 @@ dimensions = (horizontal_dimension,vertical_layer_dimension) type = real kind = kind_phys +[mp_has_graupel] + standard_name = flag_indicating_cloud_microphysics_has_graupel + long_name = flag indicating that the cloud microphysics produces graupel + units = flag + dimensions = () + type = logical \ No newline at end of file diff --git a/test/var_compatibility_test/test_reports.py b/test/var_compatibility_test/test_reports.py index 4c8f9da6..a56457bc 100755 --- a/test/var_compatibility_test/test_reports.py +++ b/test/var_compatibility_test/test_reports.py @@ -70,7 +70,9 @@ def usage(errmsg=None): _INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "horizontal_dimension", "vertical_layer_dimension", "effective_radius_of_stratiform_cloud_liquid_water_particle", "effective_radius_of_stratiform_cloud_rain_particle", - "effective_radius_of_stratiform_cloud_snow_particle"] + "effective_radius_of_stratiform_cloud_snow_particle", + "effective_radius_of_stratiform_cloud_graupel", + "flag_indicating_cloud_microphysics_has_graupel"] _OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", "effective_radius_of_stratiform_cloud_ice_particle", "effective_radius_of_stratiform_cloud_liquid_water_particle", From b1454853fbac665dac7038e6055aef66f9a114d8 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Thu, 25 Jan 2024 22:47:28 +0000 Subject: [PATCH 42/49] Address reviewers comments --- test/var_compatibility_test/effr_calc.F90 | 5 +++-- test/var_compatibility_test/effr_calc.meta | 7 +++++++ test/var_compatibility_test/test_host_data.meta | 2 +- test/var_compatibility_test/test_host_mod.meta | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/var_compatibility_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 index 4d5086ec..b877e99a 100644 --- a/test/var_compatibility_test/effr_calc.F90 +++ b/test/var_compatibility_test/effr_calc.F90 @@ -16,7 +16,7 @@ module effr_calc !! \htmlinclude arg_table_effr_calc_run.html !! subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & - effri_out, effrs_inout, errmsg, errflg) + effri_out, effrs_inout, has_graupel, errmsg, errflg) integer, intent(in) :: ncol integer, intent(in) :: nlev @@ -25,6 +25,7 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & real(kind_phys), intent(inout) :: effrl_inout(:,:) real(kind_phys), intent(out) :: effri_out(:,:) real(8),intent(inout) :: effrs_inout(:,:) + logical, intent(in) :: has_graupel character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -39,7 +40,7 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & errflg = 0 effrr_local = effrr_in - effrg_local = effrg_in + if (has_graupel) effrg_local = effrg_in effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) effri_out = re_qi_avg effrs_inout = effrs_inout + 10.0 ! in micrometer diff --git a/test/var_compatibility_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta index 19e19dff..14687f21 100644 --- a/test/var_compatibility_test/effr_calc.meta +++ b/test/var_compatibility_test/effr_calc.meta @@ -59,6 +59,13 @@ kind = 8 intent = inout top_at_one = .true. +[has_graupel] + standard_name = flag_indicating_cloud_microphysics_has_graupel + long_name = flag indicating that the cloud microphysics produces graupel + units = flag + dimensions = () + type = logical + intent = in [ errmsg ] standard_name = ccpp_error_message long_name = Error message for error handling in CCPP diff --git a/test/var_compatibility_test/test_host_data.meta b/test/var_compatibility_test/test_host_data.meta index 9916d478..1d29572c 100644 --- a/test/var_compatibility_test/test_host_data.meta +++ b/test/var_compatibility_test/test_host_data.meta @@ -32,4 +32,4 @@ dimensions = (horizontal_dimension,vertical_layer_dimension) type = real kind = kind_phys - active = (flag_indicating_cloud_microphysics_has_graupel) \ No newline at end of file + active = (flag_indicating_cloud_microphysics_has_graupel) diff --git a/test/var_compatibility_test/test_host_mod.meta b/test/var_compatibility_test/test_host_mod.meta index 852b95a0..7789dbed 100644 --- a/test/var_compatibility_test/test_host_mod.meta +++ b/test/var_compatibility_test/test_host_mod.meta @@ -33,4 +33,4 @@ long_name = flag indicating that the cloud microphysics produces graupel units = flag dimensions = () - type = logical \ No newline at end of file + type = logical From c77194065644f32b22db6d09265fe3ad8b00a617 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Wed, 31 Jan 2024 18:01:56 +0000 Subject: [PATCH 43/49] Make prettier auto generated code --- scripts/suite_objects.py | 79 ++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 96d6649c..c32035e8 100755 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1410,10 +1410,17 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er if not dimensions: if not intent == 'out': internal_var_lname = internal_var.get_prop_value('local_name') - outfile.write(f"if ({conditional}) then", indent) - outfile.write(f"! Assign value of {local_name} to internal_var", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}", indent+1) - outfile.write(f"end if", indent) + if conditional != '.true.': + outfile.write(f"if {conditional} then", indent) + outfile.write(f"! Assign value of {local_name} to internal_var", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}", indent+1) + outfile.write(f"end if", indent) + outfile.write('',indent) + else: + outfile.write(f"! Assign value of {local_name} to internal_var", indent) + outfile.write(f"{internal_var_lname} = {local_name}", indent) + outfile.write('',indent) + # endif # For arrays, check size of array against dimensions in metadata, then assign # the lower and upper bounds to the internal_var variable if the intent is in/inout else: @@ -1479,24 +1486,44 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er ubound_string = '(' + ','.join(ubound_strings) + ')' # Write size check - outfile.write(f"if ({conditional}) then", indent) - outfile.write(f"! Check size of array {local_name}", indent+1) - outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent+1) - outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+2) - outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+2) - outfile.write(f"{errcode} = 1", indent+2) - outfile.write(f"return", indent+2) - outfile.write(f"end if", indent+1) - outfile.write(f"end if", indent) + if conditional != '.true.': + outfile.write(f"if {conditional} then", indent) + outfile.write(f"! Check size of array {local_name}", indent+1) + outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent+1) + outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+2) + outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+2) + outfile.write(f"{errcode} = 1", indent+2) + outfile.write(f"return", indent+2) + outfile.write(f"end if", indent+1) + outfile.write(f"end if", indent) + outfile.write('',indent) + else: + outfile.write(f"! Check size of array {local_name}", indent) + outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent) + outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+1) + outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+1) + outfile.write(f"{errcode} = 1", indent+1) + outfile.write(f"return", indent+1) + outfile.write(f"end if", indent) + outfile.write('',indent) + # end if # Assign lower/upper bounds to internal_var (scalar) if intent is not out if not intent == 'out': internal_var_lname = internal_var.get_prop_value('local_name') - outfile.write(f"if ({conditional}) then", indent) - outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent+1) - outfile.write(f"end if", indent) + if conditional != '.true.': + outfile.write(f"if {conditional} then", indent) + outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent+1) + outfile.write(f"end if", indent) + outfile.write('',indent) + else: + outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent) + outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent) + outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent) + outfile.write('',indent) + # end if def add_var_transform(self, var, compat_obj, vert_dim): """Register any variable transformation needed by for this Scheme. @@ -1571,7 +1598,7 @@ def write_var_transform(self, var, dummy, rindices, lindices, compat_obj, rvar_lname=dummy, lvar_indices=rindices, rvar_indices=lindices) - outfile.write(stmt, indent+1) + outfile.write(stmt, indent) def write(self, outfile, errcode, errmsg, indent): # Unused arguments are for consistent write interface @@ -1585,23 +1612,29 @@ def write(self, outfile, errcode, errmsg, indent): is_func_call=True, subname=self.subroutine_name) + outfile.write('', indent) outfile.write('if ({} == 0) then'.format(errcode), indent) # Write debug checks (operating on variables # coming from the group's call list) for (var, internal_var) in self.__var_debug_checks: - stmt = self.write_var_debug_check(var, internal_var, cldicts, outfile, errcode, errmsg, indent) - + stmt = self.write_var_debug_check(var, internal_var, cldicts, outfile, errcode, errmsg, indent+1) # Write any reverse (pre-Scheme) transforms. + outfile.write('! Compute reverse (pre-scheme) transforms', indent+1) for (dummy, var, rindices, lindices, compat_obj) in self.__reverse_transforms: - tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent, False) + tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent+1, False) # Write the scheme call. stmt = 'call {}({})' + outfile.write('',indent+1) + outfile.write('! Call scheme', indent+1) outfile.write(stmt.format(self.subroutine_name, my_args), indent+1) # Write any forward (post-Scheme) transforms. + outfile.write('',indent+1) + outfile.write('! Compute forward (post-scheme) transforms', indent+1) for (var, dummy, lindices, rindices, compat_obj) in self.__forward_transforms: - tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent, True) + tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent+1, True) # + outfile.write('', indent) outfile.write('end if', indent) def schemes(self): From 0ff7939579aa29011c90a1d8590d8440ce991ab2 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Mon, 5 Feb 2024 15:50:05 +0000 Subject: [PATCH 44/49] Cleanup repeated lines in code --- scripts/suite_objects.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index c32035e8..bc02a4f1 100755 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1486,24 +1486,20 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er ubound_string = '(' + ','.join(ubound_strings) + ')' # Write size check + tmp_indent = indent if conditional != '.true.': + tmp_indent = indent + 1 outfile.write(f"if {conditional} then", indent) - outfile.write(f"! Check size of array {local_name}", indent+1) - outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent+1) - outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+2) - outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+2) - outfile.write(f"{errcode} = 1", indent+2) - outfile.write(f"return", indent+2) - outfile.write(f"end if", indent+1) - outfile.write(f"end if", indent) - outfile.write('',indent) - else: - outfile.write(f"! Check size of array {local_name}", indent) - outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", indent) - outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", indent+1) - outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", indent+1) - outfile.write(f"{errcode} = 1", indent+1) - outfile.write(f"return", indent+1) + # end if + outfile.write(f"! Check size of array {local_name}", tmp_indent) + outfile.write(f"if (size({local_name}{dim_string}) /= {array_size}) then", tmp_indent) + outfile.write(f"write({errmsg}, '(a)') 'In group {self.__group.name} before {self.__subroutine_name}:'", tmp_indent+1) + outfile.write(f"write({errmsg}, '(2(a,i8))') 'for array {local_name}, expected size ', {array_size}, ' but got ', size({local_name})", tmp_indent+1) + outfile.write(f"{errcode} = 1", tmp_indent+1) + outfile.write(f"return", tmp_indent+1) + outfile.write(f"end if", tmp_indent) + outfile.write('',tmp_indent) + if conditional != '.true.': outfile.write(f"end if", indent) outfile.write('',indent) # end if From ec817d20d4ae80bfcc4c5a97ca131bc9ae5aeff4 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Mon, 5 Feb 2024 17:26:32 +0000 Subject: [PATCH 45/49] Cleanup (more) repeated lines in code --- scripts/suite_objects.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index bc02a4f1..dda5b23f 100755 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1410,17 +1410,17 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er if not dimensions: if not intent == 'out': internal_var_lname = internal_var.get_prop_value('local_name') + tmp_indent = indent if conditional != '.true.': + tmp_indent = indent + 1 outfile.write(f"if {conditional} then", indent) - outfile.write(f"! Assign value of {local_name} to internal_var", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}", indent+1) + # end if + outfile.write(f"! Assign value of {local_name} to {internal_var}", tmp_indent) + outfile.write(f"{internal_var_lname} = {local_name}", tmp_indent) + outfile.write('',indent) + if conditional != '.true.': outfile.write(f"end if", indent) - outfile.write('',indent) - else: - outfile.write(f"! Assign value of {local_name} to internal_var", indent) - outfile.write(f"{internal_var_lname} = {local_name}", indent) - outfile.write('',indent) - # endif + # end if # For arrays, check size of array against dimensions in metadata, then assign # the lower and upper bounds to the internal_var variable if the intent is in/inout else: @@ -1498,28 +1498,26 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er outfile.write(f"{errcode} = 1", tmp_indent+1) outfile.write(f"return", tmp_indent+1) outfile.write(f"end if", tmp_indent) - outfile.write('',tmp_indent) if conditional != '.true.': outfile.write(f"end if", indent) - outfile.write('',indent) # end if + outfile.write('',indent) # Assign lower/upper bounds to internal_var (scalar) if intent is not out if not intent == 'out': internal_var_lname = internal_var.get_prop_value('local_name') + tmp_indent = indent if conditional != '.true.': + tmp_indent = indent + 1 outfile.write(f"if {conditional} then", indent) - outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent+1) + # end if + outfile.write(f"! Assign lower/upper bounds of {local_name} to {internal_var}", tmp_indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", tmp_indent+1) + outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", tmp_indent+1) + if conditional != '.true.': outfile.write(f"end if", indent) - outfile.write('',indent) - else: - outfile.write(f"! Assign lower/upper bounds of {local_name} to internal_var", indent) - outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", indent) - outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", indent) - outfile.write('',indent) # end if + outfile.write('',indent) def add_var_transform(self, var, compat_obj, vert_dim): """Register any variable transformation needed by for this Scheme. From a7b9255d5ffcaa209b01a3703f4ba2fc6c9c3188 Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Wed, 21 Feb 2024 18:06:48 -0700 Subject: [PATCH 46/49] Vertical transform bugfix for variables with only a vertical dimension (#531) Enable 1D variables with only a vertical dimension by removing code that always adds a dimension transform. var_props.py: adjust logic that always adds vertical transform and is prohibiting 1D variables with a vertical coordinate --------- Co-authored-by: Courtney Peverley --- scripts/var_props.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/var_props.py b/scripts/var_props.py index c61d9ae4..dc4c24c4 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -808,6 +808,19 @@ class VarCompatObj: _DOCTEST_RUNENV) #doctest: +ELLIPSIS + # Test that a 1-D var with no vertical transform works + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['vertical_layer_dimension'], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "m", ['vertical_layer_dimension'], "var2_lname", False, \ + _DOCTEST_RUNENV) #doctest: +ELLIPSIS + + + # Test that a 1-D var with vertical flipping works and that it + # produces the correct reverse transformation + >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['vertical_layer_dimension'], "var1_lname", False,\ + "var_stdname", "real", "kind_phys", "m", ['vertical_layer_dimension'], "var2_lname", True, \ + _DOCTEST_RUNENV).reverse_transform("var1_lname", "var2_lname", ('k',), ('nk-k+1',)) + 'var1_lname(nk-k+1) = var2_lname(k)' + # Test that a 2-D var with unit conversion m->km works >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ "var_stdname", "real", "kind_phys", "km", ['horizontal_dimension'], "var2_lname", False, \ @@ -928,12 +941,9 @@ def __init__(self, var1_stdname, var1_type, var1_kind, var1_units, # end if if self.__compat: # Check dimensions - ##XXgoldyXX: For now, we always have to create a dimension - ## transform because we do not know if the vertical - ## dimension is flipped. if var1_dims or var2_dims: _, vdim_ind = find_vertical_dimension(var1_dims) - if (var1_dims != var2_dims) or (vdim_ind >= 0): + if (var1_dims != var2_dims): self.__dim_transforms = self._get_dim_transforms(var1_dims, var2_dims) self.__compat = self.__dim_transforms is not None From ff28d15d1a4d94521fb362052a3ecae47d46913a Mon Sep 17 00:00:00 2001 From: Courtney Peverley Date: Wed, 6 Mar 2024 09:58:37 -0700 Subject: [PATCH 47/49] bugfixes for multiple groups and scalar variable transforms (#542) Updates to enable more than one scheme group, and also enable unit transforms for scalar (non-dimensioned) variables. Updated Files Groups: ccpp_suite.py - don't promote loop variables to suite level host_cap.py - fix for iterating over suite part list suite_objects.py - check if variable is at suite level before giving up hope and throwing an error Scalar transforms: suite_objects.py - don't try to do vertical dimension transform for scalar variable; conditionally write comments for var transform calls var_props.py - don't include parenthesis (e.g. variable1() = 2 * variable2()) for scalar variable transforms Misc: metavar.py - fix for bug surely introduced by me converting to fstrings at some point --- scripts/ccpp_suite.py | 6 ++- scripts/metavar.py | 2 +- scripts/suite_objects.py | 47 +++++++++++++++---- scripts/var_props.py | 30 ++++++++++-- test/capgen_test/temp_suite.xml | 4 +- test/capgen_test/test_host.F90 | 3 +- test/var_compatibility_test/effr_calc.F90 | 5 +- test/var_compatibility_test/effr_calc.meta | 8 ++++ test/var_compatibility_test/run_test | 3 ++ test/var_compatibility_test/test_host.F90 | 11 +++-- .../var_compatibility_test/test_host_data.F90 | 1 + .../test_host_data.meta | 7 +++ test/var_compatibility_test/test_host_mod.F90 | 8 ++++ test/var_compatibility_test/test_reports.py | 4 +- 14 files changed, 115 insertions(+), 24 deletions(-) diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index c5c6f4ab..d19ca4e8 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -22,6 +22,7 @@ from parse_tools import read_xml_file, validate_xml_file, find_schema_version from parse_tools import init_log, set_log_to_null from suite_objects import CallList, Group, Scheme +from metavar import CCPP_LOOP_VAR_STDNAMES # pylint: disable=too-many-lines @@ -286,13 +287,16 @@ def find_variable(self, standard_name=None, source_var=None, loop_subst=loop_subst) if var is None: # No dice? Check for a group variable which can be promoted - if standard_name in self.__gvar_stdnames: + # Don't promote loop standard names + if (standard_name in self.__gvar_stdnames and standard_name + not in CCPP_LOOP_VAR_STDNAMES): group = self.__gvar_stdnames[standard_name] var = group.find_variable(standard_name=standard_name, source_var=source_var, any_scope=False, search_call_list=srch_clist, loop_subst=loop_subst) + if var is not None: # Promote variable to suite level # Remove this entry to avoid looping back here diff --git a/scripts/metavar.py b/scripts/metavar.py index eb1973e1..3eb9cd79 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -1728,9 +1728,9 @@ def add_variable_dimensions(self, var, ignore_sources, to_dict=None, err_ret += f"{self.name}: " err_ret += f"Cannot find variable for dimension, {dimname}, of {vstdname}{ctx}" if dvar: - err_ret += f"\nFound {lname} from excluded source, '{dvar.source.ptype}'{dctx}" lname = dvar.get_prop_value('local_name') dctx = context_string(dvar.context) + err_ret += f"\nFound {lname} from excluded source, '{dvar.source.ptype}'{dctx}" # end if # end if # end if diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index dda5b23f..951e5c86 100755 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -1535,13 +1535,31 @@ def add_var_transform(self, var, compat_obj, vert_dim): # If needed, modify vertical dimension for vertical orientation flipping _, vdim = find_vertical_dimension(var.get_dimensions()) - vdim_name = vert_dim.split(':')[-1] - group_vvar = self.__group.call_list.find_variable(vdim_name) - vname = group_vvar.get_prop_value('local_name') - lindices[vdim] = '1:'+vname - rindices[vdim] = '1:'+vname - if compat_obj.has_vert_transforms: - rindices[vdim] = vname+':1:-1' + if vdim >= 0: + vdims = vert_dim.split(':') + vdim_name = vdims[-1] + group_vvar = self.__group.call_list.find_variable(vdim_name) + if group_vvar is None: + raise CCPPError(f"add_var_transform: Cannot find dimension variable, {vdim_name}") + # end if + vname = group_vvar.get_prop_value('local_name') + if len(vdims) == 2: + sdim_name = vdims[0] + group_vvar = self.find_variable(sdim_name) + if group_vvar is None: + raise CCPPError(f"add_var_transform: Cannot find dimension variable, {sdim_name}") + # end if + sname = group_vvar.get_prop_value('local_name') + else: + sname = '1' + # end if + lindices[vdim] = sname+':'+vname + if compat_obj.has_vert_transforms: + rindices[vdim] = vname+':'+sname+':-1' + else: + rindices[vdim] = sname+':'+vname + # end if + # end if # If needed, modify horizontal dimension for loop substitution. # NOT YET IMPLEMENTED @@ -1614,9 +1632,12 @@ def write(self, outfile, errcode, errmsg, indent): for (var, internal_var) in self.__var_debug_checks: stmt = self.write_var_debug_check(var, internal_var, cldicts, outfile, errcode, errmsg, indent+1) # Write any reverse (pre-Scheme) transforms. - outfile.write('! Compute reverse (pre-scheme) transforms', indent+1) + if len(self.__reverse_transforms) > 0: + outfile.comment('Compute reverse (pre-scheme) transforms', indent+1) + # end if for (dummy, var, rindices, lindices, compat_obj) in self.__reverse_transforms: tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent+1, False) + # end for # Write the scheme call. stmt = 'call {}({})' outfile.write('',indent+1) @@ -1624,10 +1645,12 @@ def write(self, outfile, errcode, errmsg, indent): outfile.write(stmt.format(self.subroutine_name, my_args), indent+1) # Write any forward (post-Scheme) transforms. outfile.write('',indent+1) - outfile.write('! Compute forward (post-scheme) transforms', indent+1) + if len(self.__forward_transforms) > 0: + outfile.comment('Compute forward (post-scheme) transforms', indent+1) + # end if for (var, dummy, lindices, rindices, compat_obj) in self.__forward_transforms: tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent+1, True) - # + # end for outfile.write('', indent) outfile.write('end if', indent) @@ -1724,6 +1747,10 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): local_dim = group.call_list.find_variable(standard_name=dim_name, any_scope=False) # end if + # If not found, check the suite level + if local_dim is None: + local_dim = group.suite.find_variable(standard_name=dim_name) + # end if if local_dim is None: emsg = 'No variable found for vertical loop dimension {}' raise ParseInternalError(emsg.format(self._dim_name)) diff --git a/scripts/var_props.py b/scripts/var_props.py index dc4c24c4..9a2fd7d0 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -821,6 +821,18 @@ class VarCompatObj: _DOCTEST_RUNENV).reverse_transform("var1_lname", "var2_lname", ('k',), ('nk-k+1',)) 'var1_lname(nk-k+1) = var2_lname(k)' + # Test that unit conversions with a scalar var works + >>> VarCompatObj("var_stdname", "real", "kind_phys", "Pa", [], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "hPa", [], "var2_lname", False, \ + _DOCTEST_RUNENV).forward_transform("var1_lname", "var2_lname", [], []) #doctest: +ELLIPSIS + 'var1_lname = 1.0E-2_kind_phys*var2_lname' + + # Test that unit conversions with a scalar var works + >>> VarCompatObj("var_stdname", "real", "kind_phys", "Pa", [], "var1_lname", False, \ + "var_stdname", "real", "kind_phys", "hPa", [], "var2_lname", False, \ + _DOCTEST_RUNENV).reverse_transform("var1_lname", "var2_lname", [], []) #doctest: +ELLIPSIS + 'var1_lname = 1.0E+2_kind_phys*var2_lname' + # Test that a 2-D var with unit conversion m->km works >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", ['horizontal_dimension'], "var1_lname", False, \ "var_stdname", "real", "kind_phys", "km", ['horizontal_dimension'], "var2_lname", False, \ @@ -976,8 +988,13 @@ def forward_transform(self, lvar_lname, rvar_lname, rvar_indices, lvar_indices, "vertical_interface_dimension"). """ # Dimension transform (Indices handled externally) - rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" - lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + if len(rvar_indices) == 0: + rhs_term = f"{rvar_lname}" + lhs_term = f"{lvar_lname}" + else: + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + # end if if self.has_kind_transforms: kind = self.__kind_transforms[1] @@ -1016,8 +1033,13 @@ def reverse_transform(self, lvar_lname, rvar_lname, rvar_indices, lvar_indices, "vertical_interface_dimension"). """ # Dimension transforms (Indices handled externally) - lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" - rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + if len(rvar_indices) == 0: + rhs_term = f"{rvar_lname}" + lhs_term = f"{lvar_lname}" + else: + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + # end if if self.has_kind_transforms: kind = self.__kind_transforms[0] diff --git a/test/capgen_test/temp_suite.xml b/test/capgen_test/temp_suite.xml index 9974b02a..6fa836db 100644 --- a/test/capgen_test/temp_suite.xml +++ b/test/capgen_test/temp_suite.xml @@ -1,8 +1,10 @@ - + temp_set + + temp_calc_adjust temp_adjust diff --git a/test/capgen_test/test_host.F90 b/test/capgen_test/test_host.F90 index 55c7159e..056b30a0 100644 --- a/test/capgen_test/test_host.F90 +++ b/test/capgen_test/test_host.F90 @@ -355,7 +355,8 @@ program test implicit none - character(len=cs), target :: test_parts1(1) = (/ 'physics ' /) + character(len=cs), target :: test_parts1(2) = (/ 'physics1 ', & + 'physics2 ' /) character(len=cs), target :: test_parts2(1) = (/ 'data_prep ' /) character(len=cm), target :: test_invars1(6) = (/ & 'potential_temperature ', & diff --git a/test/var_compatibility_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 index b877e99a..3a6caab0 100644 --- a/test/var_compatibility_test/effr_calc.F90 +++ b/test/var_compatibility_test/effr_calc.F90 @@ -16,7 +16,8 @@ module effr_calc !! \htmlinclude arg_table_effr_calc_run.html !! subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & - effri_out, effrs_inout, has_graupel, errmsg, errflg) + effri_out, effrs_inout, has_graupel, scalar_var, & + errmsg, errflg) integer, intent(in) :: ncol integer, intent(in) :: nlev @@ -26,6 +27,7 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & real(kind_phys), intent(out) :: effri_out(:,:) real(8),intent(inout) :: effrs_inout(:,:) logical, intent(in) :: has_graupel + real(kind_phys), intent(inout) :: scalar_var character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg !---------------------------------------------------------------- @@ -44,6 +46,7 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) effri_out = re_qi_avg effrs_inout = effrs_inout + 10.0 ! in micrometer + scalar_var = 2.0 ! in km end subroutine effr_calc_run diff --git a/test/var_compatibility_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta index 14687f21..d1712f3a 100644 --- a/test/var_compatibility_test/effr_calc.meta +++ b/test/var_compatibility_test/effr_calc.meta @@ -66,6 +66,14 @@ dimensions = () type = logical intent = in +[ scalar_var ] + standard_name = scalar_variable_for_testing + long_name = scalar variable for testing + units = km + dimensions = () + type = real + kind = kind_phys + intent = inout [ errmsg ] standard_name = ccpp_error_message long_name = Error message for error handling in CCPP diff --git a/test/var_compatibility_test/run_test b/test/var_compatibility_test/run_test index 9239c5d1..76ad982e 100755 --- a/test/var_compatibility_test/run_test +++ b/test/var_compatibility_test/run_test @@ -140,6 +140,7 @@ required_vars_var_compatibility="${required_vars_var_compatibility},flag_indicat required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_dimension" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_begin" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_end" +required_vars_var_compatibility="${required_vars_var_compatibility},scalar_variable_for_testing" required_vars_var_compatibility="${required_vars_var_compatibility},vertical_layer_dimension" input_vars_var_compatibility="effective_radius_of_stratiform_cloud_graupel" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" @@ -149,11 +150,13 @@ input_vars_var_compatibility="${input_vars_var_compatibility},flag_indicating_cl input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_dimension" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_begin" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_end" +input_vars_var_compatibility="${input_vars_var_compatibility},scalar_variable_for_testing" input_vars_var_compatibility="${input_vars_var_compatibility},vertical_layer_dimension" output_vars_var_compatibility="ccpp_error_code,ccpp_error_message" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" +output_vars_var_compatibility="${output_vars_var_compatibility},scalar_variable_for_testing" ## ## Run a database report and check the return string diff --git a/test/var_compatibility_test/test_host.F90 b/test/var_compatibility_test/test_host.F90 index 12215bd1..6e170607 100644 --- a/test/var_compatibility_test/test_host.F90 +++ b/test/var_compatibility_test/test_host.F90 @@ -351,21 +351,23 @@ program test character(len=cs), target :: test_parts1(1) = (/ 'radiation ' /) - character(len=cm), target :: test_invars1(5) = (/ & + character(len=cm), target :: test_invars1(6) = (/ & 'effective_radius_of_stratiform_cloud_rain_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ', & 'effective_radius_of_stratiform_cloud_graupel ', & + 'scalar_variable_for_testing ', & 'flag_indicating_cloud_microphysics_has_graupel '/) - character(len=cm), target :: test_outvars1(5) = (/ & + character(len=cm), target :: test_outvars1(6) = (/ & 'ccpp_error_code ', & 'ccpp_error_message ', & 'effective_radius_of_stratiform_cloud_ice_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & - 'effective_radius_of_stratiform_cloud_snow_particle ' /) + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'scalar_variable_for_testing ' /) - character(len=cm), target :: test_reqvars1(8) = (/ & + character(len=cm), target :: test_reqvars1(9) = (/ & 'ccpp_error_code ', & 'ccpp_error_message ', & 'effective_radius_of_stratiform_cloud_rain_particle ', & @@ -373,6 +375,7 @@ program test 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ', & 'effective_radius_of_stratiform_cloud_graupel ', & + 'scalar_variable_for_testing ', & 'flag_indicating_cloud_microphysics_has_graupel '/) type(suite_info) :: test_suites(1) diff --git a/test/var_compatibility_test/test_host_data.F90 b/test/var_compatibility_test/test_host_data.F90 index b17fb916..1980f95a 100644 --- a/test/var_compatibility_test/test_host_data.F90 +++ b/test/var_compatibility_test/test_host_data.F90 @@ -10,6 +10,7 @@ module test_host_data effrl, & ! effective radius of cloud liquid water effri, & ! effective radius of cloud ice effrg ! effective radius of cloud graupel + real(kind_phys) :: scalar_var end type physics_state public allocate_physics_state diff --git a/test/var_compatibility_test/test_host_data.meta b/test/var_compatibility_test/test_host_data.meta index 1d29572c..01ad7a4a 100644 --- a/test/var_compatibility_test/test_host_data.meta +++ b/test/var_compatibility_test/test_host_data.meta @@ -33,3 +33,10 @@ type = real kind = kind_phys active = (flag_indicating_cloud_microphysics_has_graupel) +[scalar_var] + standard_name = scalar_variable_for_testing + long_name = unused scalar variable + units = m + dimensions = () + type = real + kind = kind_phys diff --git a/test/var_compatibility_test/test_host_mod.F90 b/test/var_compatibility_test/test_host_mod.F90 index 2761d9fa..877e3432 100644 --- a/test/var_compatibility_test/test_host_mod.F90 +++ b/test/var_compatibility_test/test_host_mod.F90 @@ -27,6 +27,7 @@ subroutine init_data() phys_state%effrr = 1.0E-3 ! 1000 microns, in meter phys_state%effrl = 1.0E-4 ! 100 microns, in meter phys_state%effri = 5.0E-5 ! 50 microns, in meter + phys_state%scalar_var = 1.0 ! in m effrs = 5.0E-4 ! 500 microns, in meter if (mp_has_graupel) then phys_state%effrg = 2.5E-4 ! 250 microns, in meter @@ -40,6 +41,7 @@ logical function compare_data() real(kind_phys), parameter :: effrl_expected = 5.0E-5 ! 50 microns, in meter real(kind_phys), parameter :: effri_expected = 7.5E-5 ! 75 microns, in meter real(kind_phys), parameter :: effrs_expected = 5.1E-4 ! 510 microns, in meter + real(kind_phys), parameter :: scalar_expected = 2.0E3 ! 2 km, in meter real(kind_phys), parameter :: tolerance = 1.0E-6 ! used as scaling factor for expected value compare_data = .true. @@ -68,6 +70,12 @@ logical function compare_data() compare_data = .false. end if + if (abs( phys_state%scalar_var - scalar_expected) > tolerance*scalar_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of scalar_var from expected value exceeds tolerance: ', & + abs( phys_state%scalar_var - scalar_expected), ' > ', tolerance*scalar_expected + compare_data = .false. + end if + end function compare_data end module test_host_mod diff --git a/test/var_compatibility_test/test_reports.py b/test/var_compatibility_test/test_reports.py index a56457bc..fd5e5203 100755 --- a/test/var_compatibility_test/test_reports.py +++ b/test/var_compatibility_test/test_reports.py @@ -72,11 +72,13 @@ def usage(errmsg=None): "effective_radius_of_stratiform_cloud_rain_particle", "effective_radius_of_stratiform_cloud_snow_particle", "effective_radius_of_stratiform_cloud_graupel", + "scalar_variable_for_testing", "flag_indicating_cloud_microphysics_has_graupel"] _OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", "effective_radius_of_stratiform_cloud_ice_particle", "effective_radius_of_stratiform_cloud_liquid_water_particle", - "effective_radius_of_stratiform_cloud_snow_particle"] + "effective_radius_of_stratiform_cloud_snow_particle", + "scalar_variable_for_testing"] _REQUIRED_VARS_VAR_ACTION = _INPUT_VARS_VAR_ACTION + _OUTPUT_VARS_VAR_ACTION def fields_string(field_type, field_list, sep): From c546ccac85e8eda78062ab3516f0275075954840 Mon Sep 17 00:00:00 2001 From: Dustin Swales Date: Fri, 8 Mar 2024 08:34:26 -0700 Subject: [PATCH 48/49] Add optional attribute to Capgen (#529) This PR adds support to use the fortran optional attribute at the scheme level in Capgen. This functionality does not replace the active attribute, but rather builds on it. Previously, conditionally allocated fields needed by a scheme were required to be accompanied with the allocation logic defined by the host (e.g the "active" condition). With this change, the scheme can internally query the "presence" instead of carrying the host model logic "down into the scheme". The following modifications were made: Metadata parsing: Check for compatibility between scheme and host metadata. If host has inactive variable, ensure that scheme has variable declared as optional. Fortran parsing: Check for consistency between metadata and fortran file. If fortran file does not contain the optional attribute in its declaration, report error. For any optional scheme variable, add optional attribute to cap level declarations. Declare null pointer at cap level when host does not use optional scheme variable --- scripts/ccpp_capgen.py | 11 + scripts/ccpp_datafile.py | 2 +- scripts/fortran_tools/parse_fortran.py | 2 +- scripts/metavar.py | 55 ++-- scripts/suite_objects.py | 259 ++++++++++++++++-- test/capgen_test/temp_adjust.F90 | 4 +- test/capgen_test/temp_adjust.meta | 1 + test/var_compatibility_test/effr_calc.F90 | 21 +- test/var_compatibility_test/effr_calc.meta | 33 ++- test/var_compatibility_test/run_test | 10 +- test/var_compatibility_test/test_host.F90 | 16 +- .../var_compatibility_test/test_host_data.F90 | 29 +- .../test_host_data.meta | 19 ++ test/var_compatibility_test/test_host_mod.F90 | 13 +- .../var_compatibility_test/test_host_mod.meta | 8 +- test/var_compatibility_test/test_reports.py | 5 +- 16 files changed, 417 insertions(+), 71 deletions(-) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index cbd46599..042f6d16 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -314,6 +314,17 @@ def compare_fheader_to_mheader(meta_header, fort_header, logger): lname = mvar.get_prop_value('local_name') arrayref = is_arrayspec(lname) fvar, find = find_var_in_list(lname, flist) + # Check for consistency between optional variables in metadata and + # optional variables in fortran. Error if optional attribute is + # missing from fortran declaration. + mopt = mvar.get_prop_value('optional') + if find and mopt: + fopt = fvar.get_prop_value('optional') + if (not fopt): + errmsg = 'Missing optional attribute in fortran declaration for variable {}, in file {}' + errors_found = add_error(errors_found, errmsg.format(mname,title)) + # end if + # end if if mind >= flen: if arrayref: # Array reference, variable not in Fortran table diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 1fd5f830..3c5092ed 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -654,7 +654,7 @@ def _new_var_entry(parent, var, full_entry=True): "diagnostic_name", "diagnostic_name_fixed", "kind", "persistence", "polymorphic", "protected", "state_variable", "type", "units", "molar_mass", - "advected", "top_at_one"]) + "advected", "top_at_one", "optional"]) prop_list.extend(Var.constituent_property_names()) # end if ventry = ET.SubElement(parent, "var") diff --git a/scripts/fortran_tools/parse_fortran.py b/scripts/fortran_tools/parse_fortran.py index 9aa4de7a..e7d3c495 100644 --- a/scripts/fortran_tools/parse_fortran.py +++ b/scripts/fortran_tools/parse_fortran.py @@ -676,7 +676,7 @@ def parse_fortran_var_decl(line, source, run_env): >>> parse_fortran_var_decl("integer :: foo = 0", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') 'foo' >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional') - + False >>> parse_fortran_var_decl("integer, optional :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional') 'True' >>> parse_fortran_var_decl("integer, dimension(:) :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') diff --git a/scripts/metavar.py b/scripts/metavar.py index 3eb9cd79..2974f1db 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -150,9 +150,9 @@ class Var: >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'ino'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ParseSyntaxError: Invalid intent variable property, 'ino', at :1 - >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ParseSyntaxError: Invalid variable property name, 'optional', at :1 + >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +ELLIPSIS + + # Check that two variables that differ in their units - m vs km - are compatible >>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm', \ 'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, \ @@ -207,6 +207,8 @@ class Var: default_in=False), VariableProperty('top_at_one', bool, optional_in=True, default_in=False), + VariableProperty('optional', bool, optional_in=True, + default_in=False), VariableProperty('target', bool, optional_in=True, default_in=False)] @@ -1025,7 +1027,7 @@ def conditional(self, vdicts): vars_needed.append(dvar) return (conditional, vars_needed) - def write_def(self, outfile, indent, wdict, allocatable=False, + def write_def(self, outfile, indent, wdict, allocatable=False, target=False, dummy=False, add_intent=None, extra_space=0, public=False): """Write the definition line for the variable to . If is True, include the variable's intent. @@ -1077,11 +1079,15 @@ def write_def(self, outfile, indent, wdict, allocatable=False, raise CCPPError(errmsg.format(name)) # end if # end if + optional = self.get_prop_value('optional') if protected and dummy: intent_str = 'intent(in) ' elif allocatable: if dimstr or polymorphic: - intent_str = 'allocatable ' + intent_str = 'allocatable ' + if target: + intent_str = 'allocatable,' + intent_str += ' target' else: intent_str = ' '*13 # end if @@ -1089,11 +1095,14 @@ def write_def(self, outfile, indent, wdict, allocatable=False, alloval = self.get_prop_value('allocatable') if (intent.lower()[-3:] == 'out') and alloval: intent_str = f"allocatable, intent({intent})" + elif optional: + intent_str = f"intent({intent}),{' '*(5 - len(intent))}" + intent_str += 'target, optional ' else: intent_str = f"intent({intent}){' '*(5 - len(intent))}" # end if elif not dummy: - intent_str = '' + intent_str = ' '*20 else: intent_str = ' '*13 # end if @@ -1111,26 +1120,40 @@ def write_def(self, outfile, indent, wdict, allocatable=False, extra_space -= len(targ) if self.is_ddt(): if polymorphic: - dstr = "class({kind}){cspc}{intent} :: {name}{dims} ! {sname}" - cspc = comma + ' '*(extra_space + 12 - len(kind)) + dstr = "class({kind}){cspace}{intent} :: {name}{dims}" + cspace = comma + ' '*(extra_space + 12 - len(kind)) else: - dstr = "type({kind}){cspc}{intent} :: {name}{dims} ! {sname}" - cspc = comma + ' '*(extra_space + 13 - len(kind)) + dstr = "type({kind}){cspace}{intent} :: {name}{dims}" + cspace = comma + ' '*(extra_space + 13 - len(kind)) # end if else: if kind: - dstr = "{type}({kind}){cspc}{intent} :: {name}{dims} ! {sname}" - cspc = comma + ' '*(extra_space + 17 - len(vtype) - len(kind)) + dstr = "{type}({kind}){cspace}{intent} :: {name}{dims}" + cspace = comma + ' '*(extra_space + 17 - len(vtype) - len(kind)) else: - dstr = "{type}{cspc}{intent} :: {name}{dims} ! {sname}" - cspc = comma + ' '*(extra_space + 19 - len(vtype)) + dstr = "{type}{cspace}{intent} :: {name}{dims}" + cspace = comma + ' '*(extra_space + 19 - len(vtype)) # end if # end if - outfile.write(dstr.format(type=vtype, kind=kind, intent=intent_str, - name=name, dims=dimstr, cspc=cspc, + name=name, dims=dimstr, cspace=cspace, sname=stdname), indent) + def write_ptr_def(self, outfile, indent, name, kind, dimstr, vtype, extra_space=0): + """Write the definition line for local null pointer declaration to .""" + comma = ', ' + if kind: + dstr = "{type}({kind}){cspace}pointer :: {name}{dims}{cspace2} => null()" + cspace = comma + ' '*(extra_space + 20 - len(vtype) - len(kind)) + cspace2 = ' '*(20 -len(name) - len(dimstr)) + else: + dstr = "{type}{cspace}pointer :: {name}{dims}{cspace2} => null()" + cspace = comma + ' '*(extra_space + 22 - len(vtype)) + cspace2 = ' '*(20 -len(name) - len(dimstr)) + # end if + outfile.write(dstr.format(type=vtype, kind=kind, name=name, dims=dimstr, + cspace=cspace, cspace2=cspace2), indent) + def is_ddt(self): """Return True iff is a DDT type.""" return not self.__intrinsic diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index 951e5c86..37978d54 100755 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -143,6 +143,11 @@ def call_string(self, cldicts=None, is_func_call=False, subname=None): raise CCPPError(errmsg.format(stdname, clnames)) # end if lname = dvar.get_prop_value('local_name') + # Optional variables in the caps are associated with + # local pointers of _ptr + if dvar.get_prop_value('optional'): + lname = dummy+'_ptr' + # end if else: cldict = None aref = var.array_ref(local_name=dummy) @@ -871,10 +876,20 @@ def match_variable(self, var, run_env): new_vdims = list() new_dict_dims = dict_dims match = True + # end if # Create compatability object, containing any necessary forward/reverse # transforms from and compat_obj = var.compatible(dict_var, run_env) - + # If variable is defined as "inactive" by the host, ensure that + # this variable is declared as "optional" by the scheme. If + # not satisfied, return error. + host_var_active = dict_var.get_prop_value('active') + scheme_var_optional = var.get_prop_value('optional') + if (not scheme_var_optional and host_var_active.lower() != '.true.'): + errmsg = "Non optional scheme arguments for conditionally allocatable variables" + sname = dict_var.get_prop_value('standard_name') + errmsg += ", {}".format(sname) + raise CCPPError(errmsg) # end if # Add the variable to the parent call tree if dict_dims == new_dict_dims: @@ -1083,6 +1098,7 @@ def __init__(self, scheme_xml, context, parent, run_env): self.__var_debug_checks = list() self.__forward_transforms = list() self.__reverse_transforms = list() + self.__optional_vars = list() super().__init__(name, context, parent, run_env, active_call_list=True) def update_group_call_list_variable(self, var): @@ -1212,10 +1228,21 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end if # end if # Are there any forward/reverse transforms for this variable? + has_transform = False if compat_obj is not None and (compat_obj.has_vert_transforms or compat_obj.has_unit_transforms or compat_obj.has_kind_transforms): self.add_var_transform(var, compat_obj, vert_dim) + has_transform = True + # end if + + # Is this a conditionally allocated variable? + # If so, declare localpointer varaible. This is needed to + # pass inactive (not present) status through the caps. + if var.get_prop_value('optional'): + newvar_ptr = var.clone(var.get_prop_value('local_name')+'_ptr') + self.__optional_vars.append([dict_var, var, newvar_ptr, has_transform]) + # end if # end for if self.needs_vertical is not None: @@ -1415,9 +1442,9 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er tmp_indent = indent + 1 outfile.write(f"if {conditional} then", indent) # end if - outfile.write(f"! Assign value of {local_name} to {internal_var}", tmp_indent) + outfile.write(f"! Assign value of {local_name} to {internal_var_lname}", tmp_indent) outfile.write(f"{internal_var_lname} = {local_name}", tmp_indent) - outfile.write('',indent) + outfile.write('',tmp_indent) if conditional != '.true.': outfile.write(f"end if", indent) # end if @@ -1511,14 +1538,47 @@ def write_var_debug_check(self, var, internal_var, cldicts, outfile, errcode, er tmp_indent = indent + 1 outfile.write(f"if {conditional} then", indent) # end if - outfile.write(f"! Assign lower/upper bounds of {local_name} to {internal_var}", tmp_indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", tmp_indent+1) - outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", tmp_indent+1) + outfile.write(f"! Assign lower/upper bounds of {local_name} to {internal_var_lname}", tmp_indent) + outfile.write(f"{internal_var_lname} = {local_name}{lbound_string}", tmp_indent) + outfile.write(f"{internal_var_lname} = {local_name}{ubound_string}", tmp_indent) if conditional != '.true.': outfile.write(f"end if", indent) # end if outfile.write('',indent) + def associate_optional_var(self, dict_var, var, var_ptr, has_transform, cldicts, indent, outfile): + """Write local pointer association for optional variables.""" + if (dict_var): + (conditional, _) = dict_var.conditional(cldicts) + if (has_transform): + lname = var.get_prop_value('local_name')+'_local' + else: + lname = var.get_prop_value('local_name') + # end if + lname_ptr = var_ptr.get_prop_value('local_name') + outfile.write(f"if {conditional} then", indent) + outfile.write(f"{lname_ptr} => {lname}", indent+1) + outfile.write(f"end if", indent) + # end if + + def assign_pointer_to_var(self, dict_var, var, var_ptr, has_transform, cldicts, indent, outfile): + """Assign local pointer to variable.""" + if (dict_var): + intent = var.get_prop_value('intent') + if (intent == 'out' or intent == 'inout'): + (conditional, _) = dict_var.conditional(cldicts) + if (has_transform): + lname = var.get_prop_value('local_name')+'_local' + else: + lname = var.get_prop_value('local_name') + # end if + lname_ptr = var_ptr.get_prop_value('local_name') + outfile.write(f"if {conditional} then", indent) + outfile.write(f"{lname} = {lname_ptr}", indent+1) + outfile.write(f"end if", indent) + # end if + # end if + def add_var_transform(self, var, compat_obj, vert_dim): """Register any variable transformation needed by for this Scheme. For any transformation identified in , create dummy variable @@ -1610,6 +1670,7 @@ def write_var_transform(self, var, dummy, rindices, lindices, compat_obj, rvar_lname=dummy, lvar_indices=rindices, rvar_indices=lindices) + # end if outfile.write(stmt, indent) def write(self, outfile, errcode, errmsg, indent): @@ -1623,14 +1684,29 @@ def write(self, outfile, errcode, errmsg, indent): my_args = self.call_list.call_string(cldicts=cldicts, is_func_call=True, subname=self.subroutine_name) - + # outfile.write('', indent) outfile.write('if ({} == 0) then'.format(errcode), indent) - + # # Write debug checks (operating on variables # coming from the group's call list) + # + if self.__var_debug_checks: + outfile.write('! ##################################################################', indent+1) + outfile.comment('Begin debug tests', indent+1) + outfile.write('! ##################################################################', indent+1) + outfile.write('', indent+1) + # end if for (var, internal_var) in self.__var_debug_checks: stmt = self.write_var_debug_check(var, internal_var, cldicts, outfile, errcode, errmsg, indent+1) + # end for + if self.__var_debug_checks: + outfile.write('! ##################################################################', indent+1) + outfile.comment('End debug tests', indent+1) + outfile.write('! ##################################################################', indent+1) + outfile.write('', indent+1) + # end if + # # Write any reverse (pre-Scheme) transforms. if len(self.__reverse_transforms) > 0: outfile.comment('Compute reverse (pre-scheme) transforms', indent+1) @@ -1638,13 +1714,39 @@ def write(self, outfile, errcode, errmsg, indent): for (dummy, var, rindices, lindices, compat_obj) in self.__reverse_transforms: tstmt = self.write_var_transform(var, dummy, rindices, lindices, compat_obj, outfile, indent+1, False) # end for + outfile.write('',indent+1) + # + # Associate any conditionally allocated variables. + # + if self.__optional_vars: + outfile.write('! Associate conditional variables', indent+1) + # end if + for (dict_var, var, var_ptr, has_transform) in self.__optional_vars: + tstmt = self.associate_optional_var(dict_var, var, var_ptr, has_transform, cldicts, indent+1, outfile) + # end for + # # Write the scheme call. + # stmt = 'call {}({})' outfile.write('',indent+1) outfile.write('! Call scheme', indent+1) outfile.write(stmt.format(self.subroutine_name, my_args), indent+1) - # Write any forward (post-Scheme) transforms. outfile.write('',indent+1) + # + # Copy any local pointers. + # + first_ptr_declaration=True + for (dict_var, var, var_ptr, has_transform) in self.__optional_vars: + if first_ptr_declaration: + outfile.write('! Copy any local pointers to dummy/local variables', indent+1) + first_ptr_declaration=False + # end if + tstmt = self.assign_pointer_to_var(dict_var, var, var_ptr, has_transform, cldicts, indent+1, outfile) + # end for + outfile.write('',indent+1) + # + # Write any forward (post-Scheme) transforms. + # if len(self.__forward_transforms) > 0: outfile.comment('Compute forward (post-scheme) transforms', indent+1) # end if @@ -2184,26 +2286,84 @@ def write(self, outfile, host_arglist, indent, const_mod, group_type = 'run' # Allocate for entire run # end if # Collect information on local variables - subpart_vars = {} + subpart_allocate_vars = {} + subpart_optional_vars = {} + subpart_scalar_vars = {} allocatable_var_set = set() + optional_var_set = set() + pointer_var_set = list() + inactive_var_set = set() for item in [self]:# + self.parts: for var in item.declarations(): lname = var.get_prop_value('local_name') - if lname in subpart_vars: - if subpart_vars[lname][0].compatible(var, self.run_env): + sname = var.get_prop_value('standard_name') + if (lname in subpart_allocate_vars) or (lname in subpart_optional_vars) or (lname in subpart_scalar_vars): + if subpart_allocate_vars[lname][0].compatible(var, self.run_env): pass # We already are going to declare this variable else: errmsg = "Duplicate Group variable, {}" raise ParseInternalError(errmsg.format(lname)) # end if else: - subpart_vars[lname] = (var, item) + opt_var = var.get_prop_value('optional') dims = var.get_dimensions() if (dims is not None) and dims: - allocatable_var_set.add(lname) + if opt_var: + if (self.call_list.find_variable(standard_name=sname)): + subpart_optional_vars[lname] = (var, item, opt_var) + optional_var_set.add(lname) + else: + inactive_var_set.add(var) + # end if + else: + subpart_allocate_vars[lname] = (var, item, opt_var) + allocatable_var_set.add(lname) + # end if + else: + subpart_scalar_vars[lname] = (var, item, opt_var) # end if # end if # end for + # All optional dummy variables within group need to have + # an associated pointer array declared. + for cvar in self.call_list.variable_list(): + opt_var = cvar.get_prop_value('optional') + if opt_var: + name = cvar.get_prop_value('local_name')+'_ptr' + kind = cvar.get_prop_value('kind') + dims = cvar.get_dimensions() + if cvar.is_ddt(): + vtype = 'type' + else: + vtype = cvar.get_prop_value('type') + # end if + if dims: + dimstr = '(:' + ',:'*(len(dims) - 1) + ')' + else: + dimstr = '' + # end if + pointer_var_set.append([name,kind,dimstr,vtype]) + # end if + # end for + # Any optional arguments that are not requested by the host need to have + # a local null pointer passed from the group to the scheme. + for ivar in inactive_var_set: + name = ivar.get_prop_value('local_name')+'_ptr' + kind = ivar.get_prop_value('kind') + dims = ivar.get_dimensions() + if ivar.is_ddt(): + vtype = 'type' + else: + vtype = ivar.get_prop_value('type') + # end if + if dims: + dimstr = '(:' + ',:'*(len(dims) - 1) + ')' + else: + dimstr = '' + # end if + pointer_var_set.append([name,kind,dimstr,vtype]) + # end for + # end for # First, write out the subroutine header subname = self.name @@ -2228,7 +2388,7 @@ def write(self, outfile, host_arglist, indent, const_mod, call_vars = self.call_list.variable_list() self._ddt_library.write_ddt_use_statements(call_vars, outfile, indent+1, pad=modmax) - decl_vars = [x[0] for x in subpart_vars.values()] + decl_vars = [x[0] for x in subpart_allocate_vars.values()] self._ddt_library.write_ddt_use_statements(decl_vars, outfile, indent+1, pad=modmax) outfile.write('', 0) @@ -2240,14 +2400,39 @@ def write(self, outfile, host_arglist, indent, const_mod, self.run_env.logger.debug(msg.format(self.name, call_vars)) # end if self.call_list.declare_variables(outfile, indent+1, dummy=True) - if subpart_vars: + # DECLARE local variables + if subpart_allocate_vars or subpart_scalar_vars or subpart_optional_vars: outfile.write('\n! Local Variables', indent+1) - # Write out local variables - for key in subpart_vars: - var = subpart_vars[key][0] - spdict = subpart_vars[key][1] + # end if + # Scalars + for key in subpart_scalar_vars: + var = subpart_scalar_vars[key][0] + spdict = subpart_scalar_vars[key][1] + target = subpart_scalar_vars[key][2] + var.write_def(outfile, indent+1, spdict, + allocatable=False, target=target) + # end for + # Allocatable arrays + for key in subpart_allocate_vars: + var = subpart_allocate_vars[key][0] + spdict = subpart_allocate_vars[key][1] + target = subpart_allocate_vars[key][2] var.write_def(outfile, indent+1, spdict, - allocatable=(key in allocatable_var_set)) + allocatable=(key in allocatable_var_set), + target=target) + # end for + # Target arrays. + for key in subpart_optional_vars: + var = subpart_optional_vars[key][0] + spdict = subpart_optional_vars[key][1] + target = subpart_optional_vars[key][2] + var.write_def(outfile, indent+1, spdict, + allocatable=(key in optional_var_set), + target=target) + # end for + # Pointer variables + for (name, kind, dim, vtype) in pointer_var_set: + var.write_ptr_def(outfile, indent+1, name, kind, dim, vtype) # end for outfile.write('', 0) # Get error variable names @@ -2269,29 +2454,41 @@ def write(self, outfile, host_arglist, indent, const_mod, raise CCPPError(errmsg.format(self.name)) # end if # Initialize error variables + outfile.write("! Initialize ccpp error handling", 2) outfile.write("{} = 0".format(errcode), 2) outfile.write("{} = ''".format(errmsg), 2) + outfile.write("",2) # end if # Output threaded region check (except for run phase) if not self.run_phase(): + outfile.write("! Output threaded region check ",indent+1) Group.__thread_check.write(outfile, indent, {'phase' : self.phase(), 'errcode' : errcode, 'errmsg' : errmsg}) # Check state machine + outfile.write("! Check state machine",indent+1) self._phase_check_stmts.write(outfile, indent, {'errcode' : errcode, 'errmsg' : errmsg, 'funcname' : self.name}) # Allocate local arrays + outfile.write('\n! Allocate local arrays', indent+1) alloc_stmt = "allocate({}({}))" for lname in allocatable_var_set: - var = subpart_vars[lname][0] + var = subpart_allocate_vars[lname][0] + dims = var.get_dimensions() + alloc_str = self.allocate_dim_str(dims, var.context) + outfile.write(alloc_stmt.format(lname, alloc_str), indent+1) + # end for + for lname in optional_var_set: + var = subpart_optional_vars[lname][0] dims = var.get_dimensions() alloc_str = self.allocate_dim_str(dims, var.context) outfile.write(alloc_stmt.format(lname, alloc_str), indent+1) # end for # Allocate suite vars if allocate: + outfile.write('\n! Allocate suite_vars', indent+1) for svar in suite_vars.variable_list(): dims = svar.get_dimensions() if dims: @@ -2314,12 +2511,26 @@ def write(self, outfile, host_arglist, indent, const_mod, # end for # Write the scheme and subcycle calls for item in self.parts: - item.write(outfile, errcode, errmsg, indent + 1) + item.write(outfile, errcode, errmsg, indent + 1) # end for # Deallocate local arrays + if allocatable_var_set: + outfile.write('\n! Deallocate local arrays', indent+1) + # end if for lname in allocatable_var_set: - outfile.write('deallocate({})'.format(lname), indent+1) + outfile.write('if (allocated({})) {} deallocate({})'.format(lname,' '*(20-len(lname)),lname), indent+1) + # end for + for lname in optional_var_set: + outfile.write('if (allocated({})) {} deallocate({})'.format(lname,' '*(20-len(lname)),lname), indent+1) # end for + # Nullify local pointers + if pointer_var_set: + outfile.write('\n! Nullify local pointers', indent+1) + # end if + for (name, kind, dim, vtype) in pointer_var_set: + #cspace = ' '*(15-len(name)) + outfile.write('if (associated({})) {} nullify({})'.format(name,' '*(15-len(name)),name), indent+1) + # end fo # Deallocate suite vars if deallocate: for svar in suite_vars.variable_list(): diff --git a/test/capgen_test/temp_adjust.F90 b/test/capgen_test/temp_adjust.F90 index 356a86d1..5aba4c0b 100644 --- a/test/capgen_test/temp_adjust.F90 +++ b/test/capgen_test/temp_adjust.F90 @@ -22,7 +22,7 @@ subroutine temp_adjust_run(foo, timestep, temp_prev, temp_layer, qv, ps, & integer, intent(in) :: foo real(kind_phys), intent(in) :: timestep - real(kind_phys), intent(inout) :: qv(:) + real(kind_phys), intent(inout),optional :: qv(:) real(kind_phys), intent(inout) :: ps(:) REAL(kind_phys), intent(in) :: temp_prev(:) REAL(kind_phys), intent(inout) :: temp_layer(foo) @@ -42,7 +42,7 @@ subroutine temp_adjust_run(foo, timestep, temp_prev, temp_layer, qv, ps, & do col_index = 1, foo temp_layer(col_index) = temp_layer(col_index) + temp_prev(col_index) - qv(col_index) = qv(col_index) + 1.0_kind_phys + if (present(qv)) qv(col_index) = qv(col_index) + 1.0_kind_phys end do if (present(innie) .and. present(outie) .and. present(optsie)) then outie = innie * optsie diff --git a/test/capgen_test/temp_adjust.meta b/test/capgen_test/temp_adjust.meta index fc03a434..17eabcdb 100644 --- a/test/capgen_test/temp_adjust.meta +++ b/test/capgen_test/temp_adjust.meta @@ -41,6 +41,7 @@ kind = kind_phys intent = inout diagnostic_name_fixed = Q + optional = True [ ps ] standard_name = surface_air_pressure state_variable = true diff --git a/test/var_compatibility_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 index 3a6caab0..6dbbb722 100644 --- a/test/var_compatibility_test/effr_calc.F90 +++ b/test/var_compatibility_test/effr_calc.F90 @@ -15,21 +15,24 @@ module effr_calc !> \section arg_table_effr_calc_run Argument Table !! \htmlinclude arg_table_effr_calc_run.html !! - subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & - effri_out, effrs_inout, has_graupel, scalar_var, & - errmsg, errflg) + subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, ncg_in, nci_out, & + effrl_inout, effri_out, effrs_inout, ncl_out, & + has_graupel, scalar_var, errmsg, errflg) integer, intent(in) :: ncol integer, intent(in) :: nlev real(kind_phys), intent(in) :: effrr_in(:,:) - real(kind_phys), intent(in) :: effrg_in(:,:) + real(kind_phys), intent(in),optional :: effrg_in(:,:) + real(kind_phys), intent(in),optional :: ncg_in(:,:) + real(kind_phys), intent(out),optional :: nci_out(:,:) real(kind_phys), intent(inout) :: effrl_inout(:,:) - real(kind_phys), intent(out) :: effri_out(:,:) + real(kind_phys), intent(out),optional :: effri_out(:,:) real(8),intent(inout) :: effrs_inout(:,:) logical, intent(in) :: has_graupel real(kind_phys), intent(inout) :: scalar_var character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg + real(kind_phys), intent(out),optional :: ncl_out(:,:) !---------------------------------------------------------------- real(kind_phys), parameter :: re_qc_min = 2.5 ! microns @@ -37,14 +40,18 @@ subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, effrl_inout, & real(kind_phys), parameter :: re_qi_avg = 75. ! microns real(kind_phys) :: effrr_local(ncol,nlev) real(kind_phys) :: effrg_local(ncol,nlev) + real(kind_phys) :: ncg_in_local(ncol,nlev) + real(kind_phys) :: nci_out_local(ncol,nlev) errmsg = '' errflg = 0 effrr_local = effrr_in - if (has_graupel) effrg_local = effrg_in + if (present(effrg_in)) effrg_local = effrg_in + if (present(ncg_in)) ncg_in_local = ncg_in + if (present(nci_out)) nci_out_local = nci_out effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) - effri_out = re_qi_avg + if (present(effri_out)) effri_out = re_qi_avg effrs_inout = effrs_inout + 10.0 ! in micrometer scalar_var = 2.0 ! in km diff --git a/test/var_compatibility_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta index d1712f3a..73c36ace 100644 --- a/test/var_compatibility_test/effr_calc.meta +++ b/test/var_compatibility_test/effr_calc.meta @@ -25,7 +25,7 @@ type = real kind = kind_phys intent = in - top_at_one = .true. + top_at_one = True [effrg_in] standard_name = effective_radius_of_stratiform_cloud_graupel long_name = effective radius of cloud graupel in micrometer @@ -34,6 +34,25 @@ type = real kind = kind_phys intent = in + optional = True +[ncg_in] + standard_name = cloud_graupel_number_concentration + long_name = number concentration of cloud graupel + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + optional = True +[nci_out] + standard_name = cloud_ice_number_concentration + long_name = number concentration of cloud ice + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out + optional = True [effrl_inout] standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle long_name = effective radius of cloud liquid water particle in micrometer @@ -50,6 +69,7 @@ type = real kind = kind_phys intent = out + optional = True [effrs_inout] standard_name = effective_radius_of_stratiform_cloud_snow_particle long_name = effective radius of cloud snow particle in micrometer @@ -58,7 +78,16 @@ type = real kind = 8 intent = inout - top_at_one = .true. + top_at_one = True +[ncl_out] + standard_name = cloud_liquid_number_concentration + long_name = number concentration of cloud liquid + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out + optional = True [has_graupel] standard_name = flag_indicating_cloud_microphysics_has_graupel long_name = flag indicating that the cloud microphysics produces graupel diff --git a/test/var_compatibility_test/run_test b/test/var_compatibility_test/run_test index 76ad982e..a5edac37 100755 --- a/test/var_compatibility_test/run_test +++ b/test/var_compatibility_test/run_test @@ -131,33 +131,41 @@ module_list="effr_calc" #dependencies="" suite_list="var_compatibility_suite" required_vars_var_compatibility="ccpp_error_code,ccpp_error_message" +required_vars_var_compatibility="${required_vars_var_compatibility},cloud_graupel_number_concentration" +required_vars_var_compatibility="${required_vars_var_compatibility},cloud_ice_number_concentration" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_graupel" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" required_vars_var_compatibility="${required_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" required_vars_var_compatibility="${required_vars_var_compatibility},flag_indicating_cloud_microphysics_has_graupel" +required_vars_var_compatibility="${required_vars_var_compatibility},flag_indicating_cloud_microphysics_has_ice" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_dimension" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_begin" required_vars_var_compatibility="${required_vars_var_compatibility},horizontal_loop_end" required_vars_var_compatibility="${required_vars_var_compatibility},scalar_variable_for_testing" required_vars_var_compatibility="${required_vars_var_compatibility},vertical_layer_dimension" -input_vars_var_compatibility="effective_radius_of_stratiform_cloud_graupel" +input_vars_var_compatibility="cloud_graupel_number_concentration" +#input_vars_var_compatibility="${input_vars_var_compatibility},cloud_ice_number_concentration" +input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_graupel" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_rain_particle" input_vars_var_compatibility="${input_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" input_vars_var_compatibility="${input_vars_var_compatibility},flag_indicating_cloud_microphysics_has_graupel" +input_vars_var_compatibility="${input_vars_var_compatibility},flag_indicating_cloud_microphysics_has_ice" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_dimension" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_begin" input_vars_var_compatibility="${input_vars_var_compatibility},horizontal_loop_end" input_vars_var_compatibility="${input_vars_var_compatibility},scalar_variable_for_testing" input_vars_var_compatibility="${input_vars_var_compatibility},vertical_layer_dimension" output_vars_var_compatibility="ccpp_error_code,ccpp_error_message" +output_vars_var_compatibility="${output_vars_var_compatibility},cloud_ice_number_concentration" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_ice_particle" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_liquid_water_particle" output_vars_var_compatibility="${output_vars_var_compatibility},effective_radius_of_stratiform_cloud_snow_particle" output_vars_var_compatibility="${output_vars_var_compatibility},scalar_variable_for_testing" + ## ## Run a database report and check the return string ## $1 is the report program file diff --git a/test/var_compatibility_test/test_host.F90 b/test/var_compatibility_test/test_host.F90 index 6e170607..3a805a8e 100644 --- a/test/var_compatibility_test/test_host.F90 +++ b/test/var_compatibility_test/test_host.F90 @@ -351,23 +351,26 @@ program test character(len=cs), target :: test_parts1(1) = (/ 'radiation ' /) - character(len=cm), target :: test_invars1(6) = (/ & + character(len=cm), target :: test_invars1(8) = (/ & 'effective_radius_of_stratiform_cloud_rain_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ', & 'effective_radius_of_stratiform_cloud_graupel ', & + 'cloud_graupel_number_concentration ', & 'scalar_variable_for_testing ', & - 'flag_indicating_cloud_microphysics_has_graupel '/) + 'flag_indicating_cloud_microphysics_has_graupel ', & + 'flag_indicating_cloud_microphysics_has_ice '/) - character(len=cm), target :: test_outvars1(6) = (/ & + character(len=cm), target :: test_outvars1(7) = (/ & 'ccpp_error_code ', & 'ccpp_error_message ', & 'effective_radius_of_stratiform_cloud_ice_particle ', & 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'cloud_ice_number_concentration ', & 'scalar_variable_for_testing ' /) - character(len=cm), target :: test_reqvars1(9) = (/ & + character(len=cm), target :: test_reqvars1(12) = (/ & 'ccpp_error_code ', & 'ccpp_error_message ', & 'effective_radius_of_stratiform_cloud_rain_particle ', & @@ -375,8 +378,11 @@ program test 'effective_radius_of_stratiform_cloud_liquid_water_particle', & 'effective_radius_of_stratiform_cloud_snow_particle ', & 'effective_radius_of_stratiform_cloud_graupel ', & + 'cloud_graupel_number_concentration ', & + 'cloud_ice_number_concentration ', & 'scalar_variable_for_testing ', & - 'flag_indicating_cloud_microphysics_has_graupel '/) + 'flag_indicating_cloud_microphysics_has_graupel ', & + 'flag_indicating_cloud_microphysics_has_ice '/) type(suite_info) :: test_suites(1) logical :: run_okay diff --git a/test/var_compatibility_test/test_host_data.F90 b/test/var_compatibility_test/test_host_data.F90 index 1980f95a..9d0ca306 100644 --- a/test/var_compatibility_test/test_host_data.F90 +++ b/test/var_compatibility_test/test_host_data.F90 @@ -9,7 +9,9 @@ module test_host_data effrr, & ! effective radius of cloud rain effrl, & ! effective radius of cloud liquid water effri, & ! effective radius of cloud ice - effrg ! effective radius of cloud graupel + effrg, & ! effective radius of cloud graupel + ncg, & ! number concentration of cloud graupel + nci ! number concentration of cloud ice real(kind_phys) :: scalar_var end type physics_state @@ -17,11 +19,12 @@ module test_host_data contains - subroutine allocate_physics_state(cols, levels, state, has_graupel) + subroutine allocate_physics_state(cols, levels, state, has_graupel, has_ice) integer, intent(in) :: cols integer, intent(in) :: levels type(physics_state), intent(out) :: state logical, intent(in) :: has_graupel + logical, intent(in) :: has_ice if (allocated(state%effrr)) then deallocate(state%effrr) @@ -33,16 +36,30 @@ subroutine allocate_physics_state(cols, levels, state, has_graupel) end if allocate(state%effrl(cols, levels)) - if (allocated(state%effri)) then - deallocate(state%effri) - end if - allocate(state%effri(cols, levels)) + if (has_ice) then + if (allocated(state%effri)) then + deallocate(state%effri) + end if + allocate(state%effri(cols, levels)) + endif if (has_graupel) then if (allocated(state%effrg)) then deallocate(state%effrg) end if allocate(state%effrg(cols, levels)) + + if (allocated(state%ncg)) then + deallocate(state%ncg) + end if + allocate(state%ncg(cols, levels)) + endif + + if (has_ice) then + if (allocated(state%nci)) then + deallocate(state%nci) + end if + allocate(state%nci(cols, levels)) endif end subroutine allocate_physics_state diff --git a/test/var_compatibility_test/test_host_data.meta b/test/var_compatibility_test/test_host_data.meta index 01ad7a4a..d3bca89b 100644 --- a/test/var_compatibility_test/test_host_data.meta +++ b/test/var_compatibility_test/test_host_data.meta @@ -25,6 +25,7 @@ dimensions = (horizontal_dimension,vertical_layer_dimension) type = real kind = kind_phys + active = (flag_indicating_cloud_microphysics_has_ice) [effrg] standard_name = effective_radius_of_stratiform_cloud_graupel long_name = effective radius of cloud graupel in meter @@ -33,6 +34,24 @@ type = real kind = kind_phys active = (flag_indicating_cloud_microphysics_has_graupel) +[ncg] + standard_name = cloud_graupel_number_concentration + long_name = number concentration of cloud graupel + units = kg-1 + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + active = (flag_indicating_cloud_microphysics_has_graupel) +[nci] + standard_name = cloud_ice_number_concentration + long_name = number concentration of cloud ice + units = kg-1 + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + active = (flag_indicating_cloud_microphysics_has_ice) [scalar_var] standard_name = scalar_variable_for_testing long_name = unused scalar variable diff --git a/test/var_compatibility_test/test_host_mod.F90 b/test/var_compatibility_test/test_host_mod.F90 index 877e3432..ca1d2014 100644 --- a/test/var_compatibility_test/test_host_mod.F90 +++ b/test/var_compatibility_test/test_host_mod.F90 @@ -13,7 +13,8 @@ module test_host_mod integer, parameter :: pver = 4 type(physics_state) :: phys_state real(kind_phys) :: effrs(ncols, pver) - logical, parameter :: mp_has_graupel = .true. + logical, parameter :: has_ice = .true. + logical, parameter :: has_graupel = .true. public :: init_data public :: compare_data @@ -23,14 +24,18 @@ module test_host_mod subroutine init_data() ! Allocate and initialize state - call allocate_physics_state(ncols, pver, phys_state, mp_has_graupel) + call allocate_physics_state(ncols, pver, phys_state, has_graupel, has_ice) phys_state%effrr = 1.0E-3 ! 1000 microns, in meter phys_state%effrl = 1.0E-4 ! 100 microns, in meter - phys_state%effri = 5.0E-5 ! 50 microns, in meter phys_state%scalar_var = 1.0 ! in m effrs = 5.0E-4 ! 500 microns, in meter - if (mp_has_graupel) then + if (has_graupel) then phys_state%effrg = 2.5E-4 ! 250 microns, in meter + phys_state%ncg = 40 + endif + if (has_ice) then + phys_state%effri = 5.0E-5 ! 50 microns, in meter + phys_state%nci = 80 endif end subroutine init_data diff --git a/test/var_compatibility_test/test_host_mod.meta b/test/var_compatibility_test/test_host_mod.meta index 7789dbed..51a2f5c3 100644 --- a/test/var_compatibility_test/test_host_mod.meta +++ b/test/var_compatibility_test/test_host_mod.meta @@ -28,7 +28,13 @@ dimensions = (horizontal_dimension,vertical_layer_dimension) type = real kind = kind_phys -[mp_has_graupel] +[has_ice] + standard_name = flag_indicating_cloud_microphysics_has_ice + long_name = flag indicating that the cloud microphysics produces ice + units = flag + dimensions = () + type = logical +[has_graupel] standard_name = flag_indicating_cloud_microphysics_has_graupel long_name = flag indicating that the cloud microphysics produces graupel units = flag diff --git a/test/var_compatibility_test/test_reports.py b/test/var_compatibility_test/test_reports.py index fd5e5203..aaf08953 100755 --- a/test/var_compatibility_test/test_reports.py +++ b/test/var_compatibility_test/test_reports.py @@ -72,12 +72,15 @@ def usage(errmsg=None): "effective_radius_of_stratiform_cloud_rain_particle", "effective_radius_of_stratiform_cloud_snow_particle", "effective_radius_of_stratiform_cloud_graupel", + "cloud_graupel_number_concentration", "scalar_variable_for_testing", - "flag_indicating_cloud_microphysics_has_graupel"] + "flag_indicating_cloud_microphysics_has_graupel", + "flag_indicating_cloud_microphysics_has_ice"] _OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", "effective_radius_of_stratiform_cloud_ice_particle", "effective_radius_of_stratiform_cloud_liquid_water_particle", "effective_radius_of_stratiform_cloud_snow_particle", + "cloud_ice_number_concentration", "scalar_variable_for_testing"] _REQUIRED_VARS_VAR_ACTION = _INPUT_VARS_VAR_ACTION + _OUTPUT_VARS_VAR_ACTION From 6cdd38a032d89d0f79c66f37f0c7172e59f135cd Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Fri, 8 Mar 2024 09:53:27 -0700 Subject: [PATCH 49/49] Update scripts/parse_tools/parse_checkers.py to allow for underscores in unit metadata attribues --- scripts/parse_tools/parse_checkers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index f32df45d..1a0d1565 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -14,10 +14,11 @@ _UNITLESS_REGEX = "1" _NON_LEADING_ZERO_NUM = "[1-9]\d*" +_CHAR_WITH_UNDERSCORE = "([a-zA-Z]+_[a-zA-Z]+)+" _NEGATIVE_NON_LEADING_ZERO_NUM = f"[-]{_NON_LEADING_ZERO_NUM}" _UNIT_EXPONENT = f"({_NEGATIVE_NON_LEADING_ZERO_NUM}|{_NON_LEADING_ZERO_NUM})" _UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" -_UNITS_REGEX = f"^({_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" +_UNITS_REGEX = f"^({_CHAR_WITH_UNDERSCORE}|{_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" _UNITS_RE = re.compile(_UNITS_REGEX) _MAX_MOLAR_MASS = 10000.0