diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml new file mode 100644 index 00000000..c2d52ee8 --- /dev/null +++ b/.github/workflows/capgen_unit_tests.yaml @@ -0,0 +1,17 @@ +name: Capgen Unit Tests + +on: + workflow_dispatch: + pull_request: + branches: [feature/capgen, main] + +jobs: + unit_tests: + 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_fortran_tests.sh + diff --git a/.github/workflows/prebuild.yaml b/.github/workflows/prebuild.yaml index 80e34608..3f8085fb 100644 --- a/.github/workflows/prebuild.yaml +++ b/.github/workflows/prebuild.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10"] + python-version: ["3.8","3.9","3.10","3.11","3.12"] steps: - uses: actions/checkout@v4 @@ -35,8 +35,7 @@ jobs: run: | export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools cd test_prebuild - python3 test_metadata_parser.py - python3 test_mkstatic.py + pytest - name: ccpp-prebuild blocked data tests run: | cd test_prebuild/test_blocked_data diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml deleted file mode 100644 index c630c438..00000000 --- a/.github/workflows/python.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Python package - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.7] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - 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: Test with pytest - if: github.repository == 'NCAR/ccpp-framework' # Only run on main repo - run: | - export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools - pytest diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b7aff41..90634bee 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/CODEOWNERS b/CODEOWNERS index 39541807..b5662f11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,7 +3,7 @@ # These owners will be the default owners for everything in the repo. #* @defunkt -* @climbfuji @grantfirl @gold2718 @mkavulich +* @climbfuji @gold2718 @dustinswales @mwaxmonsky @peverwhee @grantfirl @mkavulich # Order is important. The last matching pattern has the most precedence. # So if a pull request only touches javascript files, only these owners 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/pytest.ini b/pytest.ini deleted file mode 100644 index 83323d6a..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -addopts = -ra -q --ignore=tests/test_capgen.py -testpaths = - tests \ No newline at end of file diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 7a12729a..042f6d16 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 @@ -26,11 +27,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 +46,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): ############################################################################### @@ -306,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 @@ -559,7 +578,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.""" @@ -578,12 +597,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,14 +623,21 @@ 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 + 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) - 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.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.logger and run_env.logger.isEnabledFor(logging.DEBUG): + 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])) @@ -628,7 +664,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() @@ -646,10 +683,13 @@ 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) + if return_db: + return CCPPDatabaseObj(run_env, host_model=host_model, api=ccpp_api) + # end if + return None ############################################################################### def _main_func(): @@ -665,7 +705,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..24579750 --- /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: + """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): + """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 = "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") + + 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..3c5092ed 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,12 +648,13 @@ 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", "kind", "persistence", "polymorphic", "protected", - "state_variable", "type", "units"]) + "state_variable", "type", "units", "molar_mass", + "advected", "top_at_one", "optional"]) prop_list.extend(Var.constituent_property_names()) # end if ventry = ET.SubElement(parent, "var") @@ -671,9 +668,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_suite.py b/scripts/ccpp_suite.py index 8b9a1aeb..d19ca4e8 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -17,12 +17,12 @@ 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 from suite_objects import CallList, Group, Scheme +from metavar import CCPP_LOOP_VAR_STDNAMES # pylint: disable=too-many-lines @@ -31,12 +31,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, @@ -287,19 +287,22 @@ 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 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) @@ -428,7 +431,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.verbose: run_env.logger.debug(lmsg.format(item.name, [x.name for x in item.schemes()])) @@ -491,7 +494,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.verbose: run_env.logger.debug('Writing CCPP suite file, {}'.format(filename)) # end if # Retrieve the name of the constituent module for Group use statements @@ -730,9 +733,8 @@ 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) + 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 +743,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} = 0", 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 +753,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,35 +809,49 @@ 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 + 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 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 = 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': if isinstance(elements, list): output_vars[1].add(stdname) 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/common.py b/scripts/common.py index f115aa80..c33f78fe 100755 --- a/scripts/common.py +++ b/scripts/common.py @@ -173,14 +173,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/constituents.py b/scripts/constituents.py index be8d4774..6b0ac759 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 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" ######################################################################## @@ -31,10 +28,9 @@ 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" - __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, @@ -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=}', 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"]}'] + 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) 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,24 @@ def write_constituent_use_statements(cap, suite_list, indent): # end for @staticmethod - def write_host_routines(cap, host, reg_funcname, num_const_funcname, - copy_in_funcname, copy_out_funcname, const_obj_name, - const_names_name, const_indices_name, - suite_list, err_vars): + def write_host_routines(cap, host, reg_funcname, init_funcname, num_const_funcname, + 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 . is a list of the host model's error variables. 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 + : 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,143 +491,257 @@ 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) - cap.write("", 0) + 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.blank_line() 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.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) :: " + \ + "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("", 0) + cap.write(f"type({CONST_PROP_TYPE}), pointer :: const_prop", 2) + cap.blank_line() 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.blank_line() + # 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("", 0) + 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.blank_line() # 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("", 0) - cap.write("integer {}({})".format(substmt, err_dummy_str), 1) - cap.write("! Return the number of constituent fields for this run", 2) - cap.write("", 0) - cap.write("! Dummy arguments", 2) + 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.blank_line() + cap.write(f"{substmt}(ncols, num_layers, {err_dummy_str})", 1) + cap.comment("Initialize constituent data", 2) + 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.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.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.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.write("{} = {}%num_constituents({})".format(num_const_funcname, - const_obj_name, - obj_err_callstr), 2) + 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) - # Next, write copy_in routine + # 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) + cap.blank_line() cap.write("{}(const_array, {})".format(substmt, err_dummy_str), 1) - cap.write("! Copy constituent field info into ", 2) - cap.write("", 0) - cap.write("! Dummy arguments", 2) + cap.comment("Copy constituent field info into ", 2) + 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) - # Next, write copy_out routine + # 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.write("! Update constituent field info from ", 2) - cap.write("", 0) - cap.write("! Dummy arguments", 2) + cap.comment("Update constituent field info from ", 2) + 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) + obj_err_callstr), + 2) + cap.write("end {}".format(substmt), 1) + # Write constituents routine + cap.blank_line() + cap.write(f"function {const_array_func}() result(const_ptr)", 1) + cap.blank_line() + cap.comment("Return pointer to constituent array", 2) + cap.blank_line() + cap.comment("Dummy argument", 2) + cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) + 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.blank_line() + cap.write(f"function {advect_array_func}() result(const_ptr)", 1) + cap.blank_line() + cap.comment("Return pointer to advected constituent array", 2) + cap.blank_line() + cap.comment("Dummy argument", 2) + cap.write("real(kind_phys), pointer :: const_ptr(:,:,:)", 2) + 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.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.blank_line() + cap.comment("Return pointer to array of constituent properties", 2) + cap.blank_line() + cap.comment("Dummy argument", 2) + cap.write("type(ccpp_constituent_prop_ptr_t), pointer :: const_ptr(:)", + 2) + 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.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.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.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) @staticmethod @@ -636,20 +765,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/ddt_library.py b/scripts/ddt_library.py index 30614226..72fe48b8 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.verbose): 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): @@ -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? @@ -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 run_env.verbose: + lmsg = f"Adding DDT {ddt.title} to {self.name}" + run_env.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): + ntx = context_string(dvar.context) + ctx = context_string(pvar.context) + emsg = f"Attempt to add duplicate DDT sub-variable, {stdname}{ntx}." + emsg += f"\nVariable originally defined{ctx}" + 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/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..5b186c1c 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().startswith('!'): + 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..e7d3c495 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) ######################################################################## @@ -665,12 +667,16 @@ 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') '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') @@ -720,8 +726,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: @@ -826,15 +832,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/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index f37b3377..a67816e4 100644 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -563,29 +563,37 @@ 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.verbose: 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.verbose: + 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 @@ -616,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.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = "Parsing specification of {}{}" run_env.logger.debug(msg.format(table_name, ctx)) @@ -786,6 +794,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 @@ -802,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.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: ctx = tbl.start_context() mtype = tbl.table_type msg = "Adding metadata from {}, {}{}" @@ -812,6 +821,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 @@ -876,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.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: ctx = context_string(pobj, nodir=True) msg = "Parsing Fortran module, {}{}" run_env.logger.debug(msg.format(mod_name, ctx)) @@ -912,7 +928,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.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 8ee553f4..88c9c204 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 verbose _EPILOG = ''' ''' @@ -24,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). @@ -103,8 +105,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 @@ -197,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: @@ -275,6 +284,12 @@ def kind_types(self): CCPPFrameworkEnv object.""" return self.__kind_dict.keys() + @property + def verbose(self): + """Return true if debug enabled for the CCPPFrameworkEnv's + logger object.""" + return (self.logger and verbose(self.logger)) + @property def use_error_obj(self): """Return the property for this @@ -304,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.""" @@ -379,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/host_cap.py b/scripts/host_cap.py index 9c88cf34..d2c4ed7e 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,13 +78,28 @@ 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 f"{host_model.name}_ccpp_num_suite_constituents" + ############################################################################### def constituent_register_subname(host_model): ############################################################################### - """Return the name of the subroutine used to register (initialize) the + """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 f"{host_model.name}_ccpp_register_constituents" + +############################################################################### +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_register_constituents".format(host_model.name) + return f"{host_model.name}_ccpp_initialize_constituents" ############################################################################### def constituent_num_consts_funcname(host_model): @@ -93,7 +107,15 @@ 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): +############################################################################### + """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): @@ -101,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): @@ -109,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): @@ -129,23 +151,57 @@ 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): ############################################################################### """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) + +############################################################################### +def constituent_model_consts(host_model): +############################################################################### + """Return the name of the function that will return a pointer to the + array of all constituents""" + hstr = f"{host_model.name}_constituents_array" + 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 = 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 = f"{host_model.name}_model_const_properties" + 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 = f"{host_model.name}_const_get_index" return unique_local_name(hstr, host_model) ############################################################################### @@ -161,52 +217,29 @@ 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 = "number_of_ccpp_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_constituents", " 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() for suite in suite_list: - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: lmsg = "Adding constituents from {} to {}" run_env.logger.debug(lmsg.format(suite.name, host_model.name)) # end if @@ -235,8 +268,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 +275,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 +411,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 +437,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}" @@ -420,21 +452,33 @@ def write_host_cap(host_model, api, 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(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) + 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 +519,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 +607,16 @@ 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, - numconsts_name, copyin_name, - copyout_name, const_obj_name, + ConstituentVarDict.write_host_routines(cap, host_model, reg_name, init_name, + numconsts_name, queryconsts_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..c655421b 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,8 @@ 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. + - dictionary key is title of metadata argtable is the name for this host model. is the CCPPFrameworkEnv object for this framework run. """ @@ -35,11 +37,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 +114,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 +148,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? + return f"{self.name}_ccpp_constituents" def argument_list(self, loop_vars=True): """Return a string representing the host model variable arg list""" @@ -157,8 +172,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 +187,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 +316,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/metadata_table.py b/scripts/metadata_table.py index 2ed71c2e..946e9782 100755 --- 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): @@ -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", \ @@ -686,7 +690,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) @@ -696,7 +700,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: @@ -706,7 +710,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] @@ -788,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.verbose: run_env.logger.info("Parsing {} {}{}".format(self.header_type, self.title, start_ctx)) # end if @@ -808,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.verbose: dmsg = 'Adding {} to {}' lname = newvar.get_prop_value('local_name') run_env.logger.debug(dmsg.format(lname, self.title)) @@ -901,7 +905,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) @@ -988,7 +992,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 == ':': @@ -1023,7 +1027,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: @@ -1049,7 +1053,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') @@ -1069,7 +1073,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 @@ -1083,15 +1087,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 @@ -1310,15 +1314,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 b50e806b..2974f1db 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -20,8 +20,10 @@ 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 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 @@ -116,8 +118,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') @@ -142,9 +150,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 - 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'}, \ + 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 @@ -186,7 +204,13 @@ class Var: VariableProperty('active', str, optional_in=True, default_in='.true.'), VariableProperty('polymorphic', bool, optional_in=True, - default_in='.false.')] + 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)] # XXgoldyXX: v debug only __to_add = VariableProperty('valid_values', str, @@ -202,7 +226,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} @@ -251,7 +278,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 @@ -262,7 +289,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) @@ -278,6 +305,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 @@ -354,15 +383,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): @@ -466,7 +497,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 @@ -486,7 +517,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: @@ -600,7 +631,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 " @@ -677,9 +709,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 = '' @@ -739,7 +777,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. @@ -754,10 +792,26 @@ def intrinsic_elements(self, check_dict=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(): - 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: + errmsg = f'No ddt_lib or ddt {dtitle} not in ddt_lib' + raise CCPPError(errmsg) + # end if # end if children = self.children() if (not children) and check_dict: @@ -812,7 +866,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): @@ -834,6 +888,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""" @@ -842,13 +901,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') @@ -864,7 +923,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): @@ -921,8 +980,55 @@ def has_vertical_dimension(self, dims=None): # end if return find_vertical_dimension(vdims)[0] - def write_def(self, outfile, indent, wdict, allocatable=False, - dummy=False, add_intent=None, extra_space=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, 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. If is True but the variable has no intent, add the @@ -973,24 +1079,30 @@ 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 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})" + elif optional: + intent_str = f"intent({intent}),{' '*(5 - len(intent))}" + intent_str += 'target, optional ' else: - intent_str = 'intent({}){}'.format(intent, - ' '*(5 - len(intent))) + intent_str = f"intent({intent}){' '*(5 - len(intent))}" # end if elif not dummy: - intent_str = '' + intent_str = ' '*20 else: intent_str = ' '*13 # end if @@ -999,27 +1111,49 @@ 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}" - 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 @@ -1369,11 +1503,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') @@ -1417,7 +1551,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(f'Illegal type for variables, {type_name(variables)} in {self.name}') # end if @property @@ -1598,7 +1732,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, @@ -1613,15 +1747,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, '{}'{}" lname = dvar.get_prop_value('local_name') dctx = context_string(dvar.context) - err_ret = err_ret.format(lname, dvar.source.type, dctx) + err_ret += f"\nFound {lname} from excluded source, '{dvar.source.ptype}'{dctx}" # end if # end if # end if @@ -1972,11 +2104,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/mkstatic.py b/scripts/mkstatic.py index 99b9c231..57067d4b 100755 --- a/scripts/mkstatic.py +++ b/scripts/mkstatic.py @@ -1316,7 +1316,7 @@ def write(self, metadata_request, metadata_define, arguments, debug): if container == var.container: break - + # We need some information about the host model variable (dimensions_target_name, dim_string_target_name) = extract_dimensions_from_local_name(var.target) # Derive correct horizontal loop extent for this variable for the rest of this function @@ -1359,13 +1359,31 @@ def write(self, metadata_request, metadata_define, arguments, debug): if ccpp_stage == 'run' and dims[1].lower() == CCPP_HORIZONTAL_LOOP_EXTENT: # Provide backward compatibility with blocked data structures # and bypass the unresolved problems with inactive data - if CCPP_HORIZONTAL_LOOP_BEGIN in metadata_define.keys(): + + # For this, we need to check if the host model variable + # is a contiguous array (it's horizontal dimension is + # CCPP_HORIZONTAL_DIMENSION) or part of a blocked data + # structure (it's horizontal dimension is CCPP_HORIZONTAL_LOOP_EXTENT) + for dim in metadata_define[var_standard_name][0].dimensions: + if ':' in dim: + host_var_dims = [x.lower() for x in dim.split(':')] + # Single dimensions are indices and should not be recorded as a dimension! + else: + raise Exception("THIS SHOULD NOT HAPPEN WITH CAPGEN'S METADATA PARSER") + if CCPP_HORIZONTAL_DIMENSION in host_var_dims: + host_var_is_contiguous = True + elif CCPP_HORIZONTAL_LOOP_EXTENT in host_var_dims: + host_var_is_contiguous = False + if CCPP_HORIZONTAL_LOOP_BEGIN in metadata_define.keys() and host_var_is_contiguous: dim0 = metadata_define[CCPP_HORIZONTAL_LOOP_BEGIN][0].local_name dim1 = metadata_define[CCPP_HORIZONTAL_LOOP_END][0].local_name use_explicit_dimension = True else: dim0 = metadata_define[CCPP_CONSTANT_ONE][0].local_name dim1 = metadata_define[CCPP_HORIZONTAL_LOOP_EXTENT][0].local_name + # Remove this variable so that we can catch errors + # if it doesn't get set even though it should + del host_var_is_contiguous else: if not dims[1].lower() in metadata_define.keys(): raise Exception('Dimension {}, required by variable {}, not defined in host model metadata'.format( diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 03dd0429..309bbda0 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 @@ -22,14 +22,15 @@ 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 +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 from xml_tools import PrettyElementTree +from fortran_conditional import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX # pylint: enable=wrong-import-position __all__ = [ @@ -47,6 +48,7 @@ 'check_fortran_type', 'check_local_name', 'check_valid_values', + 'check_molar_mass', 'context_string', 'find_schema_file', 'find_schema_version', @@ -71,6 +73,9 @@ 'set_log_to_file', 'set_log_to_null', '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/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index 487478e6..1a0d1565 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -12,7 +12,15 @@ ######################################################################## -_UNITS_RE = re.compile(r"^[^/@#$%^&*()\|<>\[\]{}?,.]+$") +_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"^({_CHAR_WITH_UNDERSCORE}|{_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 @@ -936,6 +944,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(f"{test_val} is not a valid molar mass") + else: + test_val = None + # end if + # end if + except: + # not an int or float, conditionally throw error + if error: + raise CCPPError(f"{test_val} is invalid; not a float or int") + 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/parse_tools/parse_log.py b/scripts/parse_tools/parse_log.py index e6561427..f85a5d09 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 verbose(logger): + """Return true if debug is enabled for this logger""" + return logger.isEnabledFor(logging.DEBUG) diff --git a/scripts/parse_tools/parse_object.py b/scripts/parse_tools/parse_object.py index 4518d75c..2c5f72f5 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() @@ -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""" @@ -170,12 +164,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..38a72953 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,13 +198,13 @@ 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...> + >>> 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 @@ -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,31 +386,23 @@ 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 ######################################################################## - -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..0ff56b3a 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -7,40 +7,42 @@ # 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): ############################################################################### """ 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: @@ -53,35 +55,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 +69,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 +81,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 +99,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 +142,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 +159,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 = 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): 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 +204,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 +230,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 +250,20 @@ 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") - else: - fmode = 'w' - root = input + 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") indent = 0 last_write_text = False with open(file, fmode) as outfile: @@ -350,19 +317,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/suite_objects.py b/scripts/suite_objects.py old mode 100644 new mode 100755 index 472556cb..37978d54 --- 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 @@ -90,6 +91,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 @@ -126,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) @@ -361,7 +383,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 +455,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) @@ -800,7 +822,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 @@ -809,21 +831,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: @@ -831,7 +851,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. @@ -857,6 +877,20 @@ def match_variable(self, var, vstdname=None, vdims=None): 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: sdict = {} @@ -878,7 +912,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, 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""" @@ -1061,6 +1095,10 @@ 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() + self.__optional_vars = list() super().__init__(name, context, parent, run_env, active_call_list=True) def update_group_call_list_variable(self, var): @@ -1128,9 +1166,14 @@ 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, 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 @@ -1184,6 +1227,23 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): vstdname)) # 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: self.parent.add_part(self, replace=True) # Should add a vloop @@ -1195,7 +1255,425 @@ def analyze(self, phase, group, scheme_library, suite_vars, level): # end if return scheme_mods - def write(self, outfile, errcode, indent): + 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') + tmp_indent = indent + if conditional != '.true.': + tmp_indent = indent + 1 + outfile.write(f"if {conditional} then", indent) + # end if + 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('',tmp_indent) + if conditional != '.true.': + outfile.write(f"end if", indent) + # 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: + 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 + tmp_indent = indent + if conditional != '.true.': + tmp_indent = indent + 1 + outfile.write(f"if {conditional} then", indent) + # 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) + if conditional != '.true.': + outfile.write(f"end if", 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) + # end if + 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 + 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()) + 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 + #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) + # end if + outfile.write(stmt, 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 """ @@ -1206,9 +1684,76 @@ 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('', 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) + # 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 + 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) + 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 + 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) def schemes(self): @@ -1271,7 +1816,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.verbose: lmsg = "Adding VerticalLoop for '{}'" run_env.logger.debug(lmsg.format(index_name)) # end if @@ -1304,6 +1849,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)) @@ -1324,13 +1873,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) @@ -1389,12 +1938,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) @@ -1439,11 +1988,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 ############################################################################### @@ -1468,7 +2017,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') @@ -1716,7 +2265,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) @@ -1737,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 @@ -1781,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) @@ -1793,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=(key in allocatable_var_set)) + 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), + 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 @@ -1822,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: @@ -1867,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, 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/scripts/var_props.py b/scripts/var_props.py old mode 100644 new mode 100755 index b7d7b16f..9a2fd7d0 --- 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 @@ -133,20 +134,24 @@ 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 + # 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'] @@ -191,18 +196,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 +286,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): @@ -502,28 +516,28 @@ 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).type == str + >>> VariableProperty('standard_name', str).ptype == str True >>> VariableProperty('units', str).is_match('units') True @@ -535,16 +549,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,18 +573,22 @@ 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)') '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.'] @@ -580,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 @@ -612,7 +632,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 +679,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 +691,22 @@ 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 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) if tval and (len(tval) == 1) and (not tval[0]): @@ -698,7 +733,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 +742,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: @@ -746,36 +781,89 @@ class VarCompatObj: character(len=) # Test that we can create a standard VarCompatObj object - >>> 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...> + >>> 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", 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 - <__main__.VarCompatObj object at 0x...> + >>> 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 - <__main__.VarCompatObj object at 0x...> + >>> 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 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 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, \ + _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", 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", 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. """ @@ -790,6 +878,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: @@ -855,14 +944,18 @@ 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 - ## 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 @@ -874,13 +967,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 @@ -892,16 +987,15 @@ 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) + # Dimension transform (Indices handled externally) + if len(rvar_indices) == 0: + rhs_term = f"{rvar_lname}" + lhs_term = f"{lvar_lname}" else: - lhs_term = f"{lvar_lname}({','.join(indices)})" + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" # end if - rhs_term = f"{rvar_lname}({','.join(indices)})" + if self.has_kind_transforms: kind = self.__kind_transforms[1] rhs_term = f"real({rhs_term}, {kind})" @@ -918,13 +1012,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 @@ -936,16 +1032,15 @@ 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) + # Dimension transforms (Indices handled externally) + if len(rvar_indices) == 0: + rhs_term = f"{rvar_lname}" + lhs_term = f"{lvar_lname}" else: - lhs_term = f"{lvar_lname}({','.join(indices)})" + lhs_term = f"{lvar_lname}({','.join(lvar_indices)})" + rhs_term = f"{rvar_lname}({','.join(rvar_indices)})" # end if - rhs_term = f"{rvar_lname}({','.join(indices)})" + if self.has_kind_transforms: kind = self.__kind_transforms[0] rhs_term = f"real({rhs_term}, {kind})" @@ -969,6 +1064,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", False, "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", False, _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) @@ -1003,6 +1118,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", False, "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", False, _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}') @@ -1011,9 +1146,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) @@ -1048,20 +1190,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", False, "var_stdname", \ + "real", "kind_phys", "m", [], \ + "var2_lname", False, _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'], \ @@ -1069,7 +1231,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', \ @@ -1157,8 +1319,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.""" @@ -1168,11 +1329,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): @@ -1270,25 +1435,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_VCOMPAT = VarCompatObj("var_stdname", "real", "kind_phys", - "m", [], "var1_lname", "var_stdname", - "real", "kind_phys", "m", [], - "var2_lname", _DOCTEST_RUNENV) - fail, _ = doctest.testmod() - sys.exit(fail) -# end if diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index 419af297..41d8213f 100644 --- a/src/ccpp_constituent_prop_mod.F90 +++ b/src/ccpp_constituent_prop_mod.F90 @@ -10,99 +10,183 @@ 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. + 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 + 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 :: 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 + 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 - procedure :: deallocate => ccp_deallocate - procedure :: set_const_index => ccp_set_const_index - procedure :: set_field_index => ccp_set_field_index + ! 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_thermo_active => ccp_set_thermo_active 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_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 + 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 + procedure :: set_thermo_active => ccpt_set_thermo_active + 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 append_errvars private handle_allocate_error + private check_var_bounds CONTAINS @@ -121,43 +205,116 @@ 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 !####################################################################### - subroutine handle_allocate_error(astat, fieldname, errcode, errmsg) + 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 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 + + 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) + 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, 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 - 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 append_errvars(astat, "Error allocating ccpp_constituent_properties_t object component " // & + 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, 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), subname, errcode=errcode, errmsg=errmsg) + end if + end subroutine check_var_bounds + + !####################################################################### + function ccp_properties_get_key(hashable) ! Return the constituent properties class key (var_std_name) @@ -171,56 +328,46 @@ 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 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_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 append_errvars(1, "ccpp_constituent_properties_t object is not initialized", & + subname, errcode=errcode, errmsg=errmsg) 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 +378,39 @@ 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 + ! 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 + this%const_type = number_concentration + else + this%const_type = mass_mixing_ratio + end if + ! 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 +429,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 +448,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 +467,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 +486,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 +502,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 +517,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 +532,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 +542,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 !####################################################################### @@ -400,18 +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_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 append_errvars(1, "ccpp_constituent_properties_t const index " // & + "is already set", subname, errcode=errcode, errmsg=errmsg) end if end if @@ -419,304 +583,501 @@ 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 + 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 - integer, intent(in) :: findex + logical, intent(in) :: thermo_flag 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 + !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_set_field_index + end subroutine ccp_is_thermo_active !####################################################################### - 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) .and. & + (this%thermo_active .eqv. oconst%thermo_active) 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 = '' + if (this%is_instantiated(errcode, errmsg)) then + val_out = this%const_type == mass_mixing_ratio + else + val_out = .false. 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 + end subroutine ccp_is_mass_mixing_ratio + + !######################################################################## + + subroutine ccp_is_volume_mixing_ratio(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 == 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 - write(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_locked + !######################################################################## + + 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 !######################################################################## - 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_dry(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_water == dry_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 function ccp_model_const_okay_to_add + end subroutine ccp_is_dry !######################################################################## - subroutine ccp_model_const_add_metadata(this, field_data, errcode, errmsg) - ! Add a constituent's metadata to the master hash table + subroutine ccp_is_moist(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 == moist_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_moist !######################################################################## - subroutine ccp_model_const_initialize(this, num_elements) - ! Initialize hash table, is total number of elements + subroutine ccp_is_wet(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 == wet_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_wet !######################################################################## - 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_min_val(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 + 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 + 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 + + !######################################################################## + + 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 + 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 + + !######################################################################## + + 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 + 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 + + !######################################################################## + + 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 + call append_errvars(1, & + "WARNING: Model constituents are locked", & + subname, errcode=errcode, errmsg=errmsg, caller=warn_func) + end if + else + 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 + + !######################################################################## + + 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 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 + 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 (errcode /= 0) then + call append_errvars(1, & + "ERROR: Unknown vertical dimension, '" // & + trim(error) // "'", subname, & + errcode=errcode, errmsg=errmsg) + end if + end if + end if + end if + else + call append_errvars(1, "WARNING: Model constituents are locked", & + subname, errcode=errcode, errmsg=errmsg) + 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 append_errvars(1, trim(error), subname, & + errcode=errcode, errmsg=errmsg) 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 append_errvars(1, "ERROR: Bad hash table value " // & + trim(standard_name), subname, errcode=errcode, errmsg=errmsg) end select end if @@ -724,118 +1085,123 @@ 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 + integer :: errcode_local + 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 + errcode_local = 0 + if (this%const_props_locked(errcode=errcode, errmsg=errmsg, warn_func=subname)) then + call append_errvars(1, & + "WARNING: Model constituents properties already locked, ignoring", & + subname, errcode=errcode, errmsg=errmsg) + errcode_local = 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 + 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 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" + hval => hiter%value() + select type(hval) + type is (ccpp_constituent_properties_t) + cprop => hval + call cprop%is_advected(check) + if (check) then + this%num_advected_vars = this%num_advected_vars + 1 end if - exit - 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 + call append_errvars(1, "ERROR: num_advected_vars index " // & + to_str(this%num_advected_vars) // & + " out of bounds " // to_str(num_vars), & + subname, 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 (errcode_local == 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%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, & + call cprop%is_advected(check) + if (check) then + index_advect = index_advect + 1 + if (index_advect > this%num_advected_vars) then + 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 + call cprop%set_const_index(index_advect, & errcode=errcode, errmsg=errmsg) + call this%const_metadata(index_advect)%set(cprop) 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 + index_const = index_const + 1 + if (index_const > num_vars) then + 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 + call cprop%set_const_index(index_const, & + errcode=errcode, errmsg=errmsg) + call this%const_metadata(index_const)%set(cprop) 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' + ! Make sure this is a layer variable + if (.not. cprop%is_layer_var()) then + call cprop%vertical_dimension(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, "ERROR: Bad hash table value", & + subname, errcode=errcode, errmsg=errmsg) + errcode_local = errcode_local + 1 exit end select call hiter%next() @@ -845,420 +1211,1003 @@ 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 - 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', & + 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 (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', & + if (index_advect /= this%num_advected_vars) then + 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) - end if - if (astat == 0) then - this%vars_2d = kphys_unassigned + 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 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, 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 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, & + "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', & + 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', & + subname, errcode=errcode, errmsg=errmsg) + errcode_local = astat + end if + 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, & + 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 + errcode_local = 1 + end if + end if + if (errcode_local == 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, & + 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. + ! 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 + logical, optional, intent(in) :: thermo_active + ! 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 + + 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, 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. + ! 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 + logical, optional, intent(in) :: thermo_active + 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, thermo_active=thermo_active)) 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, & + 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. + ! 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 + logical, optional, intent(in) :: thermo_active + 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, & + thermo_active=thermo_active)) then + ! See if we have room for another constituent + cindex = cindex + 1 + if (cindex > max_cind) then + call append_errvars(1, & + ": Too many constituents for ", & + 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, ": ERROR: "// & + "bad field index, "//to_str(fld_ind)// & + " for '"//trim(std_name)//"', should have been "// & + 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, ": ERROR: "// & + "Wrong number of vertical levels for '"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected "//to_str(this%num_layers), & + subname, errcode=errcode, errmsg=errmsg) + exit + end if + else + call this%const_metadata(index)%standard_name(std_name) + 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), & + subname, errcode=errcode, errmsg=errmsg) + 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, & + 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. + ! 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 + logical, optional, intent(in) :: thermo_active + 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, & + thermo_active=thermo_active)) then + ! See if we have room for another constituent + cindex = cindex + 1 + if (cindex > max_cind) then + call append_errvars(1, & + ": Too many constituents for ", & + 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, ": ERROR: "// & + "bad field index, "//to_str(fld_ind)// & + " for '"//trim(std_name)//"', should have been"// & + 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, & + ": Wrong number of vertical levels for '"// & + trim(std_name)//"', "//to_str(num_levels)// & + ", expected"//to_str(this%num_layers), & + subname, errcode=errcode, errmsg=errmsg) + exit + end if + else + call this%const_metadata(index)%standard_name(std_name) + 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), & + subname, errcode=errcode, errmsg=errmsg) + 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 + call this%prop%vertical_dimension(vert_dim, errcode, errmsg) + end if + else + vert_dim = '' + call append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) + end if + + 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 append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_thermo_active + + !####################################################################### + + 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_is_mass_mixing_ratio !######################################################################## - subroutine ccp_model_const_reset(this) - ! Empty (reset) the entire object + subroutine ccpt_is_volume_mixing_ratio(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_volume_mixing_ratio' - if (allocated(this%vars_layer)) then - deallocate(this%vars_layer) + if (associated(this%prop)) then + call this%prop%is_volume_mixing_ratio(val_out, errcode, errmsg) + else + val_out = .false. + call append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if - if (allocated(this%vars_interface)) then - deallocate(this%vars_interface) + + end subroutine ccpt_is_volume_mixing_ratio + + !######################################################################## + + subroutine ccpt_is_number_concentration(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_number_concentration' + + if (associated(this%prop)) then + call this%prop%is_number_concentration(val_out, errcode, errmsg) + else + val_out = .false. + call append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 append_errvars(1, ": invalid constituent pointer", & + subname, 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 + character(len=*), parameter :: subname = 'ccpt_set' + + 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 append_errvars(1, trim(errmsg2), subname, errcode=errcode, & + errmsg=errmsg) 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 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, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) end if - end subroutine ccp_model_const_metadata + 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 append_errvars(1, ": invalid constituent pointer", & + subname, errcode=errcode, errmsg=errmsg) + end if + + end subroutine ccpt_set_thermo_active 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..99cf3145 --- /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 = number_of_ccpp_constituents + long_name = Number of constituents managed by CCPP Framework + units = count + dimensions = () + type = integer +[ num_advected_vars ] + 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_constituents + long_name = Array of constituents managed by CCPP Framework + units = none + state_variable = true + dimensions = (horizontal_dimension, vertical_layer_dimension, number_of_ccpp_constituents) + type = real | kind = kind_phys +[ const_metadata ] + standard_name = ccpp_constituent_properties + units = None + type = ccpp_constituent_prop_ptr_t + dimensions = (number_of_ccpp_constituents) +[ vars_minvalue ] + standard_name = ccpp_constituent_minimum_values + units = kg kg-1 + type = real | kind = kind_phys + dimensions = (number_of_ccpp_constituents) + protected = True 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/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..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/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_host.F90 b/test/advection_test/test_host.F90 index 61bd8657..49a3c602 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,33 @@ 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, 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 + + 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 + logical function check_list(test_list, chk_list, list_desc, suite_name) ! Check a list () against its expected value () @@ -120,7 +140,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 +204,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 +221,23 @@ 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_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 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 +245,26 @@ 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 + 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) @@ -300,37 +293,296 @@ subroutine test_host(retval, test_suites) return 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) + errflg = 0 + errmsg = '' - ! 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 + ! 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, 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, errflg_final) + end if + if (num_advected /= 3) then + write(6, '(a,i0)') "ERROR: num advected constituents = ", num_advected + retval = .false. + return + end if + ! Initialize constituent data + call test_host_ccpp_initialize_constituents(ncols, pver, 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 + + ! 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 + !++++++++++++++++++++++++++++++++++ + + 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_final = -1 !Notify test script that a failure occured + end if + else + !Reset error flag to continue testing other properties: + errflg = 0 + 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_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 + 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 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_thermo_active(check, errflg, errmsg) + if (errflg /= 0) then + 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.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 + !++++++++++++++++++++++++++++++++++ + + !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( & + 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 +600,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 +624,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 +643,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 @@ -407,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 @@ -418,8 +676,16 @@ 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(7) + character(len=cm), target :: test_outvars1(6) + character(len=cm), target :: test_reqvars1(9) + + 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 ', & @@ -427,14 +693,14 @@ program test '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 ', & @@ -445,12 +711,10 @@ program test '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..c2d99798 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), dimension(:,:,:), 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 diff --git a/test/advection_test/test_reports.py b/test/advection_test/test_reports.py index c28fe38a..6d84616b 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) @@ -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/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/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/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 3749e8ac..da02aeea 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) @@ -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_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 64% rename from test/run_tests.sh rename to test/run_fortran_tests.sh index 90a3ebaa..ccfc85f9 100755 --- a/test/run_tests.sh +++ b/test/run_fortran_tests.sh @@ -37,26 +37,22 @@ if [ $res -ne 0 ]; then echo "Failure running advection test" fi -# 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 +# Run var_compatibility test + ./var_compatibility_test/run_test + res=$? + errcnt=$((errcnt + res)) + if [ $res -ne 0 ]; then + echo "Failure running var_compatibility test" + fi 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 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/sample_host_files/ddt1.F90 b/test/unit_tests/sample_host_files/ddt1.F90 new file mode 100644 index 00000000..71b22b4f --- /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, public :: 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..d1806932 --- /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, public :: 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..22d5af0e --- /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, public :: 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..00b4c170 --- /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, public :: 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..5efe0845 --- /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, public :: 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_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_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() + diff --git a/test/unit_tests/test_metadata_scheme_file.py b/test/unit_tests/test_metadata_scheme_file.py index 52f62aa1..595962c0 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,260 @@ 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) -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..3a01f98b 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 @@ -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 old mode 100644 new mode 100755 index ea044547..94a6c4a0 --- 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""" @@ -270,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) @@ -286,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) @@ -308,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) @@ -330,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}" @@ -352,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})" @@ -377,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}" @@ -401,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') @@ -411,14 +400,13 @@ 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})" self.assertEqual(rev_stmt, expected) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() + diff --git a/test/var_compatibility_test/.gitignore b/test/var_compatibility_test/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/test/var_compatibility_test/.gitignore @@ -0,0 +1 @@ +build diff --git a/test/var_compatibility_test/CMakeLists.txt b/test/var_compatibility_test/CMakeLists.txt new file mode 100644 index 00000000..8cbd7e44 --- /dev/null +++ b/test/var_compatibility_test/CMakeLists.txt @@ -0,0 +1,188 @@ +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_compatibility_files.txt") +LIST(APPEND HOST_FILES "test_host_data" "test_host_mod") +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}") + +#------------------------------------------------------------------------------ +# +# 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 () +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} + 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_compatibility_test/README.md b/test/var_compatibility_test/README.md new file mode 100644 index 00000000..9b56ec9c --- /dev/null +++ b/test/var_compatibility_test/README.md @@ -0,0 +1,6 @@ +var_compatibility 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_compatibility_test/effr_calc.F90 b/test/var_compatibility_test/effr_calc.F90 new file mode 100644 index 00000000..6dbbb722 --- /dev/null +++ b/test/var_compatibility_test/effr_calc.F90 @@ -0,0 +1,60 @@ +!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, 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),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),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 + 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) + real(kind_phys) :: ncg_in_local(ncol,nlev) + real(kind_phys) :: nci_out_local(ncol,nlev) + + errmsg = '' + errflg = 0 + + effrr_local = effrr_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) + if (present(effri_out)) effri_out = re_qi_avg + effrs_inout = effrs_inout + 10.0 ! in micrometer + scalar_var = 2.0 ! in km + + end subroutine effr_calc_run + +end module effr_calc diff --git a/test/var_compatibility_test/effr_calc.meta b/test/var_compatibility_test/effr_calc.meta new file mode 100644 index 00000000..73c36ace --- /dev/null +++ b/test/var_compatibility_test/effr_calc.meta @@ -0,0 +1,120 @@ +[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 + 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 + 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 + 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 + optional = True +[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 = 8 + intent = inout + 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 + units = flag + 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 + 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_compatibility_test/run_test b/test/var_compatibility_test/run_test new file mode 100755 index 00000000..a5edac37 --- /dev/null +++ b/test/var_compatibility_test/run_test @@ -0,0 +1,261 @@ +#! /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_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_compatibility_suite_cap.F90" +#process_list="" +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="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 +## $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_compatibility suite from command line" +check_datatable ${report_prog} ${datafile} "--required-variables" \ + ${required_vars_var_compatibility} "var_compatibility_suite" +check_datatable ${report_prog} ${datafile} "--input-variables" \ + ${input_vars_var_compatibility} "var_compatibility_suite" +check_datatable ${report_prog} ${datafile} "--output-variables" \ + ${output_vars_var_compatibility} "var_compatibility_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_compatibility_test/test_host.F90 b/test/var_compatibility_test/test_host.F90 new file mode 100644 index 00000000..3a805a8e --- /dev/null +++ b/test/var_compatibility_test/test_host.F90 @@ -0,0 +1,405 @@ +module test_prog + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public test_host + + ! Public data and interfaces + integer, public, parameter :: cs = 32 + 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 + + ! 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 + + 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(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_ice '/) + + 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(12) = (/ & + '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_graupel ', & + 'cloud_graupel_number_concentration ', & + 'cloud_ice_number_concentration ', & + 'scalar_variable_for_testing ', & + 'flag_indicating_cloud_microphysics_has_graupel ', & + 'flag_indicating_cloud_microphysics_has_ice '/) + + type(suite_info) :: test_suites(1) + logical :: run_okay + + ! Setup expected test suite info + 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 + 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_compatibility_test/test_host.meta b/test/var_compatibility_test/test_host.meta new file mode 100644 index 00000000..5d861764 --- /dev/null +++ b/test/var_compatibility_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_compatibility_test/test_host_data.F90 b/test/var_compatibility_test/test_host_data.F90 new file mode 100644 index 00000000..9d0ca306 --- /dev/null +++ b/test/var_compatibility_test/test_host_data.F90 @@ -0,0 +1,67 @@ +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 + 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 + + public allocate_physics_state + +contains + + 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) + end if + allocate(state%effrr(cols, levels)) + + if (allocated(state%effrl)) then + deallocate(state%effrl) + end if + allocate(state%effrl(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 + +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 new file mode 100644 index 00000000..d3bca89b --- /dev/null +++ b/test/var_compatibility_test/test_host_data.meta @@ -0,0 +1,61 @@ +[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 + 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 + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + 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 + 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 new file mode 100644 index 00000000..ca1d2014 --- /dev/null +++ b/test/var_compatibility_test/test_host_mod.F90 @@ -0,0 +1,86 @@ +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) + logical, parameter :: has_ice = .true. + logical, parameter :: has_graupel = .true. + + public :: init_data + public :: compare_data + +contains + + subroutine init_data() + + ! Allocate and initialize state + 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%scalar_var = 1.0 ! in m + effrs = 5.0E-4 ! 500 microns, in meter + 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 + + 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 :: 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. + + 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 + + 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_host_mod.meta b/test/var_compatibility_test/test_host_mod.meta new file mode 100644 index 00000000..51a2f5c3 --- /dev/null +++ b/test/var_compatibility_test/test_host_mod.meta @@ -0,0 +1,42 @@ +[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 +[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 + dimensions = () + type = logical diff --git a/test/var_compatibility_test/test_reports.py b/test/var_compatibility_test/test_reports.py new file mode 100755 index 00000000..aaf08953 --- /dev/null +++ b/test/var_compatibility_test/test_reports.py @@ -0,0 +1,161 @@ +#! /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] < 8)): + raise Exception("Python 3.8 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_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_compatibility_suite_cap.F90")] +_MODULE_LIST = ["effr_calc"] +_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", + "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_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 + +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("module_list"), + _MODULE_LIST) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("suite_list"), + _SUITE_LIST) +print("\nChecking variables for var_compatibility suite from python") +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("required_variables", + value="var_compatibility_suite"), + _REQUIRED_VARS_VAR_ACTION) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("input_variables", + value="var_compatibility_suite"), + _INPUT_VARS_VAR_ACTION) +NUM_ERRORS += check_datatable(_DATABASE, DatatableReport("output_variables", + value="var_compatibility_suite"), + _OUTPUT_VARS_VAR_ACTION) + +sys.exit(NUM_ERRORS) diff --git a/test/var_compatibility_test/var_compatibility_files.txt b/test/var_compatibility_test/var_compatibility_files.txt new file mode 100644 index 00000000..5f7db5b2 --- /dev/null +++ b/test/var_compatibility_test/var_compatibility_files.txt @@ -0,0 +1 @@ +effr_calc.meta diff --git a/test/var_compatibility_test/var_compatibility_suite.xml b/test/var_compatibility_test/var_compatibility_suite.xml new file mode 100644 index 00000000..4b70fe48 --- /dev/null +++ b/test/var_compatibility_test/var_compatibility_suite.xml @@ -0,0 +1,7 @@ + + + + + effr_calc + + diff --git a/test_prebuild/test_blocked_data/README.md b/test_prebuild/test_blocked_data/README.md index ee7962e1..8802e812 100644 --- a/test_prebuild/test_blocked_data/README.md +++ b/test_prebuild/test_blocked_data/README.md @@ -11,6 +11,4 @@ cd build cmake .. 2>&1 | tee log.cmake make 2>&1 | tee log.make ./test_blocked_data.x -# On systems where linking against the MPI library requires a parallel launcher, -# use 'mpirun -np 1 ./test_blocked_data.x' or 'srun -n 1 ./test_blocked_data.x' etc. ``` diff --git a/test_prebuild/test_metadata_parser.py b/test_prebuild/test_metadata_parser.py index 0f7d18d6..febc3d93 100644 --- a/test_prebuild/test_metadata_parser.py +++ b/test_prebuild/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() == []