diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml index c2d52ee8..441250c9 100644 --- a/.github/workflows/capgen_unit_tests.yaml +++ b/.github/workflows/capgen_unit_tests.yaml @@ -7,11 +7,15 @@ on: jobs: unit_tests: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-22.04] + fortran-compiler: [gfortran-9, gfortran-10, gfortran-11, gfortran-12] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: update repos and install dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential gfortran cmake python3 git + run: sudo apt-get update && sudo apt-get install -y build-essential ${{matrix.fortran-compiler}} cmake python3 git - name: Run unit tests run: cd test && ./run_fortran_tests.sh diff --git a/scripts/ccpp_prebuild.py b/scripts/ccpp_prebuild.py index a1623867..b67de540 100755 --- a/scripts/ccpp_prebuild.py +++ b/scripts/ccpp_prebuild.py @@ -471,6 +471,21 @@ def compare_metadata(metadata_define, metadata_request): var.convert_from(metadata_define[var_name][0].units) elif var.intent=='out': var.convert_to(metadata_define[var_name][0].units) + # If the host model variable is allocated based on a condition, i.e. has an active attribute other + # than T (.true.), the scheme variable must be optional + if not metadata_define[var_name][0].active == 'T': + for var in metadata_request[var_name]: + if var.optional == 'F': + logging.error("Conditionally allocated host-model variable {0} is not optional in {1}".format( + var_name, var.container)) + success = False + # TEMPORARY CHECK - IF THE VARIABLE IS ALWAYS ALLOCATED, THE SCHEME VARIABLE SHOULDN'T BE OPTIONAL + else: + for var in metadata_request[var_name]: + if var.optional == 'T': + logging.warn("Unconditionally allocated host-model variable {0} is optional in {1}".format( + var_name, var.container)) + # Construct the actual target variable and list of modules to use from the information in 'container' var = metadata_define[var_name][0] target = '' @@ -483,6 +498,7 @@ def compare_metadata(metadata_define, metadata_request): pass else: logging.error('Unknown identifier {0} in container value of defined variable {1}'.format(subitems[0], var_name)) + success = False target += var.local_name # Copy the length kind from the variable definition to update len=* in the variable requests if var.type == 'character': diff --git a/scripts/common.py b/scripts/common.py index c33f78fe..c29ccda2 100755 --- a/scripts/common.py +++ b/scripts/common.py @@ -29,6 +29,7 @@ CCPP_BLOCK_COUNT = 'ccpp_block_count' CCPP_BLOCK_SIZES = 'ccpp_block_sizes' CCPP_THREAD_NUMBER = 'ccpp_thread_number' +CCPP_THREAD_COUNT = 'ccpp_thread_count' CCPP_CHUNK_EXTENT = 'ccpp_chunk_extent' CCPP_HORIZONTAL_LOOP_BEGIN = 'horizontal_loop_begin' @@ -60,6 +61,7 @@ CCPP_LOOP_COUNTER : 'cdata%loop_cnt', CCPP_BLOCK_NUMBER : 'cdata%blk_no', CCPP_THREAD_NUMBER : 'cdata%thrd_no', + CCPP_THREAD_COUNT : 'cdata%thrd_cnt', } STANDARD_CHARACTER_TYPE = 'character' diff --git a/scripts/metadata_parser.py b/scripts/metadata_parser.py index 1757f78b..d1277ebe 100755 --- a/scripts/metadata_parser.py +++ b/scripts/metadata_parser.py @@ -198,16 +198,28 @@ def read_new_metadata(filename, module_name, table_name, scheme_name = None, sub # *DH 2020-05-26 if not new_var.get_prop_value('active'): - # If it doesn't have an active attribute, then the variable is always active (default) - active = 'T' + raise Exception("Unexpected result: no active attribute received from capgen metadata parser for {} / {}".format(standard_name,table_name)) + elif scheme_name and not new_var.get_prop_value('active').lower() == '.true.': + raise Exception("Scheme variable {} in table {} has metadata attribute active={}, which is not allowed".format( + standard_name, table_name, new_var.get_prop_value('active').lower())) elif new_var.get_prop_value('active').lower() == '.true.': active = 'T' - elif new_var.get_prop_value('active') and new_var.get_prop_value('active').lower() == '.false.': + elif new_var.get_prop_value('active').lower() == '.false.': active = 'F' else: # Replace multiple whitespaces, preserve case active = ' '.join(new_var.get_prop_value('active').split()) + if not new_var.get_prop_value('optional') in [False, True]: + raise Exception("Unexpected result: no optional attribute received from metadata parser for {} / {}".format(standard_name,table_name)) + elif not scheme_name and new_var.get_prop_value('optional'): + raise Exception("Host variable {} in table {} has metadata attribute optional={}, which is not allowed".format( + standard_name,table_name, new_var.get_prop_value('optional').lower())) + elif new_var.get_prop_value('optional'): + optional = 'T' + else: + optional = 'F' + # DH* 20210812 # Workaround for Fortran DDTs incorrectly having the type of # the DDT copied into the kind attribute in parse_metadata_file @@ -228,6 +240,7 @@ def read_new_metadata(filename, module_name, table_name, scheme_name = None, sub kind = kind, intent = new_var.get_prop_value('intent'), active = active, + optional = optional, ) # Check for duplicates in same table if standard_name in metadata.keys(): diff --git a/scripts/mkcap.py b/scripts/mkcap.py index 48b6b6e7..8a8ca7d9 100755 --- a/scripts/mkcap.py +++ b/scripts/mkcap.py @@ -32,6 +32,8 @@ def __init__(self, **kwargs): self._kind = None self._intent = None self._active = None + self._optional = None + self._pointer = False self._target = None self._actions = { 'in' : None, 'out' : None } for key, value in kwargs.items(): @@ -134,6 +136,30 @@ def active(self, value): raise ValueError('Invalid value {0} for variable property active, must be a string'.format(value)) self._active = value + @property + def optional(self): + '''Get the optional attribute of the variable.''' + return self._optional + + @optional.setter + def optional(self, value): + if not isinstance(value, str): + raise ValueError('Invalid value {0} for variable property optional, must be a string'.format(value)) + self._optional = value + + # Pointer is not set by parsing metadata attributes, but by mkstatic. + # This is a quick and dirty solution! + @property + def pointer(self): + '''Get the pointer attribute of the variable.''' + return self._pointer + + @pointer.setter + def pointer(self, value): + if not isinstance(value, bool): + raise ValueError('Invalid value {0} for variable property pointer, must be a logical'.format(value)) + self._pointer = value + @property def target(self): '''Get the target of the variable.''' @@ -270,62 +296,128 @@ def print_def_intent(self, metadata): dictionary to resolve lower bounds for array dimensions.''' # Resolve dimensisons to local names using undefined upper bounds (assumed shape) dimstring = self.dimstring_local_names(metadata, assume_shape = True) + # It is an error for host model variables to have the optional attribute in the metadata + if self.optional == 'T': + error_message = "This routine should only be called for host model variables" + \ + " that cannot be optional, but got self.optional=T" + raise Exception(error_message) + # If the host variable is potentially unallocated, add optional and target to variable declaration + elif not self.active == 'T': + optional = ', optional, target' + else: + optional = '' # if self.type in STANDARD_VARIABLE_TYPES: if self.kind: - str = "{s.type}({s._kind}), intent({s.intent}) :: {s.local_name}{dimstring}" + str = "{s.type}({s._kind}), intent({s.intent}){optional} :: {s.local_name}{dimstring}" else: - str = "{s.type}, intent({s.intent}) :: {s.local_name}{dimstring}" + str = "{s.type}, intent({s.intent}){optional} :: {s.local_name}{dimstring}" else: if self.kind: error_message = "Generating variable definition statements for derived types with" + \ " kind attributes not implemented; variable: {0}".format(self.standard_name) raise Exception(error_message) else: - str = "type({s.type}), intent({s.intent}) :: {s.local_name}{dimstring}" - return str.format(s=self, dimstring=dimstring) + str = "type({s.type}), intent({s.intent}){optional} :: {s.local_name}{dimstring}" + return str.format(s=self, optional=optional, dimstring=dimstring) def print_def_local(self, metadata): '''Print the definition line for the variable, assuming it is a local variable.''' - if self.type in STANDARD_VARIABLE_TYPES: - if self.kind: - if self.rank: - str = "{s.type}({s._kind}), dimension{s.rank}, allocatable :: {s.local_name}" + # It is an error for local variables to have the active attribute + if not self.active == 'T': + error_message = "This routine should only be called for local variables" + \ + " that cannot have an active attribute other than the" +\ + " default T, but got self.active=T" + raise Exception(error_message) + + # If it is a pointer, everything is different! + if self.pointer: + if self.type in STANDARD_VARIABLE_TYPES: + if self.kind: + if self.rank: + str = "{s.type}({s._kind}), dimension{s.rank}, pointer :: p => null()" + else: + str = "{s.type}({s._kind}), pointer :: p => null()" else: - str = "{s.type}({s._kind}) :: {s.local_name}" + if self.rank: + str = "{s.type}, dimension{s.rank}, pointer :: p => null()" + else: + str = "{s.type}, pointer :: p => null()" else: - if self.rank: - str = "{s.type}, dimension{s.rank}, allocatable :: {s.local_name}" + if self.kind: + error_message = "Generating variable definition statements for derived types with" + \ + " kind attributes not implemented; variable: {0}".format(self.standard_name) + raise Exception(error_message) else: - str = "{s.type} :: {s.local_name}" + if self.rank: + str = "type({s.type}), dimension{s.rank}, pointer :: p => null()" + else: + str = "type({s.type}), pointer :: p => null()" + return str.format(s=self) else: - if self.kind: - error_message = "Generating variable definition statements for derived types with" + \ - " kind attributes not implemented; variable: {0}".format(self.standard_name) - raise Exception(error_message) + # If the host variable is potentially unallocated, the active attribute is + # also set accordingly for the local variable; add target to variable declaration + if self.optional == 'T': + target = ', target' else: - if self.rank: - str = "type({s.type}), dimension{s.rank}, allocatable :: {s.local_name}" + target = '' + if self.type in STANDARD_VARIABLE_TYPES: + if self.kind: + if self.rank: + str = "{s.type}({s._kind}), dimension{s.rank}, allocatable{target} :: {s.local_name}" + else: + str = "{s.type}({s._kind}){target} :: {s.local_name}" else: - str = "type({s.type}) :: {s.local_name}" - return str.format(s=self) + if self.rank: + str = "{s.type}, dimension{s.rank}, allocatable{target} :: {s.local_name}" + else: + str = "{s.type}{target} :: {s.local_name}" + else: + if self.kind: + error_message = "Generating variable definition statements for derived types with" + \ + " kind attributes not implemented; variable: {0}".format(self.standard_name) + raise Exception(error_message) + else: + if self.rank: + str = "type({s.type}), dimension{s.rank}, allocatable{target} :: {s.local_name}" + else: + str = "type({s.type}){target} :: {s.local_name}" + return str.format(s=self, target=target) def print_debug(self): '''Print the data retrieval line for the variable.''' - str='''Contents of {s} (* = mandatory for compatibility): - standard_name = {s.standard_name} * - long_name = {s.long_name} - units = {s.units} * - local_name = {s.local_name} - type = {s.type} * - dimensions = {s.dimensions} - rank = {s.rank} * - kind = {s.kind} * - intent = {s.intent} - active = {s.active} - target = {s.target} - container = {s.container} - actions = {s.actions}''' + # Scheme variables don't have the active attribute + if 'SCHEME' in self.container: + str='''Contents of {s} (* = mandatory for compatibility): + standard_name = {s.standard_name} * + long_name = {s.long_name} + units = {s.units} * + local_name = {s.local_name} + type = {s.type} * + dimensions = {s.dimensions} + rank = {s.rank} * + kind = {s.kind} * + intent = {s.intent} + optional = {s.optional} + target = {s.target} + container = {s.container} + actions = {s.actions}''' + # Host model variables don't have the optional attribute + else: + str='''Contents of {s} (* = mandatory for compatibility): + standard_name = {s.standard_name} * + long_name = {s.long_name} + units = {s.units} * + local_name = {s.local_name} + type = {s.type} * + dimensions = {s.dimensions} + rank = {s.rank} * + kind = {s.kind} * + intent = {s.intent} + active = {s.active} + target = {s.target} + container = {s.container} + actions = {s.actions}''' return str.format(s=self) class CapsMakefile(object): diff --git a/scripts/mkstatic.py b/scripts/mkstatic.py index 57067d4b..4f00b9b9 100755 --- a/scripts/mkstatic.py +++ b/scripts/mkstatic.py @@ -15,7 +15,7 @@ from common import encode_container from common import CCPP_STAGES from common import CCPP_T_INSTANCE_VARIABLE, CCPP_ERROR_CODE_VARIABLE, CCPP_ERROR_MSG_VARIABLE, CCPP_LOOP_COUNTER, CCPP_LOOP_EXTENT -from common import CCPP_BLOCK_NUMBER, CCPP_BLOCK_COUNT, CCPP_BLOCK_SIZES, CCPP_THREAD_NUMBER, CCPP_INTERNAL_VARIABLES +from common import CCPP_BLOCK_NUMBER, CCPP_BLOCK_COUNT, CCPP_BLOCK_SIZES, CCPP_THREAD_NUMBER, CCPP_THREAD_COUNT, CCPP_INTERNAL_VARIABLES from common import CCPP_HORIZONTAL_LOOP_BEGIN, CCPP_HORIZONTAL_LOOP_END, CCPP_CHUNK_EXTENT from common import CCPP_CONSTANT_ONE, CCPP_HORIZONTAL_DIMENSION, CCPP_HORIZONTAL_LOOP_EXTENT, CCPP_NUM_INSTANCES from common import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX @@ -59,6 +59,12 @@ ), } +# Type and variable declarations for arrays of pointers, required for optional/inactive variables +TMPPTR_ARR_TYPE_DECLARATION = '''type :: {pointer_type_name} + {tmpptr_def} + end type {pointer_type_name}''' +TMPPTR_ARR_DECLARATION = '''type({pointer_type_name}), dimension({dims}) :: {localname}_array''' + ############################################################################### def extract_parents_and_indices_from_local_name(local_name): @@ -173,7 +179,7 @@ def create_argument_list_wrapped_explicit(arguments, additional_vars_following = argument_list = argument_list.rstrip(',') return argument_list -def create_arguments_module_use_var_defs(variable_dictionary, metadata_define, tmpvars = None): +def create_arguments_module_use_var_defs(variable_dictionary, metadata_define, tmpvars = None, tmpptrs = None): """Given a dictionary of standard names and variables, and a metadata dictionary with the variable definitions by the host model, create a list of arguments (local names), module use statements (for derived data types @@ -182,6 +188,7 @@ def create_arguments_module_use_var_defs(variable_dictionary, metadata_define, t module_use = [] var_defs = [] local_kind_and_type_vars = [] + local_pointer_type_defs = [] # We need to run through this loop twice. In the first pass, process all scalars. # In the second pass, process all arrays. This is so that any potential dimension @@ -218,12 +225,29 @@ def create_arguments_module_use_var_defs(variable_dictionary, metadata_define, t local_kind_and_type_vars.append(type_var_standard_name) iteration += 1 - # Add any local variables (required for unit conversions, array transformations, ...) - if tmpvars: + # Add any local variables (required for unit conversions, array transformations, ...), + # and add any local pointers (required for conditionally allocated arrays) + if tmpvars or tmpptrs: var_defs.append('') - var_defs.append('! Local variables for unit conversions, array transformations, ...') - for tmpvar in tmpvars: - var_defs.append(tmpvar.print_def_local(metadata_define)) + var_defs.append('! Local variables/pointers for unit conversions, array transformations, ...') + for tmpvar in list(tmpvars) + list(tmpptrs): + # Regular variables + if tmpvar in list(tmpvars): + var_defs.append(tmpvar.print_def_local(metadata_define)) + # Pointers are more complicated + else: + if tmpvar.type == 'character' and 'len=' in tmpvar.kind: + pointer_type_name = f"{tmpvar.type}_{tmpvar.kind.replace('=','')}_r{len(tmpvar.dimensions)}_ptr_arr_type" + elif tmpvar.kind: + pointer_type_name = f"{tmpvar.type}_{tmpvar.kind}_rank{len(tmpvar.dimensions)}_ptr_arr_type" + else: + pointer_type_name = f"{tmpvar.type}_default_kind_rank{len(tmpvar.dimensions)}_ptr_arr_type" + if not pointer_type_name in local_pointer_type_defs: + var_defs.append(TMPPTR_ARR_TYPE_DECLARATION.format(pointer_type_name=pointer_type_name, + tmpptr_def=tmpvar.print_def_local(metadata_define))) + local_pointer_type_defs.append(pointer_type_name) + var_defs.append(TMPPTR_ARR_DECLARATION.format(pointer_type_name=pointer_type_name, + dims=f'1:{CCPP_INTERNAL_VARIABLES[CCPP_THREAD_COUNT]}', localname=tmpvar.local_name)) # Add special kind variables if tmpvar.type in STANDARD_VARIABLE_TYPES and tmpvar.kind and not tmpvar.type == STANDARD_CHARACTER_TYPE: kind_var_standard_name = tmpvar.kind @@ -1062,6 +1086,9 @@ def write(self, metadata_request, metadata_define, arguments, debug): # For mapping temporary variable names (for unit conversions, etc) to local variable names tmpvar_cnt = 0 tmpvars = collections.OrderedDict() + # For mapping temporary pointer names (for potentially unallocated arrays) to local variable names + tmpptr_cnt = 0 + tmpptrs = collections.OrderedDict() # body = '' # Variable definitions automatically added for subroutines @@ -1324,14 +1351,14 @@ def write(self, metadata_request, metadata_define, arguments, debug): array_size = [] dim_substrings = [] for dim in var.dimensions: - # DH* TODO - # By default, don't pass explicit dimensions, only ':'. This is because - # we have not solved the problem of passing inactive arrays correctly - # (something that needs to be done before we can use chunked arrays in - # models like the ufs-weather-model). See also note further down where - # this variable gets set to True and then where it gets used. - use_explicit_dimension = False - # *DH + + # Work around for GNU compiler bugs related to allocatable strings + # in older versions of GNU (at least 9.2.0) + if var.rank and var.type == 'character': + use_explicit_dimension = False + else: + use_explicit_dimension = True + # This is not supported/implemented: tmpvar would have one dimension less # than the original array, and the metadata requesting the variable would # not pass the initial test that host model variables and scheme variables @@ -1434,7 +1461,6 @@ def write(self, metadata_request, metadata_define, arguments, debug): else: if dim0 == dim1: array_size.append('1') - # DH* TODO - is this correct? dim_substrings.append(f':') else: array_size.append(f'({dim1}-{dim0}+1)') @@ -1478,7 +1504,10 @@ def write(self, metadata_request, metadata_define, arguments, debug): # will catch any out of bound reads with the appropriate compiler flags. It naturally # deals with non-uniform block sizes etc. pass - elif var.rank: + # Some older versions of GNU currently in use can not do these variable size tests on strings + # 0x5b6fdd gimplify_expr(tree_node**, gimple**, gimple**, bool (*)(tree_node*), int) + # /tmp/role.apps/spack-stage/spack-stage-gcc-9.2.0-ku6r4f5qa5obpfnqpa6pezhogxq6sp7h/spack-src/gcc/gimplify.c:13477 + elif var.rank and not var.type == 'character': assign_test = ''' ! Check if variable {var_name} is associated/allocated and has the correct size if (size({var_name}{dim_string})/={var_size_expected}) then write({ccpp_errmsg}, '(2(a,i8))') 'Detected size mismatch for variable {var_name}{dim_string} in group {group_name} before {subroutine_name}, expected ', & @@ -1486,7 +1515,9 @@ def write(self, metadata_request, metadata_define, arguments, debug): ierr = 1 return end if -'''.format(var_name=local_vars[var_standard_name]['name'].replace(dim_string_target_name, ''), dim_string=dim_string, var_size_expected=var_size_expected, +'''.format(var_name=local_vars[var_standard_name]['name'].replace(dim_string_target_name, ''), + dim_string=dim_string, + var_size_expected=var_size_expected, ccpp_errmsg=CCPP_INTERNAL_VARIABLES[CCPP_ERROR_MSG_VARIABLE], group_name = self.name, subroutine_name=subroutine_name) # end if debug @@ -1494,53 +1525,66 @@ def write(self, metadata_request, metadata_define, arguments, debug): # kind_string is used for automated unit conversions, i.e. foo_kind_phys kind_string = '_' + local_vars[var_standard_name]['kind'] if local_vars[var_standard_name]['kind'] else '' + # conditional is the conditional allocation, which can be '.true.', '.false.', or any regular Fortran logical expression + conditional=conditionals[var_standard_name] + + # If the host variable is conditionally allocated, create a pointer for it + if not conditional == '.true.': + # Reuse existing temporary pointer variable, if possible; otherwise add a local pointer (tmpptr) + if local_vars[var_standard_name]['name'] in tmpptrs.keys(): + tmpptr = tmpptrs[local_vars[var_standard_name]['name']] + else: + tmpptr_cnt += 1 + tmpptr = copy.deepcopy(var) + tmpptr.local_name = '{0}_{1}_ptr'.format(var.local_name, tmpptr_cnt) + tmpptr.pointer = True + tmpptrs[local_vars[var_standard_name]['name']] = tmpptr + # Convert blocked data in init and finalize steps - only required for variables with block number and horizontal_dimension if ccpp_stage in ['init', 'timestep_init', 'timestep_finalize', 'finalize'] and \ CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER] in local_vars[var_standard_name]['name'] and \ '{}:{}'.format(CCPP_CONSTANT_ONE,CCPP_HORIZONTAL_DIMENSION) in var.dimensions: - # Reuse existing temporary variable, if possible + # Reuse existing temporary variable, if possible; otherwise add a local variable (tmpvar) if local_vars[var_standard_name]['name'] in tmpvars.keys(): - # If the variable already has a local variable (tmpvar), reuse it tmpvar = tmpvars[local_vars[var_standard_name]['name']] actions_in = tmpvar.actions['in'] actions_out = tmpvar.actions['out'] else: - # Add a local variable (tmpvar) for this variable tmpvar_cnt += 1 tmpvar = copy.deepcopy(var) - tmpvar.local_name = '{0}_local'.format(var.local_name) - # - # Create string for allocating the temporary array by converting the dimensions - # (in standard_name format) to local names as known to the host model - alloc_dimensions = [] - for dim in tmpvar.dimensions: - # This is not supported/implemented: tmpvar would have one dimension less - # than the original array, and the metadata requesting the variable would - # not pass the initial test that host model variables and scheme variables - # have the same rank. - if dim == CCPP_BLOCK_NUMBER: - raise Exception("{} cannot be part of the dimensions of variable {}".format( - CCPP_BLOCK_NUMBER, var_standard_name)) + tmpvar.local_name = '{0}_{1}_local'.format(var.local_name, tmpvar_cnt) + + # Create string for allocating the temporary array by converting the dimensions + # (in standard_name format) to local names as known to the host model + alloc_dimensions = [] + for dim in tmpvar.dimensions: + # This is not supported/implemented: tmpvar would have one dimension less + # than the original array, and the metadata requesting the variable would + # not pass the initial test that host model variables and scheme variables + # have the same rank. + if dim == CCPP_BLOCK_NUMBER: + raise Exception("{} cannot be part of the dimensions of variable {}".format( + CCPP_BLOCK_NUMBER, var_standard_name)) + else: + # Handle dimensions like "A:B", "A:3", "-1:Z" + if ':' in dim: + dims = [ x.lower() for x in dim.split(':')] + try: + dim0 = int(dims[0]) + except ValueError: + dim0 = metadata_define[dims[0]][0].local_name + try: + dim1 = int(dims[1]) + except ValueError: + dim1 = metadata_define[dims[1]][0].local_name + # Single dimensions else: - # Handle dimensions like "A:B", "A:3", "-1:Z" - if ':' in dim: - dims = [ x.lower() for x in dim.split(':')] - try: - dim0 = int(dims[0]) - except ValueError: - dim0 = metadata_define[dims[0]][0].local_name - try: - dim1 = int(dims[1]) - except ValueError: - dim1 = metadata_define[dims[1]][0].local_name - # Single dimensions - else: - dim0 = 1 - try: - dim1 = int(dim) - except ValueError: - dim1 = metadata_define[dim][0].local_name - alloc_dimensions.append('{}:{}'.format(dim0,dim1)) + dim0 = 1 + try: + dim1 = int(dim) + except ValueError: + dim1 = metadata_define[dim][0].local_name + alloc_dimensions.append('{}:{}'.format(dim0,dim1)) # Padding of additional dimensions - before and after the horizontal dimension hdim_index = tmpvar.dimensions.index('{}:{}'.format(CCPP_CONSTANT_ONE,CCPP_HORIZONTAL_DIMENSION)) @@ -1602,6 +1646,13 @@ def write(self, metadata_request, metadata_define, arguments, debug): c=var.actions['in'].format(var=tmpvar.local_name, kind=kind_string)) + # If the variable is conditionally allocated, assign pointer + if not conditional == '.true.': + # We don't want the dimstring here - this can lead to dimension mismatches. + # We know for sure that we need to reference the entire de-blocked array anyway. + actions_in += ' {p} => {t}\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p", + t=tmpvar.local_name, d=dim_string) + if var.actions['out']: # Add unit conversion after returning from the subroutine, before copying the non-blocked # data back to the blocked data and deallocating the temporary array @@ -1610,23 +1661,29 @@ def write(self, metadata_request, metadata_define, arguments, debug): kind=kind_string)) + \ actions_out + # If the variable is conditionally allocated, nullify pointer + if not conditional == '.true.': + actions_out += ' nullify({p})\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") + # Add the conditionals for the "before" operations actions_before += ''' if ({conditional}) then {actions_in} end if -'''.format(conditional=conditionals[var_standard_name], - actions_in=actions_in.rstrip('\n')) +'''.format(conditional=conditional, actions_in=actions_in.rstrip('\n')) # Add the conditionals for the "after" operations actions_after += ''' if ({conditional}) then {actions_out} end if -'''.format(conditional=conditionals[var_standard_name], - actions_out=actions_out.rstrip('\n')) +'''.format(conditional=conditional, actions_out=actions_out.rstrip('\n')) # Add to argument list - arg = '{local_name}={var_name},'.format(local_name=var.local_name, var_name=tmpvar.local_name) + if conditional == '.true.': + arg = '{local_name}={var_name},'.format(local_name=var.local_name, var_name=tmpvar.local_name) + else: + arg = '{local_name}={ptr_name},'.format(local_name=var.local_name, + ptr_name=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") # Variables stored in blocked data structures but without horizontal dimension not supported at this time (doesn't make sense anyway) elif ccpp_stage in ['init', 'timestep_init', 'timestep_finalize', 'finalize'] and \ @@ -1656,23 +1713,31 @@ def write(self, metadata_request, metadata_define, arguments, debug): # Add a local variable (tmpvar) for this variable tmpvar_cnt += 1 tmpvar = copy.deepcopy(var) - tmpvar.local_name = 'tmpvar{0}'.format(tmpvar_cnt) + tmpvar.local_name = 'tmpvar_{0}'.format(tmpvar_cnt) tmpvars[local_vars[var_standard_name]['name']] = tmpvar if tmpvar.rank: # Add allocate statement if the variable has a rank > 0 using the dimstring derived above - actions_in += f' allocate({tmpvar.local_name}{dim_string})' + actions_in += f' allocate({tmpvar.local_name}{dim_string})\n' if var.actions['in']: # Add unit conversion before entering the subroutine actions_in += ' {t} = {c}{d}\n'.format(t=tmpvar.local_name, c=var.actions['in'].format(var=tmpvar.target.replace(dim_string_target_name, ''), kind=kind_string), d=dim_string) + # If the variable is conditionally allocated, assign pointer + if not conditional == '.true.': + actions_in += ' {p} => {t}{d}\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p", + t=tmpvar.local_name, d=dim_string) if var.actions['out']: # Add unit conversion after returning from the subroutine actions_out += ' {v}{d} = {c}\n'.format(v=tmpvar.target.replace(dim_string_target_name, ''), d=dim_string, c=var.actions['out'].format(var=tmpvar.local_name, kind=kind_string)) + # If the variable is conditionally allocated, nullify pointer + if not conditional == '.true.': + actions_out += ' nullify({p})\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") + if tmpvar.rank: # Add deallocate statement if the variable has a rank > 0 actions_out += ' deallocate({t})\n'.format(t=tmpvar.local_name) @@ -1682,35 +1747,61 @@ def write(self, metadata_request, metadata_define, arguments, debug): if ({conditional}) then {actions_in} end if -'''.format(conditional=conditionals[var_standard_name], - actions_in=actions_in.rstrip('\n')) +'''.format(conditional=conditional, actions_in=actions_in.rstrip('\n')) # Add the conditionals for the "after" operations actions_after += ''' if ({conditional}) then {actions_out} end if -'''.format(conditional=conditionals[var_standard_name], - actions_out=actions_out.rstrip('\n')) +'''.format(conditional=conditional, actions_out=actions_out.rstrip('\n')) # Add to argument list - arg = '{local_name}={var_name}{dim_string},'.format(local_name=var.local_name, - var_name=tmpvar.local_name.replace(dim_string_target_name, ''), dim_string=dim_string) + if conditional == '.true.': + arg = '{local_name}={var_name}{dim_string},'.format(local_name=var.local_name, + var_name=tmpvar.local_name.replace(dim_string_target_name, ''), dim_string=dim_string) + else: + arg = '{local_name}={ptr_name},'.format(local_name=var.local_name, + ptr_name=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") # Ordinary variables, no blocked data or unit conversions elif var_standard_name in arguments[scheme_name][subroutine_name]: if debug and assign_test: actions_in = assign_test + else: + actions_in = '' + actions_out = '' + # If the variable is conditionally allocated, assign pointer + if not conditional == '.true.': + actions_in += ' {p} => {t}{d}\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p", + t=var.target.replace(dim_string_target_name, ''), + d=dim_string) + # If the variable is conditionally allocated, nullify pointer + if not conditional == '.true.': + actions_out += ' nullify({p})\n'.format(p=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") + + if actions_in: # Add the conditionals for the "before" operations actions_before += ''' if ({conditional}) then {actions_in} end if -'''.format(conditional=conditionals[var_standard_name], - actions_in=actions_in.rstrip('\n')) +'''.format(conditional=conditional, actions_in=actions_in.rstrip('\n')) + if actions_out: + # Add the conditionals for the "after" operations + actions_after += ''' + if ({conditional}) then +{actions_out} + end if +'''.format(conditional=conditional, actions_out=actions_out.rstrip('\n')) # Add to argument list - arg = '{local_name}={var_name}{dim_string},'.format(local_name=var.local_name, - var_name=local_vars[var_standard_name]['name'].replace(dim_string_target_name, ''), dim_string=dim_string) + if conditional == '.true.': + arg = '{local_name}={var_name}{dim_string},'.format(local_name=var.local_name, + var_name=local_vars[var_standard_name]['name'].replace(dim_string_target_name, ''), dim_string=dim_string) + else: + arg = '{local_name}={ptr_name},'.format(local_name=var.local_name, + ptr_name=f"{tmpptr.local_name}_array({CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER]})%p") + else: arg = '' args += arg @@ -1788,7 +1879,8 @@ def write(self, metadata_request, metadata_define, arguments, debug): # Get list of arguments, module use statement and variable definitions for this subroutine (=stage for the group) (self.arguments[ccpp_stage], sub_module_use, sub_var_defs) = create_arguments_module_use_var_defs( - self.parents[ccpp_stage], metadata_define, tmpvars.values()) + self.parents[ccpp_stage], metadata_define, + tmpvars.values(), tmpptrs.values()) sub_argument_list = create_argument_list_wrapped(self.arguments[ccpp_stage]) # Remove duplicates from additional manual variable definitions diff --git a/src/ccpp_types.F90 b/src/ccpp_types.F90 index 12faa75c..7370add4 100644 --- a/src/ccpp_types.F90 +++ b/src/ccpp_types.F90 @@ -43,6 +43,9 @@ module ccpp_types integer, parameter :: CCPP_DEFAULT_CHUNK_NUMBER = -999 integer, parameter :: CCPP_DEFAULT_THREAD_NUMBER = -999 + !> @var The default maximum number of threads for CCPP + integer, parameter :: CCPP_DEFAULT_THREAD_COUNT = -999 + !! \section arg_table_ccpp_t !! \htmlinclude ccpp_t.html !! @@ -64,6 +67,7 @@ module ccpp_types integer :: blk_no = CCPP_DEFAULT_BLOCK_NUMBER integer :: chunk_no = CCPP_DEFAULT_CHUNK_NUMBER integer :: thrd_no = CCPP_DEFAULT_THREAD_NUMBER + integer :: thrd_cnt = CCPP_DEFAULT_THREAD_COUNT integer :: ccpp_instance = 1 contains diff --git a/src/ccpp_types.meta b/src/ccpp_types.meta index 7042d29b..cdec1dc2 100644 --- a/src/ccpp_types.meta +++ b/src/ccpp_types.meta @@ -50,6 +50,12 @@ units = index dimensions = () type = integer +[thrd_cnt] + standard_name = ccpp_thread_count + long_name = total number of threads for threading in CCPP + units = index + dimensions = () + type = integer [ccpp_instance] standard_name = ccpp_instance long_name = ccpp_instance diff --git a/test_prebuild/run_all_tests.sh b/test_prebuild/run_all_tests.sh new file mode 100755 index 00000000..2e199500 --- /dev/null +++ b/test_prebuild/run_all_tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +echo "" && echo "Running unit_tests " && cd unit_tests && ./run_tests.sh && cd .. +echo "" && echo "Running test_opt_arg " && cd test_opt_arg && ./run_test.sh && cd .. +echo "" && echo "Running test_blocked_data" && cd test_blocked_data && ./run_test.sh && cd .. +echo "" && echo "Running test_chunked_data" && cd test_chunked_data && ./run_test.sh && cd .. + +echo "" && echo "Running test_track_variables" && pytest test_track_variables.py \ No newline at end of file diff --git a/test_prebuild/test_blocked_data/CMakeLists.txt b/test_prebuild/test_blocked_data/CMakeLists.txt index fdd5f9af..edce19a3 100644 --- a/test_prebuild/test_blocked_data/CMakeLists.txt +++ b/test_prebuild/test_blocked_data/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.10) project(ccpp_blocked_data VERSION 1.0.0 - LANGUAGES C Fortran) + LANGUAGES Fortran) #------------------------------------------------------------------------------ # Request a static build @@ -11,7 +11,7 @@ option(BUILD_SHARED_LIBS "Build a shared library" OFF) #------------------------------------------------------------------------------ # Set MPI flags for C/C++/Fortran with MPI F08 interface -find_package(MPI REQUIRED C Fortran) +find_package(MPI REQUIRED Fortran) if(NOT MPI_Fortran_HAVE_F08_MODULE) message(FATAL_ERROR "MPI implementation does not support the Fortran 2008 mpi_f08 interface") endif() diff --git a/test_prebuild/test_blocked_data/run_test.sh b/test_prebuild/test_blocked_data/run_test.sh new file mode 100755 index 00000000..ee67d183 --- /dev/null +++ b/test_prebuild/test_blocked_data/run_test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +rm -fr build +mkdir build +../../scripts/ccpp_prebuild.py --debug --config=ccpp_prebuild_config.py --builddir=build +cd build +cmake .. 2>&1 | tee log.cmake +make 2>&1 | tee log.make +./test_blocked_data.x +cd .. +rm -fr build diff --git a/test_prebuild/test_chunked_data/CMakeLists.txt b/test_prebuild/test_chunked_data/CMakeLists.txt index 512a5a7e..e2e7cf93 100644 --- a/test_prebuild/test_chunked_data/CMakeLists.txt +++ b/test_prebuild/test_chunked_data/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.10) project(ccpp_chunked_data VERSION 1.0.0 - LANGUAGES C Fortran) + LANGUAGES Fortran) #------------------------------------------------------------------------------ # Request a static build @@ -11,7 +11,7 @@ option(BUILD_SHARED_LIBS "Build a shared library" OFF) #------------------------------------------------------------------------------ # Set MPI flags for C/C++/Fortran with MPI F08 interface -find_package(MPI REQUIRED C Fortran) +find_package(MPI REQUIRED Fortran) if(NOT MPI_Fortran_HAVE_F08_MODULE) message(FATAL_ERROR "MPI implementation does not support the Fortran 2008 mpi_f08 interface") endif() diff --git a/test_prebuild/test_chunked_data/main.F90 b/test_prebuild/test_chunked_data/main.F90 index cad6f2b6..8864aa80 100644 --- a/test_prebuild/test_chunked_data/main.F90 +++ b/test_prebuild/test_chunked_data/main.F90 @@ -28,14 +28,16 @@ program test_chunked_data ! set to 0, indicating that arrays are to be sent ! following their dimension specification in the ! metadata (must match horizontal_dimension). - ccpp_data_domain%thrd_no = 0 + ccpp_data_domain%thrd_no = 0 ccpp_data_domain%chunk_no = 0 + ccpp_data_domain%thrd_cnt = 1 ! Loop over all chunks and threads for ccpp_data_chunks do ic=1,nchunks ! Assign the correct chunk numbers, only one thread ccpp_data_chunks(ic)%chunk_no = ic - ccpp_data_chunks(ic)%thrd_no = 1 + ccpp_data_chunks(ic)%thrd_no = 1 + ccpp_data_chunks(ic)%thrd_cnt = 1 end do call chunked_data_instance%create(ncols) diff --git a/test_prebuild/test_chunked_data/run_test.sh b/test_prebuild/test_chunked_data/run_test.sh new file mode 100755 index 00000000..9ba2a36e --- /dev/null +++ b/test_prebuild/test_chunked_data/run_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +rm -fr build +mkdir build +../../scripts/ccpp_prebuild.py --debug --config=ccpp_prebuild_config.py --builddir=build +cd build +cmake .. 2>&1 | tee log.cmake +make 2>&1 | tee log.make +./test_chunked_data.x +cd .. +rm -fr build diff --git a/test_prebuild/test_opt_arg/CMakeLists.txt b/test_prebuild/test_opt_arg/CMakeLists.txt new file mode 100644 index 00000000..58e6e6b5 --- /dev/null +++ b/test_prebuild/test_opt_arg/CMakeLists.txt @@ -0,0 +1,98 @@ +#------------------------------------------------------------------------------ +cmake_minimum_required(VERSION 3.10) + +project(ccpp_opt_arg + VERSION 1.0.0 + LANGUAGES Fortran) + +#------------------------------------------------------------------------------ +# Request a static build +option(BUILD_SHARED_LIBS "Build a shared library" OFF) + +#------------------------------------------------------------------------------ +# Set MPI flags for C/C++/Fortran with MPI F08 interface +find_package(MPI REQUIRED Fortran) +if(NOT MPI_Fortran_HAVE_F08_MODULE) + message(FATAL_ERROR "MPI implementation does not support the Fortran 2008 mpi_f08 interface") +endif() + +#------------------------------------------------------------------------------ +# Set the sources: physics type definitions +set(TYPEDEFS $ENV{CCPP_TYPEDEFS}) +if(TYPEDEFS) + message(STATUS "Got CCPP TYPEDEFS from environment variable: ${TYPEDEFS}") +else(TYPEDEFS) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_TYPEDEFS.cmake) + message(STATUS "Got CCPP TYPEDEFS from cmakefile include file: ${TYPEDEFS}") +endif(TYPEDEFS) + +# Generate list of Fortran modules from the CCPP type +# definitions that need need to be installed +foreach(typedef_module ${TYPEDEFS}) + list(APPEND MODULES_F90 ${CMAKE_CURRENT_BINARY_DIR}/${typedef_module}) +endforeach() + +#------------------------------------------------------------------------------ +# Set the sources: physics schemes +set(SCHEMES $ENV{CCPP_SCHEMES}) +if(SCHEMES) + message(STATUS "Got CCPP SCHEMES from environment variable: ${SCHEMES}") +else(SCHEMES) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_SCHEMES.cmake) + message(STATUS "Got CCPP SCHEMES from cmakefile include file: ${SCHEMES}") +endif(SCHEMES) + +# Set the sources: physics scheme caps +set(CAPS $ENV{CCPP_CAPS}) +if(CAPS) + message(STATUS "Got CCPP CAPS from environment variable: ${CAPS}") +else(CAPS) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_CAPS.cmake) + message(STATUS "Got CCPP CAPS from cmakefile include file: ${CAPS}") +endif(CAPS) + +# Set the sources: physics scheme caps +set(API $ENV{CCPP_API}) +if(API) + message(STATUS "Got CCPP API from environment variable: ${API}") +else(API) + include(${CMAKE_CURRENT_BINARY_DIR}/CCPP_API.cmake) + message(STATUS "Got CCPP API from cmakefile include file: ${API}") +endif(API) + +set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} -O0 -fno-unsafe-math-optimizations -frounding-math -fsignaling-nans -ffpe-trap=invalid,zero,overflow -fcheck=all -ggdb -fbacktrace -ffree-line-length-none") + +#------------------------------------------------------------------------------ +add_library(ccpp_opt_arg STATIC ${SCHEMES} ${CAPS} ${API}) +target_link_libraries(ccpp_opt_arg PRIVATE MPI::MPI_Fortran) +# Generate list of Fortran modules from defined sources +foreach(source_f90 ${CAPS} ${API}) + get_filename_component(tmp_source_f90 ${source_f90} NAME) + string(REGEX REPLACE ".F90" ".mod" tmp_module_f90 ${tmp_source_f90}) + string(TOLOWER ${tmp_module_f90} module_f90) + list(APPEND MODULES_F90 ${CMAKE_CURRENT_BINARY_DIR}/${module_f90}) +endforeach() + +set_target_properties(ccpp_opt_arg PROPERTIES VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) + +add_executable(test_opt_arg.x main.F90) +add_dependencies(test_opt_arg.x ccpp_opt_arg) +target_link_libraries(test_opt_arg.x ccpp_opt_arg) +set_target_properties(test_opt_arg.x PROPERTIES LINKER_LANGUAGE Fortran) + +# Define where to install the library +install(TARGETS ccpp_opt_arg + EXPORT ccpp_opt_arg-targets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib +) +# Export our configuration +install(EXPORT ccpp_opt_arg-targets + FILE ccpp_opt_arg-config.cmake + DESTINATION lib/cmake +) +# Define where to install the C headers and Fortran modules +#install(FILES ${HEADERS_C} DESTINATION include) +install(FILES ${MODULES_F90} DESTINATION include) diff --git a/test_prebuild/test_opt_arg/ccpp_kinds.F90 b/test_prebuild/test_opt_arg/ccpp_kinds.F90 new file mode 100644 index 00000000..cf6bfeaf --- /dev/null +++ b/test_prebuild/test_opt_arg/ccpp_kinds.F90 @@ -0,0 +1,13 @@ +module ccpp_kinds + +!! \section arg_table_ccpp_kinds +!! \htmlinclude ccpp_kinds.html +!! + + use iso_fortran_env, only: real64 + + implicit none + + integer, parameter :: kind_phys = real64 + +end module ccpp_kinds diff --git a/test_prebuild/test_opt_arg/ccpp_kinds.meta b/test_prebuild/test_opt_arg/ccpp_kinds.meta new file mode 100644 index 00000000..0e95702e --- /dev/null +++ b/test_prebuild/test_opt_arg/ccpp_kinds.meta @@ -0,0 +1,15 @@ +[ccpp-table-properties] + name = ccpp_kinds + type = module + dependencies = + +######################################################################## +[ccpp-arg-table] + name = ccpp_kinds + type = module +[kind_phys] + standard_name = kind_phys + long_name = definition of kind_phys + units = none + dimensions = () + type = integer diff --git a/test_prebuild/test_opt_arg/ccpp_prebuild_config.py b/test_prebuild/test_opt_arg/ccpp_prebuild_config.py new file mode 100755 index 00000000..bf3bc1cc --- /dev/null +++ b/test_prebuild/test_opt_arg/ccpp_prebuild_config.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# CCPP prebuild config for GFDL Finite-Volume Cubed-Sphere Model (FV3) + +############################################################################### +# Definitions # +############################################################################### + +HOST_MODEL_IDENTIFIER = "FV3" + +# Add all files with metadata tables on the host model side and in CCPP, +# relative to basedir = top-level directory of host model. This includes +# kind and type definitions used in CCPP physics. Also add any internal +# dependencies of these files to the list. +VARIABLE_DEFINITION_FILES = [ + # actual variable definition files + '../../src/ccpp_types.F90', + 'ccpp_kinds.F90', + 'data.F90', + ] + +TYPEDEFS_NEW_METADATA = { + 'ccpp_types' : { + 'ccpp_t' : 'cdata', + 'ccpp_types' : '', + }, + 'data' : { + 'data' : '', + }, + } + +# Add all physics scheme files relative to basedir +SCHEME_FILES = [ + 'opt_arg_scheme.F90', + ] + +# Default build dir, relative to current working directory, +# if not specified as command-line argument +DEFAULT_BUILD_DIR = 'build' + +# Auto-generated makefile/cmakefile snippets that contain all type definitions +TYPEDEFS_MAKEFILE = '{build_dir}/CCPP_TYPEDEFS.mk' +TYPEDEFS_CMAKEFILE = '{build_dir}/CCPP_TYPEDEFS.cmake' +TYPEDEFS_SOURCEFILE = '{build_dir}/CCPP_TYPEDEFS.sh' + +# Auto-generated makefile/cmakefile snippets that contain all schemes +SCHEMES_MAKEFILE = '{build_dir}/CCPP_SCHEMES.mk' +SCHEMES_CMAKEFILE = '{build_dir}/CCPP_SCHEMES.cmake' +SCHEMES_SOURCEFILE = '{build_dir}/CCPP_SCHEMES.sh' + +# Auto-generated makefile/cmakefile snippets that contain all caps +CAPS_MAKEFILE = '{build_dir}/CCPP_CAPS.mk' +CAPS_CMAKEFILE = '{build_dir}/CCPP_CAPS.cmake' +CAPS_SOURCEFILE = '{build_dir}/CCPP_CAPS.sh' + +# Directory where to put all auto-generated physics caps +CAPS_DIR = '{build_dir}' + +# Directory where the suite definition files are stored +SUITES_DIR = '.' + +# Optional arguments - only required for schemes that use +# optional arguments. ccpp_prebuild.py will throw an exception +# if it encounters a scheme subroutine with optional arguments +# if no entry is made here. Possible values are: 'all', 'none', +# or a list of standard_names: [ 'var1', 'var3' ]. +OPTIONAL_ARGUMENTS = {} + +# Directory where to write static API to +STATIC_API_DIR = '{build_dir}' +STATIC_API_CMAKEFILE = '{build_dir}/CCPP_API.cmake' +STATIC_API_SOURCEFILE = '{build_dir}/CCPP_API.sh' + +# Directory for writing HTML pages generated from metadata files +METADATA_HTML_OUTPUT_DIR = '{build_dir}' + +# HTML document containing the model-defined CCPP variables +HTML_VARTABLE_FILE = '{build_dir}/CCPP_VARIABLES_opt_arg.html' + +# LaTeX document containing the provided vs requested CCPP variables +LATEX_VARTABLE_FILE = '{build_dir}/CCPP_VARIABLES_opt_arg.tex' diff --git a/test_prebuild/test_opt_arg/data.F90 b/test_prebuild/test_opt_arg/data.F90 new file mode 100644 index 00000000..e16051fd --- /dev/null +++ b/test_prebuild/test_opt_arg/data.F90 @@ -0,0 +1,23 @@ +module data + +!! \section arg_table_data Argument Table +!! \htmlinclude data.html +!! + use ccpp_types, only: ccpp_t + use ccpp_kinds, only: kind_phys + + implicit none + + private + + public cdata, nx, flag_for_opt_arg, std_arg, opt_arg, opt_arg_2 + + type(ccpp_t), target :: cdata + integer, parameter :: nx = 3 + logical :: flag_for_opt_arg + + integer, dimension(nx) :: std_arg + integer, dimension(:), allocatable :: opt_arg + real(kind=kind_phys), dimension(:), allocatable :: opt_arg_2 + +end module data diff --git a/test_prebuild/test_opt_arg/data.meta b/test_prebuild/test_opt_arg/data.meta new file mode 100644 index 00000000..03f3c472 --- /dev/null +++ b/test_prebuild/test_opt_arg/data.meta @@ -0,0 +1,46 @@ +[ccpp-table-properties] + name = data + type = module + dependencies = +[ccpp-arg-table] + name = data + type = module +[cdata] + standard_name = ccpp_t_instance + long_name = instance of derived data type ccpp_t + units = DDT + dimensions = () + type = ccpp_t +[nx] + standard_name = size_of_std_arg + long_name = size of std_arg + units = count + dimensions = () + type = integer +[std_arg] + standard_name = std_arg + long_name = mandatory variable + units = 1 + dimensions = (size_of_std_arg) + type = integer +[opt_arg] + standard_name = opt_arg + long_name = optional variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + active = flag_for_opt_arg +[opt_arg_2] + standard_name = opt_arg_2 + long_name = optional variable with unit conversions + units = km + dimensions = (size_of_std_arg) + type = real + kind = kind_phys + active = flag_for_opt_arg +[flag_for_opt_arg] + standard_name = flag_for_opt_arg + long_name = flag for optional variable + units = 1 + dimensions = () + type = logical diff --git a/test_prebuild/test_opt_arg/main.F90 b/test_prebuild/test_opt_arg/main.F90 new file mode 100644 index 00000000..932958bc --- /dev/null +++ b/test_prebuild/test_opt_arg/main.F90 @@ -0,0 +1,119 @@ +program test_opt_arg + + use, intrinsic :: iso_fortran_env, only: output_unit, error_unit + + use ccpp_types, only: ccpp_t + use data, only: cdata, nx, flag_for_opt_arg, std_arg, opt_arg, opt_arg_2 + + use ccpp_static_api, only: ccpp_physics_init, & + ccpp_physics_timestep_init, & + ccpp_physics_run, & + ccpp_physics_timestep_finalize, & + ccpp_physics_finalize + + implicit none + + character(len=*), parameter :: ccpp_suite = 'opt_arg_suite' + integer :: ierr + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + cdata%blk_no = 1 + cdata%thrd_no = 1 + cdata%thrd_cnt = 1 + + std_arg = 1 + flag_for_opt_arg = .true. + allocate(opt_arg(nx)) + allocate(opt_arg_2(nx)) + + ! std_arg must all be 1, opt_arg must all be 0 + write(output_unit,'(a)') "After ccpp_init: check std_arg(:)==1, opt_arg(:)==0, opt_arg_2(:)==0" + if (.not. all(std_arg .eq. 1)) write(error_unit,'(a,3i3)') "Error after ccpp_init: std_arg=", std_arg + if (.not. all(opt_arg .eq. 0)) write(error_unit,'(a,3i3)') "Error after ccpp_init: opt_arg=", opt_arg + if (.not. all(opt_arg_2 .eq. 0)) write(error_unit,'(a,3i3)') "Error after ccpp_init: opt_arg_2=", opt_arg_2 + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + call ccpp_physics_init(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + ! std_arg must all be 1, opt_arg must all be 0 + write(output_unit,'(a)') "After ccpp_physics_init: check std_arg(:)==1 and opt_arg(:)==0" + if (.not. all(std_arg .eq. 1)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_init: std_arg=", std_arg + if (.not. all(opt_arg .eq. 0)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_init: opt_arg=", opt_arg + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics timestep init step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + call ccpp_physics_timestep_init(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + ! std_arg must all be 1, opt_arg must all be 2 + write(output_unit,'(a)') "After ccpp_physics_timestep_init: check std_arg(:)==1 and opt_arg(:)==2" + if (.not. all(std_arg .eq. 1)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_timestep_init: std_arg=", std_arg + if (.not. all(opt_arg .eq. 2)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_timestep_init: opt_arg=", opt_arg + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics run step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + call ccpp_physics_run(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_run:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + ! std_arg must all be 1, opt_arg must all be 3 + write(output_unit,'(a)') "After ccpp_physics_run: check std_arg(:)==1 and opt_arg(:)==3" + if (.not. all(std_arg .eq. 1)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_run: std_arg=", std_arg + if (.not. all(opt_arg .eq. 3)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_run: opt_arg=", opt_arg + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics timestep finalize step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + deallocate(opt_arg) + flag_for_opt_arg = .false. + + call ccpp_physics_timestep_finalize(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + ! std_arg must all be 7, opt_arg no longer allocated + write(output_unit,'(a)') "After ccpp_physics_timestep_final: check std_arg(:)==7; opt_arg not allocated" + if (.not. all(std_arg .eq. 7)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_timestep_final: std_arg=", std_arg + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! CCPP physics finalize step ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + call ccpp_physics_finalize(cdata, suite_name=trim(ccpp_suite), ierr=ierr) + if (ierr/=0) then + write(error_unit,'(a)') "An error occurred in ccpp_physics_timestep_init:" + write(error_unit,'(a)') trim(cdata%errmsg) + stop 1 + end if + + ! std_arg must all be 7, opt_arg no longer allocated + write(output_unit,'(a)') "After ccpp_physics_timestep_final: check std_arg(:)==7; opt_arg not allocated" + if (.not. all(std_arg .eq. 7)) write(error_unit,'(a,3i3)') "Error after ccpp_physics_timestep_final: std_arg=", std_arg + +end program test_opt_arg diff --git a/test_prebuild/test_opt_arg/opt_arg_scheme.F90 b/test_prebuild/test_opt_arg/opt_arg_scheme.F90 new file mode 100644 index 00000000..1a36fffd --- /dev/null +++ b/test_prebuild/test_opt_arg/opt_arg_scheme.F90 @@ -0,0 +1,90 @@ +!>\file opt_arg_scheme.F90 +!! This file contains a opt_arg_scheme CCPP scheme that does nothing +!! except requesting the minimum, mandatory variables. + +module opt_arg_scheme + + use, intrinsic :: iso_fortran_env, only: error_unit + use ccpp_kinds, only: kind_phys + + implicit none + + private + public :: opt_arg_scheme_timestep_init, & + opt_arg_scheme_run, & + opt_arg_scheme_timestep_finalize + + contains + +!! \section arg_table_opt_arg_scheme_timestep_init Argument Table +!! \htmlinclude opt_arg_scheme_timestep_init.html +!! + subroutine opt_arg_scheme_timestep_init(nx, var, opt_var, opt_var_2, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: nx + integer, intent(in) :: var(:) + integer, optional, intent(out) :: opt_var(:) + real(kind=kind_phys), optional, intent(out) :: opt_var_2(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Initialize opt_var from var if opt_var if present + if (present(opt_var)) then + opt_var = 2*var + end if + ! Initialize opt_var_2 from var if opt_var_2 present + if (present(opt_var_2)) then + opt_var_2 = 3.0_kind_phys*var + end if + end subroutine opt_arg_scheme_timestep_init + +!! \section arg_table_opt_arg_scheme_run Argument Table +!! \htmlinclude opt_arg_scheme_run.html +!! + subroutine opt_arg_scheme_run(nx, var, opt_var, opt_var_2, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: nx + integer, intent(in) :: var(:) + integer, optional, intent(inout) :: opt_var(:) + real(kind=kind_phys), optional, intent(inout) :: opt_var_2(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Update opt_var from var if opt_var present + if (present(opt_var)) then + opt_var = 3*var + end if + ! Update opt_var_2 from var if opt_var_2 present + if (present(opt_var_2)) then + opt_var_2 = 4.0_kind_phys*var + end if + end subroutine opt_arg_scheme_run + +!! \section arg_table_opt_arg_scheme_timestep_finalize Argument Table +!! \htmlinclude opt_arg_scheme_timestep_finalize.html +!! + subroutine opt_arg_scheme_timestep_finalize(nx, var, opt_var, opt_var_2, errmsg, errflg) + character(len=*), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(in) :: nx + integer, intent(inout) :: var(:) + integer, optional, intent(in) :: opt_var(:) + real(kind=kind_phys), optional, intent(inout) :: opt_var_2(:) + ! Initialize CCPP error handling variables + errmsg = '' + errflg = 0 + ! Update var from opt_var if opt_var is present + if (present(opt_var)) then + var = 4*opt_var + else + var = 7*var + end if + ! Update opt_var_2 if present + if (present(opt_var_2)) then + opt_var_2 = opt_var_2 + 5.0_kind_phys + end if + end subroutine opt_arg_scheme_timestep_finalize + +end module opt_arg_scheme diff --git a/test_prebuild/test_opt_arg/opt_arg_scheme.meta b/test_prebuild/test_opt_arg/opt_arg_scheme.meta new file mode 100644 index 00000000..a00519ec --- /dev/null +++ b/test_prebuild/test_opt_arg/opt_arg_scheme.meta @@ -0,0 +1,157 @@ +[ccpp-table-properties] + name = opt_arg_scheme + type = scheme + dependencies = ccpp_kinds.F90 + +######################################################################## +[ccpp-arg-table] + name = opt_arg_scheme_timestep_init + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[nx] + standard_name = size_of_std_arg + long_name = size of std_arg + units = count + dimensions = () + type = integer + intent = in +[var] + standard_name = std_arg + long_name = mandatory variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = in +[opt_var] + standard_name = opt_arg + long_name = optional variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = out + optional = True +[opt_var_2] + standard_name = opt_arg_2 + long_name = optional variable with unit conversions + units = m + dimensions = (size_of_std_arg) + type = real + kind = kind_phys + intent = out + optional = True + +######################################################################## +[ccpp-arg-table] + name = opt_arg_scheme_run + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[nx] + standard_name = size_of_std_arg + long_name = size of std_arg + units = count + dimensions = () + type = integer + intent = in +[var] + standard_name = std_arg + long_name = mandatory variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = in +[opt_var] + standard_name = opt_arg + long_name = optional variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = inout + optional = True +[opt_var_2] + standard_name = opt_arg_2 + long_name = optional variable with unit conversions + units = m + dimensions = (size_of_std_arg) + type = real + kind = kind_phys + intent = inout + optional = True + +######################################################################## +[ccpp-arg-table] + name = opt_arg_scheme_timestep_finalize + type = scheme +[errmsg] + standard_name = ccpp_error_message + long_name = error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=* + intent = out +[errflg] + standard_name = ccpp_error_code + long_name = error code for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +[nx] + standard_name = size_of_std_arg + long_name = size of std_arg + units = count + dimensions = () + type = integer + intent = in +[var] + standard_name = std_arg + long_name = mandatory variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = inout +[opt_var] + standard_name = opt_arg + long_name = optional variable + units = 1 + dimensions = (size_of_std_arg) + type = integer + intent = in + optional = True +[opt_var_2] + standard_name = opt_arg_2 + long_name = optional variable with unit conversions + units = m + dimensions = (size_of_std_arg) + type = real + kind = kind_phys + intent = inout + optional = True diff --git a/test_prebuild/test_opt_arg/run_test.sh b/test_prebuild/test_opt_arg/run_test.sh new file mode 100755 index 00000000..cbe50e6d --- /dev/null +++ b/test_prebuild/test_opt_arg/run_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +rm -fr build +mkdir build +../../scripts/ccpp_prebuild.py --debug --config=ccpp_prebuild_config.py --builddir=build +cd build +cmake .. 2>&1 | tee log.cmake +make 2>&1 | tee log.make +./test_opt_arg.x +cd .. +rm -fr build diff --git a/test_prebuild/test_opt_arg/suite_opt_arg_suite.xml b/test_prebuild/test_opt_arg/suite_opt_arg_suite.xml new file mode 100644 index 00000000..e66514a4 --- /dev/null +++ b/test_prebuild/test_opt_arg/suite_opt_arg_suite.xml @@ -0,0 +1,9 @@ + + + + + + opt_arg_scheme + + + diff --git a/test_prebuild/unit_tests/run_tests.sh b/test_prebuild/unit_tests/run_tests.sh new file mode 100755 index 00000000..3b80c071 --- /dev/null +++ b/test_prebuild/unit_tests/run_tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +THIS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +FRAMEWORK_DIR="${THIS_DIR}/../.." + +export PYTHONPATH="${FRAMEWORK_DIR}/scripts/parse_tools:${FRAMEWORK_DIR}/scripts:${PYTHONPATH}" + +set -ex +python3 ./test_metadata_parser.py +python3 ./test_mkstatic.py diff --git a/test_prebuild/test_metadata_parser.py b/test_prebuild/unit_tests/test_metadata_parser.py similarity index 100% rename from test_prebuild/test_metadata_parser.py rename to test_prebuild/unit_tests/test_metadata_parser.py diff --git a/test_prebuild/test_mkstatic.py b/test_prebuild/unit_tests/test_mkstatic.py similarity index 100% rename from test_prebuild/test_mkstatic.py rename to test_prebuild/unit_tests/test_mkstatic.py