diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 3775a985..bf1e7c36 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index dfa2e211..cbd46599 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -27,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__)) @@ -44,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): ############################################################################### @@ -579,12 +586,22 @@ def capgen(run_env, return_db=False): # 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) @@ -595,14 +612,21 @@ def capgen(run_env, return_db=False): # 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])) @@ -629,7 +653,8 @@ def capgen(run_env, return_db=False): cap_filenames = ccpp_api.write(outtemp_dir, run_env) if run_env.generate_host_cap: # Create a cap file - 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() @@ -647,7 +672,6 @@ def capgen(run_env, return_db=False): # end if # Finally, create the database of generated files and caps # This can be directly in output_dir because it will not affect dependencies - src_dir = os.path.join(__FRAMEWORK_ROOT, "src") generate_ccpp_datatable(run_env, host_model, ccpp_api, scheme_headers, scheme_tdict, host_files, cap_filenames, kinds_file, src_dir) diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 562a8f4b..7a4c3d8d 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -653,7 +653,8 @@ def _new_var_entry(parent, var, full_entry=True): prop_list.extend(["allocatable", "active", "default_value", "diagnostic_name", "diagnostic_name_fixed", "kind", "persistence", "polymorphic", "protected", - "state_variable", "type", "units"]) + "state_variable", "type", "units", "molar_mass", + "advected"]) prop_list.extend(Var.constituent_property_names()) # end if ventry = ET.SubElement(parent, "var") diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index c4d7fbab..c5c6f4ab 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -427,7 +427,7 @@ def analyze(self, host_model, scheme_library, ddt_library, run_env): phase = RUN_PHASE_NAME # end if lmsg = "Group {}, schemes = {}" - if run_env.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()])) @@ -490,7 +490,7 @@ def write(self, output_dir, run_env): (calling the group caps one after another)""" # Set name of module and filename of cap filename = '{module_name}.F90'.format(module_name=self.module) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: run_env.logger.debug('Writing CCPP suite file, {}'.format(filename)) # end if # Retrieve the name of the constituent module for Group use statements @@ -808,32 +808,46 @@ def write_req_vars_sub(self, ofile, errmsg_name, errcode_name): 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/constituents.py b/scripts/constituents.py index 659f997f..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 @@ -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/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_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 6456134e..c0bb4139 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 = ''' ''' @@ -275,6 +276,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 diff --git a/scripts/host_cap.py b/scripts/host_cap.py index f7447de2..d2c4ed7e 100644 --- a/scripts/host_cap.py +++ b/scripts/host_cap.py @@ -11,6 +11,7 @@ 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 @@ -77,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): @@ -91,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): @@ -99,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): @@ -107,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): @@ -127,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) ############################################################################### @@ -159,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 @@ -233,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 '{}'" @@ -242,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) @@ -375,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 {}' @@ -402,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}" @@ -418,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: @@ -561,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 49f4ffb1..946e9782 100755 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -792,7 +792,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): self.__pobj.add_syntax_err(mismatch) self.__section_valid = False # end if - if run_env.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 @@ -812,7 +812,7 @@ def __init_from_file(self, table_name, table_type, known_ddts, run_env): newvar, curr_line = self.parse_variable(curr_line, known_ddts) valid_lines = newvar is not None if valid_lines: - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: dmsg = 'Adding {} to {}' lname = newvar.get_prop_value('local_name') run_env.logger.debug(dmsg.format(lname, self.title)) diff --git a/scripts/metavar.py b/scripts/metavar.py index 8c600546..c7be0f5c 100755 --- a/scripts/metavar.py +++ b/scripts/metavar.py @@ -20,6 +20,7 @@ from parse_tools import check_units, check_dimensions, check_cf_standard_name from parse_tools import check_diagnostic_id, check_diagnostic_fixed from parse_tools import check_default_value, check_valid_values +from parse_tools import check_molar_mass from parse_tools import ParseContext, ParseSource, type_name from parse_tools import ParseInternalError, ParseSyntaxError, CCPPError from var_props import CCPP_LOOP_DIM_SUBSTS, VariableProperty, VarCompatObj @@ -202,6 +203,8 @@ class Var: VariableProperty('active', str, optional_in=True, default_in='.true.'), VariableProperty('polymorphic', bool, optional_in=True, + default_in=False), + VariableProperty('target', bool, optional_in=True, default_in=False)] # XXgoldyXX: v debug only @@ -218,7 +221,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} @@ -616,7 +622,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 " @@ -693,9 +700,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 = '' @@ -755,7 +768,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. @@ -770,10 +783,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: @@ -850,6 +879,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""" @@ -938,7 +972,7 @@ def has_vertical_dimension(self, dims=None): return find_vertical_dimension(vdims)[0] def write_def(self, outfile, indent, wdict, allocatable=False, - dummy=False, add_intent=None, extra_space=0): + dummy=False, add_intent=None, extra_space=0, public=False): """Write the definition line for the variable to . If is True, include the variable's intent. If is True but the variable has no intent, add the @@ -1000,10 +1034,9 @@ def write_def(self, outfile, indent, wdict, allocatable=False, elif intent is not None: alloval = self.get_prop_value('allocatable') if (intent.lower()[-3:] == 'out') and alloval: - intent_str = 'allocatable, intent({})'.format(intent) + intent_str = f"allocatable, intent({intent})" else: - intent_str = 'intent({}){}'.format(intent, - ' '*(5 - len(intent))) + intent_str = f"intent({intent}){' '*(5 - len(intent))}" # end if elif not dummy: intent_str = '' @@ -1015,6 +1048,13 @@ def write_def(self, outfile, indent, wdict, allocatable=False, else: comma = ' ' # end if + if self.get_prop_value('target'): + targ = ", target" + else: + targ = "" + # end if + comma = targ + comma + extra_space -= len(targ) if self.is_ddt(): if polymorphic: dstr = "class({kind}){cspc}{intent} :: {name}{dims} ! {sname}" @@ -1032,6 +1072,7 @@ def write_def(self, outfile, indent, wdict, allocatable=False, cspc = comma + ' '*(extra_space + 19 - len(vtype)) # end if # end if + outfile.write(dstr.format(type=vtype, kind=kind, intent=intent_str, name=name, dims=dimstr, cspc=cspc, sname=stdname), indent) diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index 4f888fb1..4990da88 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -22,10 +22,10 @@ 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 @@ -47,6 +47,7 @@ 'check_fortran_type', 'check_local_name', 'check_valid_values', + 'check_molar_mass', 'context_string', 'find_schema_file', 'find_schema_version', diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index ba8722d9..f32df45d 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -19,6 +19,7 @@ _UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" _UNITS_REGEX = f"^({_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" _UNITS_RE = re.compile(_UNITS_REGEX) +_MAX_MOLAR_MASS = 10000.0 def check_units(test_val, prop_dict, error): """Return if a valid unit, otherwise, None @@ -942,6 +943,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/xml_tools.py b/scripts/parse_tools/xml_tools.py index 120ab645..0ff56b3a 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -7,35 +7,35 @@ # Python library imports from __future__ import print_function import os -import os.path import re +import shutil import subprocess import sys import xml.etree.ElementTree as ET sys.path.insert(0, os.path.dirname(__file__)) -# pylint: disable=wrong-import-position -try: - from distutils.spawn import find_executable - _XMLLINT = find_executable('xmllint') -except ImportError: - _XMLLINT = None -# End try # CCPP framework imports from parse_source import CCPPError from parse_log import init_log, set_log_to_null -# pylint: enable=wrong-import-position # Global data _INDENT_STR = " " +_XMLLINT = shutil.which('xmllint') # Blank if not installed beg_tag_re = re.compile(r"([<][^/][^<>]*[^/][>])") end_tag_re = re.compile(r"([<][/][^<>/]+[>])") simple_tag_re = re.compile(r"([<][^/][^<>/]+[/][>])") # Find python version -PY3 = sys.version_info[0] > 2 PYSUBVER = sys.version_info[1] _LOGGER = None +############################################################################### +class XMLToolsInternalError(ValueError): +############################################################################### + """Error class for reporting internal errors""" + def __init__(self, message): + """Initialize this exception""" + super().__init__(message) + ############################################################################### def call_command(commands, logger, silent=False): ############################################################################### @@ -55,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 @@ -92,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 ############################################################################### @@ -104,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' @@ -120,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 ############################################################################### @@ -163,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 ############################################################################### @@ -180,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 @@ -220,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 ############################################################################### @@ -250,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 """ @@ -270,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: diff --git a/scripts/suite_objects.py b/scripts/suite_objects.py index cc34a750..c52c7c4d 100644 --- a/scripts/suite_objects.py +++ b/scripts/suite_objects.py @@ -90,6 +90,22 @@ def add_vars(self, call_list, run_env, gen_unique=False): # end if # end for + def add_variable(self, newvar, run_env, exists_ok=False, gen_unique=False, + adjust_intent=False): + """Add as for VarDictionary but make sure that the variable + has an intent with the default being intent(in). + """ + # We really need an intent on a dummy argument + if newvar.get_prop_value("intent") is None: + subst_dict = {'intent' : 'in'} + oldvar = newvar + newvar = oldvar.clone(subst_dict, source_name=self.name, + source_type=_API_GROUP_VAR_NAME, + context=oldvar.context) + # end if + super().add_variable(newvar, run_env, exists_ok=exists_ok, + gen_unique=gen_unique, adjust_intent=adjust_intent) + def call_string(self, cldicts=None, is_func_call=False, subname=None): """Return a dummy argument string for this call list. may be a list of VarDictionary objects to search for @@ -1271,7 +1287,7 @@ def __init__(self, index_name, context, parent, run_env, items=None): # self._local_dim_name is the variable name for self._dim_name self._local_dim_name = None super().__init__(index_name, context, parent, run_env) - if run_env.logger and run_env.logger.isEnabledFor(logging.DEBUG): + if run_env.verbose: lmsg = "Adding VerticalLoop for '{}'" run_env.logger.debug(lmsg.format(index_name)) # end if diff --git a/scripts/var_props.py b/scripts/var_props.py index 15613567..78656df8 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -14,6 +14,7 @@ from conversion_tools import unit_conversion from framework_env import CCPPFrameworkEnv from parse_tools import check_local_name, check_fortran_type, context_string +from parse_tools import check_molar_mass from parse_tools import FORTRAN_DP_RE, FORTRAN_SCALAR_REF_RE, fortran_list_match from parse_tools import check_units, check_dimensions, check_cf_standard_name from parse_tools import check_diagnostic_id, check_diagnostic_fixed @@ -586,6 +587,8 @@ class VariableProperty: 'foo(bar)' >>> VariableProperty('local_name', str, check_fn_in=check_local_name).valid_value('q(:,:,index_of_water_vapor_specific_humidity)') 'q(:,:,index_of_water_vapor_specific_humidity)' + >>> VariableProperty('molar_mass', float, check_fn_in=check_molar_mass).valid_value('12.1') + 12.1 """ __true_vals = ['t', 'true', '.true.'] @@ -597,7 +600,7 @@ def __init__(self, name_in, type_in, valid_values_in=None, """Conduct sanity checks and initialize this variable property.""" self._name = name_in self._type = type_in - if self._type not in [bool, int, list, str]: + if self._type not in [bool, int, list, str, float]: emsg = "{} has invalid VariableProperty type, '{}'" raise CCPPError(emsg.format(name_in, type_in)) # end if @@ -688,6 +691,21 @@ def valid_value(self, test_value, prop_dict=None, error=False): valid_val = tval except CCPPError: valid_val = None # Redundant but more expressive than pass + elif self.ptype is float: + try: + tval = float(test_value) + if self._valid_values is not None: + if tval in self._valid_values: + valid_val = tval + else: + valid_val = None # i.e. pass + # end if + else: + valid_val = tval + # end if + except CCPPError: + valid_val = None + # end try elif self.ptype is list: if isinstance(test_value, str): tval = fortran_list_match(test_value) diff --git a/src/ccpp_constituent_prop_mod.F90 b/src/ccpp_constituent_prop_mod.F90 index 419af297..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/cld_ice.meta b/test/advection_test/cld_ice.meta index f66888e0..010fb419 100644 --- a/test/advection_test/cld_ice.meta +++ b/test/advection_test/cld_ice.meta @@ -44,6 +44,7 @@ [ 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 @@ -76,6 +77,7 @@ [ 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/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..17e9a2ad 100644 --- a/test/advection_test/test_reports.py +++ b/test/advection_test/test_reports.py @@ -22,8 +22,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR) diff --git a/test/capgen_test/test_reports.py b/test/capgen_test/test_reports.py index 3749e8ac..e8237476 100644 --- a/test/capgen_test/test_reports.py +++ b/test/capgen_test/test_reports.py @@ -23,8 +23,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR) diff --git a/test/unit_tests/sample_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 28b2ee50..595962c0 100644 --- a/test/unit_tests/test_metadata_scheme_file.py +++ b/test/unit_tests/test_metadata_scheme_file.py @@ -59,6 +59,7 @@ # pylint: disable=wrong-import-position from ccpp_capgen import parse_scheme_files from framework_env import CCPPFrameworkEnv +from parse_tools import CCPPError # pylint: enable=wrong-import-position class MetadataHeaderTestCase(unittest.TestCase): @@ -67,6 +68,7 @@ class MetadataHeaderTestCase(unittest.TestCase): def setUp(self): """Setup important directories and logging""" self._sample_files_dir = os.path.join(_TEST_DIR, "sample_scheme_files") + self._host_files_dir = os.path.join(_TEST_DIR, "sample_host_files") logger = logging.getLogger(self.__class__.__name__) self._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', 'scheme_files':'', @@ -85,203 +87,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__": unittest.main() + diff --git a/test/var_action_test/test_reports.py b/test/var_action_test/test_reports.py index 3d9a8637..fff8603d 100755 --- a/test/var_action_test/test_reports.py +++ b/test/var_action_test/test_reports.py @@ -23,8 +23,8 @@ # end if if ((sys.version_info[0] < 3) or - (sys.version_info[0] == 3) and (sys.version_info[1] < 7)): - raise Exception("Python 3.7 or greater required") + (sys.version_info[0] == 3) and (sys.version_info[1] < 8)): + raise Exception("Python 3.8 or greater required") # end if sys.path.append(_SCRIPTS_DIR)