#!/usr/bin/env python3 # import collections import copy import getopt import filecmp import logging import os import re import sys import types import xml.etree.ElementTree as ET from common import encode_container from common import CCPP_STAGES from common import 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_CONSTANT_ONE, CCPP_HORIZONTAL_DIMENSION, CCPP_HORIZONTAL_LOOP_EXTENT from common import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX from common import CCPP_TYPE, STANDARD_VARIABLE_TYPES, STANDARD_CHARACTER_TYPE from common import CCPP_STATIC_API_MODULE, CCPP_STATIC_SUBROUTINE_NAME from metadata_parser import CCPP_MANDATORY_VARIABLES from mkcap import Var ############################################################################### # Limit suite names to 37 characters; this keeps cap names below 64 characters # Cap names of 64 characters or longer can cause issues with some compilers. SUITE_NAME_MAX_CHARS = 37 # Maximum number of dimensions of an array allowed by the Fortran 2008 standard FORTRAN_ARRAY_MAX_DIMS = 15 # These variables always need to be present for creating suite and group caps CCPP_SUITE_VARIABLES = { **CCPP_MANDATORY_VARIABLES, CCPP_LOOP_COUNTER : Var(local_name = 'loop_cnt', standard_name = CCPP_LOOP_COUNTER, long_name = 'loop counter for subcycling loops in CCPP', units = 'index', type = 'integer', dimensions = [], rank = '', kind = '', intent = 'in', active = 'T', ), CCPP_LOOP_EXTENT : Var(local_name = 'loop_max', standard_name = CCPP_LOOP_EXTENT, long_name = 'loop counter for subcycling loops in CCPP', units = 'count', type = 'integer', dimensions = [], rank = '', kind = '', intent = 'in', active = 'T', ), } ############################################################################### def extract_parents_and_indices_from_local_name(local_name): """Break apart local_name into the different components (members of DDTs) to determine all variables that are required; this must work for complex constructs such as Atm(mytile)%q(:,:,:,Atm2(mytile2)%graupel), with result parent = 'Atm', indices = [mytile, Atm2, mytile2]""" # First, extract all variables/indices in parentheses (used for subsetting) indices = [] while '(' in local_name: for i in range(len(local_name)): if local_name[i] == '(': last_open = i elif local_name[i] == ')': last_closed = i break index_set = local_name[last_open+1:last_closed].split(',') for index_group in index_set: for index in index_group.split(':'): if index: if '%' in index: indices.append(index[:index.find('%')]) else: # Skip hard-coded integers that are not variables try: int(index) except ValueError: indices.append(index) # Remove this innermost index group (...) from local_name local_name = local_name.replace(local_name[last_open:last_closed+1], '') # Remove duplicates from indices indices = list(set(indices)) # Derive parent of actual variable (now that all subsets have been processed) if '%' in local_name: parent = local_name[:local_name.find('%')] else: parent = local_name # Remove whitespaces parent = parent.strip() indices = [ x.strip() for x in indices ] return (parent, indices) def create_argument_list_wrapped(arguments): """Create a wrapped argument list, remove trailing ',' """ argument_list = '' length = 0 for argument in arguments: argument_list += argument + ',' length += len(argument)+1 # Split args so that lines don't exceed 260 characters (for PGI) if length > 70 and not argument == arguments[-1]: argument_list += ' &\n ' length = 0 if argument_list: argument_list = argument_list.rstrip(',') return argument_list def create_argument_list_wrapped_explicit(arguments, additional_vars_following = False): """Create a wrapped argument list with explicit arguments x=y. If no additional variables are added (additional_vars_following == False), remove trailing ',' """ argument_list = '' length = 0 for argument in arguments: argument_list += argument + '=' + argument + ',' length += 2*len(argument)+2 # Split args so that lines don't exceed 260 characters (for PGI) if length > 70 and not argument == arguments[-1]: argument_list += ' &\n ' length = 0 if argument_list and not additional_vars_following: argument_list = argument_list.rstrip(',') return argument_list def create_arguments_module_use_var_defs(variable_dictionary, metadata_define, tmpvars = 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 and non-standard kinds), and the variable definition statements.""" arguments = [] module_use = [] var_defs = [] local_kind_and_type_vars = [] # 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 # that is used in the following array variable definitions is defined first to avoid # violating the Fortran 2008 standard. # https://community.intel.com/t5/Intel-Fortran-Compiler/Order-of-declaration-statements-with-and-without-implicit-typing/td-p/1176155 iteration = 1 while iteration <= 2: for standard_name in variable_dictionary.keys(): if iteration == 1 and variable_dictionary[standard_name].dimensions: continue elif iteration == 2 and not variable_dictionary[standard_name].dimensions: continue # Add variable local name and variable definitions arguments.append(variable_dictionary[standard_name].local_name) var_defs.append(variable_dictionary[standard_name].print_def_intent(metadata_define)) # Add special kind variables and derived data type definitions to module use statements if variable_dictionary[standard_name].type in STANDARD_VARIABLE_TYPES and variable_dictionary[standard_name].kind \ and not variable_dictionary[standard_name].type == STANDARD_CHARACTER_TYPE: kind_var_standard_name = variable_dictionary[standard_name].kind if not kind_var_standard_name in local_kind_and_type_vars: if not kind_var_standard_name in metadata_define.keys(): raise Exception("Kind {kind} not defined by host model".format(kind=kind_var_standard_name)) kind_var = metadata_define[kind_var_standard_name][0] module_use.append(kind_var.print_module_use()) local_kind_and_type_vars.append(kind_var_standard_name) elif not variable_dictionary[standard_name].type in STANDARD_VARIABLE_TYPES: type_var_standard_name = variable_dictionary[standard_name].type if not type_var_standard_name in local_kind_and_type_vars: if not type_var_standard_name in metadata_define.keys(): raise Exception("Type {type} not defined by host model".format(type=type_var_standard_name)) type_var = metadata_define[type_var_standard_name][0] module_use.append(type_var.print_module_use()) local_kind_and_type_vars.append(type_var_standard_name) iteration += 1 # Add any local variables (required for unit conversions, array transformations, ...) if tmpvars: 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)) # 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 if not kind_var_standard_name in local_kind_and_type_vars: if not kind_var_standard_name in metadata_define.keys(): raise Exception("Kind {kind} not defined by host model".format(kind=kind_var_standard_name)) kind_var = metadata_define[kind_var_standard_name][0] module_use.append(kind_var.print_module_use()) local_kind_and_type_vars.append(kind_var_standard_name) return (arguments, module_use, var_defs) class API(object): header=''' ! ! This work (Common Community Physics Package), 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 API for the CCPP static build !! ! module {module} {module_use} implicit none private public :: {subroutines} contains ''' sub = ''' subroutine {subroutine}({ccpp_var_name}, suite_name, group_name, ierr) use ccpp_types, only : ccpp_t implicit none type(ccpp_t), intent(inout) :: {ccpp_var_name} character(len=*), intent(in) :: suite_name character(len=*), optional, intent(in) :: group_name integer, intent(out) :: ierr ierr = 0 {suite_switch} else write({ccpp_var_name}%errmsg,'(*(a))') 'Invalid suite ' // trim(suite_name) ierr = 1 end if {ccpp_var_name}%errflg = ierr end subroutine {subroutine} ''' footer = ''' end module {module} ''' def __init__(self, **kwargs): self._filename = CCPP_STATIC_API_MODULE + '.F90' self._module = CCPP_STATIC_API_MODULE self._subroutines = None self._suites = [] self._directory = '.' self._update_api = True for key, value in kwargs.items(): setattr(self, "_"+key, value) @property def filename(self): '''Get the filename to write API to.''' return self._filename @filename.setter def filename(self, value): self._filename = value @property def directory(self): '''Get the directory to write API to.''' return self._directory @directory.setter def directory(self, value): self._directory = value @property def update_api(self): '''Get the update_api flag.''' return self._update_api @update_api.setter def update_api(self, value): self._update_api = value @property def module(self): '''Get the module name of the API.''' return self._module @module.setter def module(self, value): self._module = value @property def subroutines(self): '''Get the subroutines names of the API to.''' return self._subroutines def write(self): """Write API for static build""" if not self._suites: raise Exception("No suites specified for generating API") suites = self._suites # Module use statements for suite and group caps module_use = '' for suite in suites: for subroutine in suite.subroutines: module_use += ' use {module}, only: {subroutine}\n'.format(module=suite.module, subroutine=subroutine) for group in suite.groups: for subroutine in group.subroutines: module_use += ' use {module}, only: {subroutine}\n'.format(module=group.module, subroutine=subroutine) # Add all variables required to module use statements. This is for the API only, # because the static API imports all variables from modules instead of receiving them # via the argument list. Special handling for a single variable of type CCPP_TYPE (ccpp_t), # which comes in as a scalar for any potential block/thread via the argument list. ccpp_var = None parent_standard_names = [] for ccpp_stage in CCPP_STAGES.keys(): for suite in suites: for parent_standard_name in suite.parents[ccpp_stage].keys(): if not parent_standard_name in parent_standard_names: parent_var = suite.parents[ccpp_stage][parent_standard_name] # Identify which variable is of type CCPP_TYPE (need local name) if parent_var.type == CCPP_TYPE: if ccpp_var and not ccpp_var.local_name==parent_var.local_name: raise Exception('There can be only one variable of type {0}, found {1} and {2}'.format( CCPP_TYPE, ccpp_var.local_name, parent_var.local_name)) ccpp_var = parent_var continue module_use += ' {0}\n'.format(parent_var.print_module_use()) parent_standard_names.append(parent_standard_name) if not ccpp_var: raise Exception('No variable of type {0} found - need a scalar instance.'.format(CCPP_TYPE)) elif not ccpp_var.rank == '': raise Exception('CCPP variable {0} of type {1} must be a scalar.'.format(ccpp_var.local_name, CCPP_TYPE)) del parent_standard_names # Create a subroutine for each stage self._subroutines=[] subs = '' for ccpp_stage in CCPP_STAGES.keys(): suite_switch = '' for suite in suites: # Calls to groups of schemes for this stage group_calls = '' for group in suite.groups: # The and groups require special treatment, # since they can only be run in the respective stage (init/finalize) if (group.init and not ccpp_stage == 'init') or \ (group.finalize and not ccpp_stage == 'finalize'): continue if not group_calls: clause = 'if' else: clause = 'else if' argument_list_group = create_argument_list_wrapped_explicit(group.arguments[ccpp_stage]) group_calls += ''' {clause} (trim(group_name)=="{group_name}") then ierr = {suite_name}_{group_name}_{stage}_cap({arguments})'''.format(clause=clause, suite_name=group.suite, group_name=group.name, stage=CCPP_STAGES[ccpp_stage], arguments=argument_list_group) group_calls += ''' else write({ccpp_var_name}%errmsg, '(*(a))') 'Group ' // trim(group_name) // ' not found' ierr = 1 end if '''.format(ccpp_var_name=ccpp_var.local_name, group_name=group.name) # Call to entire suite for this stage # Create argument list for calling the full suite argument_list_suite = create_argument_list_wrapped_explicit(suite.arguments[ccpp_stage]) suite_call = ''' ierr = {suite_name}_{stage}_cap({arguments}) '''.format(suite_name=suite.name, stage=CCPP_STAGES[ccpp_stage], arguments=argument_list_suite) # Add call to all groups of this suite and to the entire suite if not suite_switch: clause = 'if' else: clause = 'else if' suite_switch += ''' {clause} (trim(suite_name)=="{suite_name}") then if (present(group_name)) then {group_calls} else {suite_call} end if '''.format(clause=clause, suite_name=suite.name, group_calls=group_calls, suite_call=suite_call) subroutine = CCPP_STATIC_SUBROUTINE_NAME.format(stage=ccpp_stage) self._subroutines.append(subroutine) subs += API.sub.format(subroutine=subroutine, ccpp_var_name=ccpp_var.local_name, suite_switch=suite_switch) # Write output to stdout or file if (self.filename is not sys.stdout): filepath = os.path.split(self.filename)[0] if filepath and not os.path.isdir(filepath): os.makedirs(filepath) # If the file exists, write to temporary file first and compare them: # - if identical, delete the temporary file and keep the existing one # and set the API update flag to false # - if different, replace existing file with temporary file and set # the API update flag to true (default value) # - always replace the file if any of the suite caps has changed # If the file does not exist, write the API an set the flag to true if os.path.isfile(self.filename) and \ not any([suite.update_cap for suite in suites]): write_to_test_file = True test_filename = self.filename + '.test' f = open(test_filename, 'w') else: write_to_test_file = False f = open(self.filename, 'w') else: f = sys.stdout f.write(API.header.format(module=self._module, module_use=module_use, subroutines=','.join(self._subroutines))) f.write(subs) f.write(Suite.footer.format(module=self._module)) if (f is not sys.stdout): f.close() # See comment above on updating the API or not if write_to_test_file: if filecmp.cmp(self.filename, test_filename): # Files are equal, delete the test API and set update flag to False os.remove(test_filename) self.update_api = False else: # Files are different, replace existing API with # the test API and set update flag to True # Python 3 only: os.replace(test_filename, self.filename) os.remove(self.filename) os.rename(test_filename, self.filename) self.update_api = True else: self.update_api = True return def write_includefile(self, source_filename, type): success = True filepath = os.path.split(source_filename)[0] if filepath and not os.path.isdir(filepath): os.makedirs(filepath) # If the file exists, write to temporary file first and compare them: # - if identical, delete the temporary file and keep the existing one # - if different, replace existing file with temporary file # - however, always replace the file if the API update flag is true if os.path.isfile(source_filename) and not self.update_api: write_to_test_file = True test_filename = source_filename + '.test' f = open(test_filename, 'w') else: write_to_test_file = False f = open(source_filename, 'w') if type == 'shell': # Contents of shell/source file contents = """# The CCPP static API is defined here. # # This file is auto-generated using ccpp_prebuild.py # at compile time, do not edit manually. # export CCPP_STATIC_API=\"{filename}\" """.format(filename=os.path.abspath(os.path.join(self.directory,self.filename))) elif type == 'cmake': # Contents of cmake include file contents = """# The CCPP static API is defined here. # # This file is auto-generated using ccpp_prebuild.py # at compile time, do not edit manually. # set(API \"{filename}\") """.format(filename=os.path.abspath(os.path.join(self.directory,self.filename))) else: logging.error('Encountered unknown type of file "{type}" when writing include file for static API'.format(type=type)) success = False return f.write(contents) f.close() # See comment above on updating the API or not if write_to_test_file: if filecmp.cmp(source_filename, test_filename): # Files are equal, delete the test file os.remove(test_filename) else: # Files are different, replace existing file # Python 3 only: os.replace(test_filename, source_filename) os.remove(source_filename) os.rename(test_filename, source_filename) return success class Suite(object): header=''' ! ! This work (Common Community Physics Package), 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 cap module for the CCPP suite !! ! module {module} {module_use} implicit none private public :: {subroutines} contains ''' sub = ''' function {subroutine}({arguments}) result(ierr) {module_use} implicit none integer :: ierr {var_defs} ierr = 0 {body} end function {subroutine} ''' footer = ''' end module {module} ''' def __init__(self, **kwargs): self._name = None self._filename = sys.stdout self._sdf_name = None self._all_schemes_called = None self._all_subroutines_called = None self._call_tree = {} self._caps = None self._module = None self._subroutines = None self._parents = { ccpp_stage : collections.OrderedDict() for ccpp_stage in CCPP_STAGES.keys() } self._arguments = { ccpp_stage : [] for ccpp_stage in CCPP_STAGES.keys() } self._update_cap = True for key, value in kwargs.items(): setattr(self, "_"+key, value) @property def name(self): '''Get the name of the suite.''' return self._name @property def sdf_name(self): '''Get the name of the suite definition file.''' return self._sdf_name @sdf_name.setter def sdf_name(self, value): self._sdf_name = value @property def filename(self): '''Get the filename of write the output to.''' return self._filename @filename.setter def filename(self, value): self._filename = value @property def update_cap(self): '''Get the update_cap flag.''' return self._update_cap @update_cap.setter def update_cap(self, value): self._update_cap = value def parse(self, make_call_tree=False): '''Parse the suite definition file.''' success = True if not os.path.exists(self._sdf_name): logging.critical("Suite definition file {0} not found.".format(self._sdf_name)) success = False return success tree = ET.parse(self._sdf_name) suite_xml = tree.getroot() self._name = suite_xml.get('name') # Validate name of suite in XML tag against filename; could be moved to common.py if not (os.path.basename(self._sdf_name) == 'suite_{}.xml'.format(self._name)): logging.critical("Invalid suite name {0} in suite definition file {1}.".format( self._name, self._sdf_name)) success = False return success # Check if suite name is too long if len(self._name) > SUITE_NAME_MAX_CHARS: logging.critical(f"Suite name {self._name} has more than the allowed {SUITE_NAME_MAX_CHARS} characters") success = False return success # Flattened lists of all schemes and subroutines in SDF self._all_schemes_called = [] self._all_subroutines_called = [] if make_call_tree: # Call tree of all schemes in SDF. call_tree is a dictionary, with keys corresponding to each group in a suite, and # the value associated with each key being an ordered list of the schemes in each group (with duplicates and subcycles) self._call_tree = {} # Build hierarchical structure as in SDF self._groups = [] for group_xml in suite_xml: subcycles = [] self._call_tree[group_xml.attrib['name']] = [] # Add suite-wide init scheme to group 'init', similar for finalize if group_xml.tag.lower() == 'init' or group_xml.tag.lower() == 'finalize': self._all_schemes_called.append(group_xml.text) self._all_subroutines_called.append(group_xml.text + '_' + group_xml.tag.lower()) schemes = [group_xml.text] subcycles.append(Subcycle(loop=1, schemes=schemes)) if group_xml.tag.lower() == 'init': self._groups.append(Group(name=group_xml.tag.lower(), subcycles=subcycles, suite=self._name, init=True)) elif group_xml.tag.lower() == 'finalize': self._groups.append(Group(name=group_xml.tag.lower(), subcycles=subcycles, suite=self._name, finalize=True)) continue # Parse subcycles of all regular groups for subcycle_xml in group_xml: schemes = [] for scheme_xml in subcycle_xml: self._all_schemes_called.append(scheme_xml.text) schemes.append(scheme_xml.text) loop=int(subcycle_xml.get('loop')) for ccpp_stage in CCPP_STAGES: self._all_subroutines_called.append(scheme_xml.text + '_' + CCPP_STAGES[ccpp_stage]) subcycles.append(Subcycle(loop=loop, schemes=schemes)) if make_call_tree: # Populate call tree from SDF's heirarchical structure, including multiple calls in subcycle loops for loop in range(0,int(subcycle_xml.get('loop'))): for scheme_xml in subcycle_xml: self._call_tree[group_xml.attrib['name']].append(scheme_xml.text) self._groups.append(Group(name=group_xml.get('name'), subcycles=subcycles, suite=self._name)) # Remove duplicates from list of all subroutines an schemes self._all_schemes_called = list(set(self._all_schemes_called)) self._all_subroutines_called = list(set(self._all_subroutines_called)) return success def print_debug(self): '''Basic debugging output about the suite.''' print("ALL SUBROUTINES:") print(self._all_subroutines_called) print("STRUCTURED:") print(self._groups) for group in self._groups: group.print_debug() @property def all_schemes_called(self): '''Get the list of all schemes.''' return self._all_schemes_called @property def call_tree(self): '''Get the call tree of the suite (all schemes, in order, with duplicates and loops).''' return self._call_tree @property def all_subroutines_called(self): '''Get the list of all subroutines.''' return self._all_subroutines_called @property def module(self): '''Get the list of the module generated for this suite.''' return self._module @property def subroutines(self): '''Get the list of all subroutines generated for this suite.''' return self._subroutines @property def caps(self): '''Get the list of all caps.''' return self._caps @property def groups(self): '''Get the list of groups in this suite.''' return self._groups @property def parents(self): '''Get the parent variables for the suite.''' return self._parents @parents.setter def parents(self, value): self._parents = value @property def arguments(self): '''Get the argument list for the suite.''' return self._arguments @arguments.setter def arguments(self, value): self._arguments = value def write(self, metadata_request, metadata_define, arguments, debug): """Create caps for all groups in the suite and for the entire suite (calling the group caps one after another). Add additional code for debugging if debug flag is True.""" # Set name of module and filename of cap self._module = 'ccpp_{suite_name}_cap'.format(suite_name=self._name) self.filename = '{module_name}.F90'.format(module_name=self._module) # Init self._subroutines = [] # Write group caps and generate module use statements; combine the argument lists # and variable definitions for all groups into a suite argument list. This may # require adjusting the intent of the variables. module_use = '' for group in self._groups: group.write(metadata_request, metadata_define, arguments, debug) for subroutine in group.subroutines: module_use += ' use {m}, only: {s}\n'.format(m=group.module, s=subroutine) for ccpp_stage in CCPP_STAGES.keys(): for parent_standard_name in group.parents[ccpp_stage].keys(): if parent_standard_name in self.parents[ccpp_stage]: if self.parents[ccpp_stage][parent_standard_name].intent == 'in' and \ not group.parents[ccpp_stage][parent_standard_name].intent == 'in': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' elif self.parents[ccpp_stage][parent_standard_name].intent == 'out' and \ not group.parents[ccpp_stage][parent_standard_name].intent == 'out': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' else: self.parents[ccpp_stage][parent_standard_name] = copy.deepcopy(group.parents[ccpp_stage][parent_standard_name]) subs = '' for ccpp_stage in CCPP_STAGES.keys(): # Create a wrapped argument list for calling the suite, # get module use statements and variable definitions (self.arguments[ccpp_stage], sub_module_use, sub_var_defs) = \ create_arguments_module_use_var_defs(self.parents[ccpp_stage], metadata_define) argument_list_suite = create_argument_list_wrapped(self.arguments[ccpp_stage]) body = '' for group in self._groups: # Groups 'init'/'finalize' are only run in stages 'init'/'finalize' if (group.init and not ccpp_stage == 'init') or \ (group.finalize and not ccpp_stage == 'finalize'): continue # Create a wrapped argument list for calling the group (arguments_group, dummy, dummy) = create_arguments_module_use_var_defs(group.parents[ccpp_stage], metadata_define) argument_list_group = create_argument_list_wrapped_explicit(arguments_group) # Write to body that calls the groups for this stage body += ''' ierr = {suite_name}_{group_name}_{stage}_cap({arguments}) if (ierr/=0) return '''.format(suite_name=self._name, group_name=group.name, stage=CCPP_STAGES[ccpp_stage], arguments=argument_list_group) # Add name of subroutine in the suite cap to list of subroutine names subroutine = '{name}_{stage}_cap'.format(name=self._name, stage=CCPP_STAGES[ccpp_stage]) self._subroutines.append(subroutine) # Add subroutine to output subs += Suite.sub.format(subroutine=subroutine, arguments=argument_list_suite, module_use='\n '.join(sub_module_use), var_defs='\n '.join(sub_var_defs), body=body) # Write cap to stdout or file if (self.filename is not sys.stdout): filepath = os.path.split(self.filename)[0] if filepath and not os.path.isdir(filepath): os.makedirs(filepath) # If the file exists, write to temporary file first and compare them: # - if identical, delete the temporary file and keep the existing one # and set the suite cap update flag to false # - if different, replace existing file with temporary file and set # the suite cap update flag to true (default value) # - however, if any of the group caps has changed, rewrite the suite # cap as well and set the suite cap update flag to true # If the file does not exist, write the cap an set the flag to true if os.path.isfile(self.filename) and \ not any([group.update_cap for group in self._groups]): write_to_test_file = True test_filename = self.filename + '.test' f = open(test_filename, 'w') else: write_to_test_file = False f = open(self.filename, 'w') else: f = sys.stdout f.write(Suite.header.format(module=self._module, module_use=module_use, subroutines=', &\n '.join(self._subroutines))) f.write(subs) f.write(Suite.footer.format(module=self._module)) if (f is not sys.stdout): f.close() # See comment above on updating the suite cap or not if write_to_test_file: if filecmp.cmp(self.filename, test_filename): # Files are equal, delete the test cap # and set update flag to False os.remove(test_filename) self.update_cap = False else: # Files are different, replace existing cap # with test cap and set flag to True # Python 3 only: os.replace(test_filename, self.filename) os.remove(self.filename) os.rename(test_filename, self.filename) self.update_cap = True else: self.update_cap = True # Create list of all caps generated (for groups and suite) self._caps = [ self.filename ] for group in self._groups: self._caps.append(group.filename) ############################################################################### class Group(object): header=''' ! ! This work (Common Community Physics Package), 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 cap module for the CCPP {group} group !! ! module {module} {module_use} implicit none private public :: {subroutines} logical, save :: initialized = .false. contains ''' sub = ''' function {subroutine}({argument_list}) result(ierr) {module_use} implicit none ! Error handling integer :: ierr {var_defs} ierr = 0 {initialized_test_block} {body} {initialized_set_block} end function {subroutine} ''' footer = ''' end module {module} ''' initialized_test_blocks = { 'init' : ''' if (initialized) return ''', 'timestep_init' : ''' if (.not.initialized) then write({target_name_msg},'(*(a))') '{name}_timestep_init called before {name}_init' {target_name_flag} = 1 return end if ''', 'run' : ''' if (.not.initialized) then write({target_name_msg},'(*(a))') '{name}_run called before {name}_init' {target_name_flag} = 1 return end if ''', 'timestep_finalize' : ''' if (.not.initialized) then write({target_name_msg},'(*(a))') '{name}_timestep_finalize called before {name}_init' {target_name_flag} = 1 return end if ''', 'finalize' : ''' if (.not.initialized) return ''', } initialized_set_blocks = { 'init' : ''' initialized = .true. ''', 'timestep_init' : '', 'run' : '', 'timestep_finalize' : '', 'finalize' : ''' initialized = .false. ''', } def __init__(self, **kwargs): self._name = '' self._suite = None self._filename = sys.stdout self._init = False self._finalize = False self._module = None self._subroutines = None self._pset = None self._parents = { ccpp_stage : collections.OrderedDict() for ccpp_stage in CCPP_STAGES } self._arguments = { ccpp_stage : [] for ccpp_stage in CCPP_STAGES } self._update_cap = True for key, value in kwargs.items(): setattr(self, "_"+key, value) def write(self, metadata_request, metadata_define, arguments, debug): """Create caps for all stages of this group. Add additional code for debugging if debug flag is True.""" # Create an inverse lookup table of local variable names defined (by the host model) and standard names standard_name_by_local_name_define = collections.OrderedDict() for standard_name in metadata_define.keys(): standard_name_by_local_name_define[metadata_define[standard_name][0].local_name] = standard_name # First get target names of standard CCPP variables for subcycling and error handling ccpp_loop_counter_target_name = metadata_request[CCPP_LOOP_COUNTER][0].target ccpp_loop_extent_target_name = metadata_request[CCPP_LOOP_EXTENT][0].target ccpp_error_code_target_name = metadata_request[CCPP_ERROR_CODE_VARIABLE][0].target ccpp_error_msg_target_name = metadata_request[CCPP_ERROR_MSG_VARIABLE][0].target # module_use = '' self._module = 'ccpp_{suite}_{name}_cap'.format(name=self._name, suite=self._suite) self._filename = '{module_name}.F90'.format(module_name=self._module) self._subroutines = [] local_subs = '' # for ccpp_stage in CCPP_STAGES.keys(): # The special init and finalize routines are only run in that stage if self._init and not ccpp_stage == 'init': continue elif self._finalize and not ccpp_stage == 'finalize': continue # For mapping local variable names to standard names local_vars = collections.OrderedDict() # For mapping temporary variable names (for unit conversions, etc) to local variable names tmpvar_cnt = 0 tmpvars = collections.OrderedDict() # body = '' # Variable definitions automatically added for subroutines var_defs = '' # List of manual variable definitions, for example for handling blocked data structures var_defs_manual = [] # Conditionals for variables (used or allocated only under certain conditions) conditionals = {} # for subcycle in self._subcycles: subcycle_body = '' # Call all schemes for scheme_name in subcycle.schemes: # actions_before and actions_after capture operations such # as unit conversions, transformations that have to happen # before and/or after the call to the subroutine (scheme) actions_before = '' actions_after = '' # module_name = scheme_name subroutine_name = scheme_name + '_' + ccpp_stage container = encode_container(module_name, scheme_name, subroutine_name) # Skip entirely empty routines or non-existent routines if not subroutine_name in arguments[scheme_name].keys() or not arguments[scheme_name][subroutine_name]: continue error_check = '' args = '' length = 0 # First identify all dimensions needed to handle the arguments # and add them to the list of required variables for the cap additional_variables_required = [] # for var_standard_name in arguments[scheme_name][subroutine_name]: if not var_standard_name in metadata_define.keys(): raise Exception('Variable {standard_name} not defined in host model metadata'.format( standard_name=var_standard_name)) var = metadata_define[var_standard_name][0] # dim_expression can be 'A', '1', '1:A', ... for dim_expression in var.dimensions: dims = dim_expression.split(':') for dim in dims: dim = dim.lower() try: dim = int(dim) except ValueError: if not dim in local_vars.keys() and \ not dim in additional_variables_required + arguments[scheme_name][subroutine_name]: if not dim in metadata_define.keys(): raise Exception('Dimension {}, required by variable {}, not defined in host model metadata'.format( dim, var_standard_name)) logging.debug("Adding dimension {} for variable {}".format(dim, var_standard_name)) additional_variables_required.append(dim) # If blocked data structures need to be converted, add necessary variables if ccpp_stage in ['init', 'timestep_init', 'timestep_finalize', 'finalize'] and CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER] in var.local_name: if not CCPP_BLOCK_COUNT in local_vars.keys() \ and not CCPP_BLOCK_COUNT in additional_variables_required + arguments[scheme_name][subroutine_name]: logging.debug("Adding variable {} for handling blocked data structures".format(CCPP_BLOCK_COUNT)) additional_variables_required.append(CCPP_BLOCK_COUNT) if not CCPP_HORIZONTAL_LOOP_EXTENT in local_vars.keys() \ and not CCPP_HORIZONTAL_LOOP_EXTENT in additional_variables_required + arguments[scheme_name][subroutine_name]: logging.debug("Adding variable {} for handling blocked data structures".format(CCPP_HORIZONTAL_LOOP_EXTENT)) additional_variables_required.append(CCPP_HORIZONTAL_LOOP_EXTENT) if not CCPP_HORIZONTAL_DIMENSION in local_vars.keys() \ and not CCPP_HORIZONTAL_DIMENSION in additional_variables_required + arguments[scheme_name][subroutine_name]: logging.debug("Adding variable {} for handling blocked data structures".format(CCPP_HORIZONTAL_DIMENSION)) additional_variables_required.append(CCPP_HORIZONTAL_DIMENSION) # If the variable is only active/used under certain conditions, add necessary variables # also record the conditional for later use in unit conversions / blocked data conversions. if var.active == 'T': conditional = '.true.' elif var.active == 'F': conditional = '.false.' else: # Convert conditional expression in standard_name format to local names known to the host model conditional = '' # Find all words in the conditional, for each of them look for a matching # standard name in the list of known variables items = FORTRAN_CONDITIONAL_REGEX.findall(var.active) for item in items: item = item.lower() if item in FORTRAN_CONDITIONAL_REGEX_WORDS: conditional += item else: # Detect integers, following Python's "easier to ask forgiveness than permission" mentality try: int(item) conditional += item except ValueError: if not item in metadata_define.keys(): raise Exception("Variable {} used in conditional for {} not known to host model".format( item, var_standard_name)) var2 = metadata_define[item][0] conditional += var2.local_name # Add to list of required variables for the cap if not item in local_vars.keys() \ and not item in additional_variables_required + arguments[scheme_name][subroutine_name]: logging.debug("Adding variable {} for handling conditionals".format(item)) additional_variables_required.append(item) # Conditionals are identical per requirement, no need to test for consistency again if not var_standard_name in conditionals.keys(): conditionals[var_standard_name] = conditional # Extract all variables needed (including indices for components/slices of arrays and # including their parents). We need to run this twice, because the dimensions of parent # variables get added to additional_variables_required in the first pass. iteration = 1 while iteration <= 2: for var_standard_name in additional_variables_required + arguments[scheme_name][subroutine_name]: # Pick the correct variable for this module/scheme/subroutine # from the list of requested variables, if it is in that list if var_standard_name in arguments[scheme_name][subroutine_name]: for var in metadata_request[var_standard_name]: if container == var.container: break # This is a dimension or required variable added automatically (e.g. for handling blocked data) else: # Create a copy of the variable in the metadata dictionary # of host model variables and set necessary default values var = copy.deepcopy(metadata_define[var_standard_name][0]) var.intent = 'in' if not var_standard_name in local_vars.keys(): # The full name of the variable as known to the host model var_local_name_define = metadata_define[var_standard_name][0].local_name # Break apart var_local_name_define into the different components (members of DDTs) # to determine all variables that are required (parent_local_name_define, parent_local_names_define_indices) = \ extract_parents_and_indices_from_local_name(var_local_name_define) parent_standard_name = None parent_var = None # Check for each of the derived parent local names as defined by the host model # if they are registered (i.e. if there is a standard name for it). Note that # the output of extract_parents_and_indices_from_local_name is stripped of any # array subset information, i.e. a local name 'Atm(:)%...' will produce a # parent local name 'Atm'. Since the rank of the parent variable is not known # at this point and since the local name in the host model metadata table could # contain '(:)', '(:,:)', ... (up to the rank of the array), we search for the # maximum number of dimensions allowed by the Fortran standard. for local_name_define in [parent_local_name_define] + parent_local_names_define_indices: parent_standard_name = None parent_var = None for i in range(FORTRAN_ARRAY_MAX_DIMS+1): if i==0: dims_string = '' else: # (:) for i==1, (:,:) for i==2, ... dims_string = '(' + ','.join([':' for j in range(i)]) + ')' if local_name_define+dims_string in standard_name_by_local_name_define.keys(): parent_standard_name = standard_name_by_local_name_define[local_name_define+dims_string] parent_var = metadata_define[parent_standard_name][0] break if not parent_var: raise Exception('Parent variable {parent} of {child} with standard name '.format( parent=local_name_define, child=var_local_name_define)+\ '{standard_name} not defined in host model metadata'.format( standard_name=var_standard_name)) # Reset local name for entire array to a notation without (:), (:,:), etc.; # this is needed for the var.print_def_intent() routine to work correctly parent_var.local_name = local_name_define # Add the parent_var's dimensions to the locally defined dimensions: # dim_expression can be 'A', '1', '1:A', ... for dim_expression in parent_var.dimensions: dims = dim_expression.split(':') for dim in dims: dim = dim.lower() try: dim = int(dim) except ValueError: if not dim in local_vars.keys() and \ not dim in additional_variables_required + arguments[scheme_name][subroutine_name]: if not dim in metadata_define.keys(): raise Exception('Dimension {}, required by parent variable {}, not defined in host model metadata'.format( dim, parent_standard_name)) logging.debug("Adding dimension {} for parent variable {}".format(dim, parent_standard_name)) additional_variables_required.append(dim) # Add variable to dictionary of parent variables, if not already there. # Set or update intent, depending on whether the variable is an index # in var_local_name_define or the actual parent of that variable. if not parent_standard_name in self.parents[ccpp_stage].keys(): self.parents[ccpp_stage][parent_standard_name] = copy.deepcopy(parent_var) # Copy the intent of the actual variable being processed if local_name_define == parent_local_name_define: self.parents[ccpp_stage][parent_standard_name].intent = var.intent # It's an index for the actual variable being processed --> intent(in) else: self.parents[ccpp_stage][parent_standard_name].intent = 'in' elif self.parents[ccpp_stage][parent_standard_name].intent == 'in': # Adjust the intent if the actual variable is not intent(in) if local_name_define == parent_local_name_define and not var.intent == 'in': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' # It's an index for the actual variable being processed, intent is ok #else: # # nothing to do elif self.parents[ccpp_stage][parent_standard_name].intent == 'out': # Adjust the intent if the actual variable is not intent(out) if local_name_define == parent_local_name_define and not var.intent == 'out': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' # Adjust the intent, because the variable is also used as index variable else: self.parents[ccpp_stage][parent_standard_name].intent = 'inout' # Record the parent information for this variable (with standard name var_standard_name) if local_name_define == parent_local_name_define: local_vars[var_standard_name] = { 'name' : metadata_define[var_standard_name][0].local_name, 'kind' : metadata_define[var_standard_name][0].kind, 'parent_standard_name' : parent_standard_name } # Reset parent to actual parent of the variable with standard name var_standard_name if local_vars[var_standard_name]['parent_standard_name']: parent_standard_name = local_vars[var_standard_name]['parent_standard_name'] parent_var = metadata_define[parent_standard_name][0] elif local_vars[var_standard_name]['parent_standard_name']: parent_standard_name = local_vars[var_standard_name]['parent_standard_name'] parent_var = metadata_define[parent_standard_name][0] # Update intent information if necessary if self.parents[ccpp_stage][parent_standard_name].intent == 'in' and not var.intent == 'in': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' elif self.parents[ccpp_stage][parent_standard_name].intent == 'out' and not var.intent == 'out': self.parents[ccpp_stage][parent_standard_name].intent = 'inout' # End of iteration (while) loop, increase iteration counter iteration += 1 # Loop over actual arguments for this subroutine and create the argument list. # This is not required for the additional dimensions and variables. for var_standard_name in arguments[scheme_name][subroutine_name]: # Pick the correct variable for this module/scheme/subroutine # from the list of requested variables for var in metadata_request[var_standard_name]: if container == var.container: break # To assist debugging efforts, check if arrays have the correct size (ignore scalars for now) assign_test = '' if debug: 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: # We don't need extra tests for blocked arrays, because the de-blocking logic below # 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: array_size = [] for dim in var.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]) dim0 = dims[0] except ValueError: if not dims[0].lower() in metadata_define.keys(): raise Exception('Dimension {}, required by variable {}, not defined in host model metadata'.format( dims[0].lower(), var_standard_name)) dim0 = metadata_define[dims[0].lower()][0].local_name try: dim1 = int(dims[1]) dim1 = dims[1] except ValueError: if not dims[1].lower() in metadata_define.keys(): raise Exception('Dimension {}, required by variable {}, not defined in host model metadata'.format( dims[1].lower(), var_standard_name)) dim1 = metadata_define[dims[1].lower()][0].local_name # Single dimensions else: dim0 = 1 try: dim1 = int(dim) dim1 = dim except ValueError: if not dim.lower() in metadata_define.keys(): raise Exception('Dimension {}, required by variable {}, not defined in host model metadata'.format( dim.lower(), var_standard_name)) dim1 = metadata_define[dim.lower()][0].local_name array_size.append('({last}-{first}+1)'.format(last=dim1, first=dim0)) var_size_expected = '({})'.format('*'.join(array_size)) assign_test = ''' ! Check if variable {var_name} is associated/allocated and has the correct size if (size({var_name})/={var_size_expected}) then write({ccpp_errmsg}, '(2(a,i8))') 'Detected size mismatch for variable {var_name} in group {group_name} before {subroutine_name}, expected ', & {var_size_expected}, ' but got ', size({var_name}) ierr = 1 return end if '''.format(var_name=local_vars[var_standard_name]['name'], 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 # 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 '' # 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 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)) 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)) # Padding of additional dimensions - before and after the horizontal dimension hdim_index = tmpvar.dimensions.index('{}:{}'.format(CCPP_CONSTANT_ONE,CCPP_HORIZONTAL_DIMENSION)) dimpad_before = '' + ':,'*(len(tmpvar.dimensions[:hdim_index])) dimpad_after = '' + ',:'*(len(tmpvar.dimensions[hdim_index+1:])) # Add necessary local variables for looping over blocks var_defs_manual.append('integer :: ib, nb') # Define actions before. Always copy data in, independent of intent. actions_in = ''' ! Allocate local variable to copy blocked data {var} into a contiguous array allocate({tmpvar}({dims})) ib = 1 do nb=1,{block_count} {tmpvar}({dimpad_before}ib:ib+{block_size}-1{dimpad_after}) = {var} ib = ib+{block_size} end do '''.format(tmpvar=tmpvar.local_name, block_count=metadata_define[CCPP_BLOCK_COUNT][0].local_name.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), block_size=metadata_define[CCPP_HORIZONTAL_LOOP_EXTENT][0].local_name.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), var=tmpvar.target.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), dims=','.join(alloc_dimensions), dimpad_before=dimpad_before, dimpad_after=dimpad_after, ) # Define actions after, depending on intent. if var.intent in [ 'inout', 'out' ]: actions_out = ''' ib = 1 do nb=1,{block_count} {var} = {tmpvar}({dimpad_before}ib:ib+{block_size}-1{dimpad_after}) ib = ib+{block_size} end do deallocate({tmpvar}) '''.format(tmpvar=tmpvar.local_name, block_count=metadata_define[CCPP_BLOCK_COUNT][0].local_name.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), block_size=metadata_define[CCPP_HORIZONTAL_LOOP_EXTENT][0].local_name.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), var=tmpvar.target.replace(CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER],'nb'), dimpad_before=dimpad_before, dimpad_after=dimpad_after, ) else: actions_out = ''' deallocate({tmpvar}) '''.format(tmpvar=tmpvar.local_name) # Set/update actions for this temporary variable tmpvar.actions = {'in' : actions_in, 'out' : actions_out} tmpvars[local_vars[var_standard_name]['name']] = tmpvar # Add unit conversions, if necessary if var.actions['in']: # Add unit conversion before entering the subroutine, after allocating the temporary # array holding the non-blocked data and copying the blocked data to it actions_in = actions_in + \ ' {t} = {c}\n'.format(t=tmpvar.local_name, c=var.actions['in'].format(var=tmpvar.local_name, kind=kind_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 actions_out = ' {t} = {c}\n'.format(t=tmpvar.local_name, c=var.actions['out'].format(var=tmpvar.local_name, kind=kind_string)) + \ actions_out # 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')) # 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')) # Add to argument list arg = '{local_name}={var_name},'.format(local_name=var.local_name, var_name=tmpvar.local_name) # 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 \ CCPP_INTERNAL_VARIABLES[CCPP_BLOCK_NUMBER] in local_vars[var_standard_name]['name']: raise Exception("Variables stored in blocked data structures but without horizontal dimension not supported in phases ' + \ 'init, timestep_init, timestep_finalize, finalize at this time: {} in {}".format(var_standard_name, subroutine_name)) # Limitations for UFS: Variables stored in threaded data structures (i.e. only for one block at a time) in GFS_interstitial DDT # are not supported at this time (doesn't make sense anyway) elif ccpp_stage in ['init', 'timestep_init', 'timestep_finalize', 'finalize'] and \ CCPP_INTERNAL_VARIABLES[CCPP_THREAD_NUMBER] in local_vars[var_standard_name]['name']: raise Exception("Variables stored in thread-specific data structures (GFS_interstitial DDT) are not supported in phases ' + \ 'init, timestep_init, timestep_finalize, finalize at this time: {} in {}".format(var_standard_name, subroutine_name)) # Unit conversions without converting blocked data structures elif var.actions['in'] or var.actions['out']: # If requested, check that arrays are allocated/associated and have the correct size if debug: actions_in = assign_test else: actions_in = '' actions_out = '' 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']] else: # Add a local variable (tmpvar) for this variable tmpvar_cnt += 1 tmpvar = copy.deepcopy(var) 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 # According to https://fortran-lang.discourse.group/t/allocated-array-bounds-with-mold-and-source-on-expressions/3032, # allocating with 'source=...' sets the correct lower and upper bounds for the newly created array actions_in += ' allocate({t}, source={v})\n'.format(t=tmpvar.local_name, v=tmpvar.target) if var.actions['in']: # Add unit conversion before entering the subroutine actions_in += ' {t} = {c}\n'.format(t=tmpvar.local_name, c=var.actions['in'].format(var=tmpvar.target, kind=kind_string)) if var.actions['out']: # Add unit conversion after returning from the subroutine actions_out += ' {v} = {c}\n'.format(v=tmpvar.target, c=var.actions['out'].format(var=tmpvar.local_name, kind=kind_string)) if tmpvar.rank: # Add deallocate statement if the variable has a rank > 0 actions_out += ' deallocate({t})\n'.format(t=tmpvar.local_name) # 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')) # 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')) # Add to argument list arg = '{local_name}={var_name},'.format(local_name=var.local_name, var_name=tmpvar.local_name) # 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 # 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')) # Add to argument list arg = '{local_name}={var_name},'.format(local_name=var.local_name, var_name=local_vars[var_standard_name]['name']) else: arg = '' args += arg length += len(arg) # Split args so that lines don't get too long if length > 70 and not var_standard_name == arguments[scheme_name][subroutine_name][-1]: args += ' &\n ' length = 0 args = args.rstrip(',') subroutine_call = ''' {actions_before} call {subroutine_name}({args}) {actions_after} '''.format(subroutine_name=subroutine_name, args=args, actions_before=actions_before.rstrip('\n'), actions_after=actions_after.rstrip('\n')) error_check = '''if ({target_name_flag}/=0) then {target_name_msg} = "An error occured in {subroutine_name}: " // trim({target_name_msg}) ierr={target_name_flag} return end if '''.format(target_name_flag=ccpp_error_code_target_name, target_name_msg=ccpp_error_msg_target_name, subroutine_name=subroutine_name) subcycle_body += ''' {subroutine_call} {error_check} '''.format(subroutine_call=subroutine_call, error_check=error_check) module_use += ' use {m}, only: {s}\n'.format(m=module_name, s=subroutine_name) # If this subcycle calls any schemes, i.e. has any variables registered # that need to be passed to the group for this stage, then handle the # subcycle loops by prepending/appending the necessary code to subcycle_body subcycle_body_prefix = ''' ! Start of next subcycle ''' subcycle_body_suffix = '' if self.parents[ccpp_stage]: # Set subcycle loop extent if ccpp_stage == 'run': subcycle_body_prefix += ''' ! Set loop extent variable for the following subcycle {loop_extent_var_name} = {loop_cnt_max} '''.format(loop_extent_var_name=ccpp_loop_extent_target_name, loop_cnt_max=subcycle.loop) else: subcycle_body_prefix += ''' ! Set loop extent variable for the following subcycle {loop_extent_var_name} = 1 '''.format(loop_extent_var_name=ccpp_loop_extent_target_name) # Create subcycle (Fortran do loop) if needed if subcycle.loop > 1 and ccpp_stage == 'run': subcycle_body_prefix += ''' associate(cnt => {loop_var_name}) do cnt=1,{loop_cnt_max}\n\n'''.format(loop_var_name=ccpp_loop_counter_target_name, loop_cnt_max=subcycle.loop) subcycle_body_suffix += ''' end do end associate ''' else: subcycle_body_prefix += ''' {loop_var_name} = 1\n'''.format(loop_var_name=ccpp_loop_counter_target_name) # Add this subcycle's Fortran body to the group body if subcycle_body: body += subcycle_body_prefix + subcycle_body + subcycle_body_suffix # 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()) sub_argument_list = create_argument_list_wrapped(self.arguments[ccpp_stage]) # Remove duplicates from additional manual variable definitions var_defs_manual = list(set(var_defs_manual)) # Write cap - shorten certain ccpp_stages to stay under the 63 character limit for Fortran function names subroutine = self._suite + '_' + self._name + '_' + CCPP_STAGES[ccpp_stage] + '_cap' self._subroutines.append(subroutine) # Test and set blocks for initialization status - check that at least # the mandatory CCPP error handling arguments are present (i.e. there is # at least one subroutine that gets called from this group), or skip. if self.arguments[ccpp_stage]: initialized_test_block = Group.initialized_test_blocks[ccpp_stage].format( target_name_flag=ccpp_error_code_target_name, target_name_msg=ccpp_error_msg_target_name, name=self._name) else: initialized_test_block = '' initialized_set_block = Group.initialized_set_blocks[ccpp_stage].format( target_name_flag=ccpp_error_code_target_name, target_name_msg=ccpp_error_msg_target_name, name=self._name) # Create subroutine local_subs += Group.sub.format(subroutine=subroutine, argument_list=sub_argument_list, module_use='\n '.join(sub_module_use), initialized_test_block=initialized_test_block, initialized_set_block=initialized_set_block, var_defs='\n '.join(sub_var_defs + var_defs_manual), body=body) # Write output to stdout or file if (self.filename is not sys.stdout): filepath = os.path.split(self.filename)[0] if filepath and not os.path.isdir(filepath): os.makedirs(filepath) # If the file exists, write to temporary file first and compare them: # - if identical, delete the temporary file and keep the existing one # and set the group cap update flag to false # - if different, replace existing file with temporary file and set # the group cap update flag to true (default value) # If the file does not exist, write the cap an set the flag to true if os.path.isfile(self.filename): write_to_test_file = True test_filename = self.filename + '.test' f = open(test_filename, 'w') else: write_to_test_file = False f = open(self.filename, 'w') else: f = sys.stdout f.write(Group.header.format(group=self._name, module=self._module, module_use=module_use, subroutines=', &\n '.join(self._subroutines))) f.write(local_subs) f.write(Group.footer.format(module=self._module)) if (f is not sys.stdout): f.close() # See comment above on updating the group cap or not if write_to_test_file: if filecmp.cmp(self.filename, test_filename): # Files are equal, delete the test cap # and set update flag to False os.remove(test_filename) self.update_cap = False else: # Files are different, replace existing cap # with test cap and set flag to True # Python 3 only: os.replace(test_filename, self.filename) os.remove(self.filename) os.rename(test_filename, self.filename) self.update_cap = True else: self.update_cap = True return @property def name(self): '''Get the name of the group.''' return self._name @name.setter def name(self, value): self._name = value @property def filename(self): '''Get the filename of write the output to.''' return self._filename @filename.setter def filename(self, value): self._filename = value @property def update_cap(self): '''Get the update_cap flag.''' return self._update_cap @update_cap.setter def update_cap(self, value): self._update_cap = value @property def init(self): '''Get the init flag.''' return self._init @init.setter def init(self, value): if not type(value) == types.BooleanType: raise Exception("Invalid type {0} of argument value, boolean expected".format(type(value))) self._init = value @property def finalize(self): '''Get the finalize flag.''' return self._finalize @finalize.setter def finalize(self, value): if not type(value) == types.BooleanType: raise Exception("Invalid type {0} of argument value, boolean expected".format(type(value))) self._finalize = value @property def suite(self): '''Get the suite name.''' return self._suite @property def module(self): '''Get the module name.''' return self._module @property def subcycles(self): '''Get the subcycles.''' return self._subcycles @property def subroutines(self): '''Get the subroutine names.''' return self._subroutines def print_debug(self): '''Basic debugging output about the group.''' print(self._name) for subcycle in self._subcycles: subcycle.print_debug() @property def pset(self): '''Get the unique physics set of this group.''' return self._pset @pset.setter def pset(self, value): self._pset = value @property def parents(self): '''Get the parent variables for the group.''' return self._parents @parents.setter def parents(self, value): self._parents = value @property def arguments(self): '''Get the argument list of the group.''' return self._arguments @arguments.setter def arguments(self, value): self._arguments = value class Subcycle(object): def __init__(self, **kwargs): self._filename = 'sys.stdout' self._schemes = None for key, value in kwargs.items(): setattr(self, "_"+key, value) @property def loop(self): '''Get the list of loop.''' return self._loop @loop.setter def loop(self, value): if not type(value) is int: raise Exception("Invalid type {0} of argument value, integer expected".format(type(value))) self._loop = value @property def schemes(self): '''Get the list of schemes.''' return self._schemes @schemes.setter def schemes(self, value): if not type(value) is list: raise Exception("Invalid type {0} of argument value, list expected".format(type(value))) self._schemes = value def print_debug(self): '''Basic debugging output about the subcycle.''' print(self._loop) for scheme in self._schemes: print(scheme) ############################################################################### if __name__ == "__main__": main()