Skip to content

Commit

Permalink
Add CCPP register phase (#582)
Browse files Browse the repository at this point in the history
## Overview
This PR adds a new phase, *register*, that can be called by a host model
and used by schemes to perform any set up that needs to happen BEFORE
the grid is established.

NOTE: this PR also *removes* the old `dynamic_constituent_routine`
metadata implementation for runtime constituents.

## Description
I have implemented it as an "optional" phase, by which I mean that it is
not required that a host model call this phase (though I'm happy to be
overruled!). As a result, the register phase does not change the CCPP
"state" (but will produce an error if it is called after the `init`
phase).

More:
### Dynamic/run-time constituent handling:
- If a scheme has run-time constituents, those shall be allocated,
instantiated, and returned from the scheme's register phase. This
metadata is required (the framework determines that there are runtime
constituents from a scheme if there is a `ccpp_constituent_properties_t`
variable required):
```
[ <unique dynamic constituent local name> ]
  standard_name = <some unique standard name>
  dimensions = (:)
  type = ccpp_constituent_properties_t
  intent = out
  allocatable = true
```
- The standard name doesn't really matter but MUST be different from
other runtime constituent standard names in the scheme; it may be
easiest to standardize this to something like
`dynamic_constituents_for_<scheme>`
- The framework will then compile all scheme constituents into
module-level variables in the host cap called
`<suite>_dynamic_constituents`, which are then used to pack and
initialize the module level constituents object
`<host>_constituents_obj`.
- If there are no dynamic constituents registered by any schemes within
a suite, that suite's dynamic constituents array is allocated to 0.

*Generated host cap code examples*
1. Multiple schemes have dynamic constituents:
```
subroutine test_host_ccpp_physics_register(suite_name, errmsg, errflg)

      use ccpp_cld_suite_cap, only: cld_suite_register

      character(len=*)                         :: suite_name
      character(len=512)                       :: errmsg
      integer                                  :: errflg
      type(ccpp_constituent_properties_t),allocatable          :: dyn_const(:)
      type(ccpp_constituent_properties_t),allocatable          :: dyn_const_ice(:)
      integer                                  :: num_dyn_consts
      integer                                  :: const_index

      errflg = 0
      errmsg = ""
      if (trim(suite_name) == 'cld_suite') then
         call cld_suite_register(errflg=errflg, errmsg=errmsg, dyn_const=dyn_const,               &
              dyn_const_ice=dyn_const_ice)
         allocate(cld_suite_dynamic_constituents(0+size(dyn_const)+size(dyn_const_ice)))
         ! Pack the suite-level dynamic, run-time constituents array
         num_dyn_consts = 0
         do const_index = 1, size(dyn_const)
            cld_suite_dynamic_constituents(num_dyn_consts + const_index) = dyn_const(const_index)
         end do
         num_dyn_consts = num_dyn_consts + size(dyn_const)
         deallocate(dyn_const)
         do const_index = 1, size(cld_suite_dynamic_constituents)
            call cld_suite_dynamic_constituents(const_index)%standard_name(stdname,               &
                 errcode=errflg, errmsg=errmsg)
         end do
         do const_index = 1, size(dyn_const_ice)
            cld_suite_dynamic_constituents(num_dyn_consts + const_index) =                        &
                 dyn_const_ice(const_index)
         end do
         num_dyn_consts = num_dyn_consts + size(dyn_const_ice)
         deallocate(dyn_const_ice)
      else
         write(errmsg, '(3a)')"No suite named ", trim(suite_name), "found"
         errflg = 1
      end if

   end subroutine test_host_ccpp_physics_register
```
2.  No schemes have dynamic constituents:
```
subroutine test_host_ccpp_physics_register(suite_name, errmsg, errflg)

      use ccpp_ddt_suite_cap,  only: ddt_suite_register
      use ccpp_temp_suite_cap, only: temp_suite_register

      character(len=*)                         :: suite_name
      character(len=512)                       :: errmsg
      integer                                  :: errflg

      errflg = 0
      errmsg = ""
      if (trim(suite_name) == 'ddt_suite') then
         call ddt_suite_register(errflg=errflg, errmsg=errmsg)
         ! Suite does not return dynamic constituents; allocate to zero
         allocate(ddt_suite_dynamic_constituents(0))
      else if (trim(suite_name) == 'temp_suite') then
         call temp_suite_register(errflg=errflg, errmsg=errmsg, config_var=config_var)
         ! Suite does not return dynamic constituents; allocate to zero
         allocate(temp_suite_dynamic_constituents(0))
      else
         write(errmsg, '(3a)')"No suite named ", trim(suite_name), "found"
         errflg = 1
      end if

   end subroutine test_host_ccpp_physics_register
```

### Misc notes
Since this phase is called before the grid is initialized, variables are
not allocated at this time (that still happens in `init`) and no
variables with horizontal and vertical dimensions can be passed in.

## UI Changes
User interface changes?: Yes, but they're optional
If a host model wishes to utilize schemes' register phases, they must
add a call to `<host_model>_ccpp_physics_register(suite_name, errmsg,
errflg)`

## Testing
test removed: removed unit tests for dyn_const_routines (old
implementation of runtime constituent handling) - all pass
unit tests: Removed old dynamic constituents testing - all pass
system tests: Updated capgen and advection tests to include register
phases (with and without dynamic constituents)
- Also updated advection test CMakeLists to first run a version with
dynamic constituents in the wrong phase and have an expected error
- This is perhaps not the best way to test this, but it's what I came up
with
manual testing:

Fixes:
closes #572

---------

Co-authored-by: Courtney Peverley <[email protected]>
Co-authored-by: Courtney Peverley <[email protected]>
Co-authored-by: Courtney Peverley <[email protected]>
  • Loading branch information
4 people authored Oct 31, 2024
1 parent 49a3c3f commit 5ec3e42
Show file tree
Hide file tree
Showing 41 changed files with 557 additions and 903 deletions.
52 changes: 3 additions & 49 deletions scripts/ccpp_capgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def compare_fheader_to_mheader(meta_header, fort_header, logger):
###############################################################################
def check_fortran_against_metadata(meta_headers, fort_headers,
mfilename, ffilename, logger,
dyn_routines=None, fortran_routines=None):
fortran_routines=None):
###############################################################################
"""Compare a set of metadata headers from <mfilename> against the
code in the associated Fortran file, <ffilename>.
Expand Down Expand Up @@ -452,17 +452,6 @@ def check_fortran_against_metadata(meta_headers, fort_headers,
's' if num_errors > 1 else '',
mfilename, ffilename))
# end if
# Check that any dynamic constituent routines declared in the metadata are
# present in the Fortran
if dyn_routines:
for routine in dyn_routines:
if routine not in fortran_routines:
# throw an error - it's not in the Fortran
errmsg = f"Dynamic constituent routine {routine} not found in fortran {ffilename}"
raise CCPPError(errmsg)
# end if
# end for
# end if
# No return, an exception is raised on error

###############################################################################
Expand Down Expand Up @@ -561,15 +550,8 @@ def parse_scheme_files(scheme_filenames, run_env, skip_ddt_check=False):
for sect in [x.sections() for x in ftables]:
fheaders.extend(sect)
# end for
dyn_routines = []
for table in mtables:
if table.dyn_const_routine:
dyn_routines.append(table.dyn_const_routine)
# end if
# end for
check_fortran_against_metadata(mheaders, fheaders,
filename, fort_file, logger,
dyn_routines=dyn_routines,
fortran_routines=additional_routines)
# Check for duplicate tables, then add to dict
for table in mtables:
Expand All @@ -593,22 +575,6 @@ def parse_scheme_files(scheme_filenames, run_env, skip_ddt_check=False):
# end if
# end for
# end for
# Check for duplicate dynamic constituent routine names
dyn_val_dict = {}
for table in table_dict:
routine_name = table_dict[table].dyn_const_routine
if routine_name:
if routine_name in dyn_val_dict:
# dynamic constituent routines must have unique names
scheme_name = dyn_val_dict[routine_name]
errmsg = f"ERROR: Dynamic constituent routine names must be unique. Cannot add " \
f"{routine_name} for {table}. Routine already exists in {scheme_name}. "
raise CCPPError(errmsg)
else:
dyn_val_dict[routine_name] = table
# end if
# end if
# end for

return header_dict.values(), table_dict

Expand Down Expand Up @@ -674,24 +640,12 @@ def capgen(run_env, return_db=False):
# First up, handle the host files
host_model = parse_host_model_files(host_files, host_name, run_env)
# Next, parse the scheme files
# We always need to parse the ccpp_constituent_prop_ptr_t DDT
# We always need to parse the constituent DDTs
const_prop_mod = os.path.join(src_dir, "ccpp_constituent_prop_mod.meta")
if const_prop_mod not in scheme_files:
scheme_files= [const_prop_mod] + scheme_files
# end if
scheme_headers, scheme_tdict = parse_scheme_files(scheme_files, run_env)
# Pull out the dynamic constituent routines, if any
dyn_const_dict = {}
dyn_val_dict = {}
for table in scheme_tdict:
routine_name = scheme_tdict[table].dyn_const_routine
if routine_name is not None:
if routine_name not in dyn_val_dict:
dyn_const_dict[table] = routine_name
dyn_val_dict[routine_name] = table
# end if
# end if
# end for
if run_env.verbose:
ddts = host_model.ddt_lib.keys()
if ddts:
Expand Down Expand Up @@ -722,7 +676,7 @@ def capgen(run_env, return_db=False):
# end if
os.makedirs(outtemp_dir)
# end if
ccpp_api = API(sdfs, host_model, scheme_headers, run_env, dyn_const_dict)
ccpp_api = API(sdfs, host_model, scheme_headers, run_env)
cap_filenames = ccpp_api.write(outtemp_dir, run_env)
if run_env.generate_host_cap:
# Create a cap file
Expand Down
65 changes: 0 additions & 65 deletions scripts/ccpp_datafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
{"report" : "dependencies", "type" : bool,
"help" : ("Return a list of scheme and host "
"dependency module names")},
{"report" : "dyn_const_routines", "type" : bool,
"help" : ("Return the constituent routines for a suite")},
{"report" : "suite_list", "type" : bool,
"help" : "Return a list of configured suite names"},
{"report" : "required_variables", "type" : str,
Expand Down Expand Up @@ -106,8 +104,6 @@ def __init__(self, action, value=True):
# Test a valid action
>>> DatatableReport('input_variables', False).action
'input_variables'
>>> DatatableReport('dyn_const_routines', True).value
True
# Test an invalid action
>>> DatatableReport('banana', True).value
Expand Down Expand Up @@ -400,40 +396,6 @@ def _retrieve_dependencies(table):
# end for
return sorted(result)

###############################################################################
def _retrieve_dyn_const_routines(table):
###############################################################################
"""Find and return a list of all scheme constituent routines.
# Test valid dynamic constituent routines
>>> table = ET.fromstring("<ccpp_datatable version='1.0'><dyn_const_routines>" \
"<dyn_const_routine parent='banana'>dyn_const_get" \
"</dyn_const_routine><dyn_const_routine>dyn_const_2" \
"</dyn_const_routine></dyn_const_routines></ccpp_datatable>")
>>> _retrieve_dyn_const_routines(table)
['dyn_const_2', 'dyn_const_get']
# Test no dynamic constituent routines
>>> table = ET.fromstring("<ccpp_datatable version='1.0'><dyn_const_routines>" \
"</dyn_const_routines></ccpp_datatable>")
>>> _retrieve_dyn_const_routines(table)
[]
# Test missing dynamic constituent routines tag
>>> table = ET.fromstring("<ccpp_datatable version='1.0'></ccpp_datatable>")
>>> _retrieve_dyn_const_routines(table)
Traceback (most recent call last):
...
ccpp_datafile.CCPPDatatableError: Could not find 'dyn_const_routines' element
"""
routines = table.find("dyn_const_routines")
if routines is None:
raise CCPPDatatableError("Could not find 'dyn_const_routines' element")
# end if
routine_names = [routine.text for routine in routines if routine.text]
# end for
return sorted(routine_names)

###############################################################################
def _find_var_dictionary(table, dict_name=None, dict_type=None):
###############################################################################
Expand Down Expand Up @@ -746,8 +708,6 @@ def datatable_report(datatable, action, sep, exclude_protected=False):
result = _retrieve_module_list(table)
elif action.action_is("dependencies"):
result = _retrieve_dependencies(table)
elif action.action_is("dyn_const_routines"):
result = _retrieve_dyn_const_routines(table)
elif action.action_is("suite_list"):
result = _retrieve_suite_list(table)
elif action.action_is("required_variables"):
Expand Down Expand Up @@ -1093,20 +1053,6 @@ def _add_dependencies(parent, scheme_depends, host_depends):
entry.text = sfile
# end for

###############################################################################
def _add_dyn_const_routine(file_entry, routine, scheme):
###############################################################################
"""Add a section to <parent> that lists all the constituent routines
for the suite
>>> file_entry = ET.fromstring("<ccpp_datatable><dyn_const_routines></dyn_const_routines></ccpp_datatable>")
>>> _add_dyn_const_routine(file_entry, 'test_dyn_const', 'test_scheme')
>>> table_entry_pretty_print(file_entry, 0)
'<ccpp_datatable>\\n <dyn_const_routines />\\n <dyn_const_routine parent=test_scheme>\\n test_dyn_const\\n </dyn_const_routine>\\n</ccpp_datatable>\\n'
"""
entry = ET.SubElement(file_entry, "dyn_const_routine")
entry.text = routine
entry.set("parent", scheme)

###############################################################################
def _add_generated_files(parent, host_files, suite_files, ccpp_kinds, src_dir):
###############################################################################
Expand Down Expand Up @@ -1233,17 +1179,6 @@ def generate_ccpp_datatable(run_env, host_model, api, scheme_headers,
# end for
# end for
_add_dependencies(datatable, scheme_depends, host_depends)
# Add in all constituent routines
first_const_routine = True
for table in scheme_tdict:
if scheme_tdict[table].dyn_const_routine is not None:
if first_const_routine:
file_entry = ET.SubElement(datatable, "dyn_const_routines")
first_const_routine = False
# end if
_add_dyn_const_routine(file_entry, scheme_tdict[table].dyn_const_routine, table)
# end if
# end for
# Write tree
datatable_tree = PrettyElementTree(datatable)
datatable_tree.write(run_env.datatable_file)
Expand Down
5 changes: 4 additions & 1 deletion scripts/ccpp_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# CCPP framework imports
from state_machine import StateMachine

_REG_ST = r"(?:register)"
_INIT_ST = r"(?:init(?:ial(?:ize)?)?)"
_FINAL_ST = r"(?:final(?:ize)?)"
_RUN_ST = r"(?:run)"
Expand All @@ -12,7 +13,9 @@
# Allowed CCPP transitions
# pylint: disable=bad-whitespace
RUN_PHASE_NAME = 'run'
CCPP_STATE_MACH = StateMachine((('initialize', 'uninitialized',
CCPP_STATE_MACH = StateMachine((('register', 'uninitialized',
'uninitialized', _REG_ST),
('initialize', 'uninitialized',
'initialized', _INIT_ST),
('timestep_initial', 'initialized',
'in_time_step', _TS_INIT_ST),
Expand Down
27 changes: 15 additions & 12 deletions scripts/ccpp_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class Suite(VarDictionary):
'''

# Note that these group names need to match CCPP_STATE_MACH
__register_group_name = 'register'

__initial_group_name = 'initialize'

__final_group_name = 'finalize'
Expand Down Expand Up @@ -205,6 +207,8 @@ def parse(self, run_env):
if run_env.logger and run_env.logger.isEnabledFor(logging.INFO):
run_env.logger.info(lmsg.format(self.name))
# end if
gname = Suite.__register_group_name
self.__suite_reg_group = self.new_group_from_name(gname, run_env)
gname = Suite.__initial_group_name
self.__suite_init_group = self.new_group_from_name(gname, run_env)
gname = Suite.__final_group_name
Expand All @@ -214,11 +218,13 @@ def parse(self, run_env):
gname = Suite.__timestep_final_group_name
self.__timestep_final_group = self.new_group_from_name(gname, run_env)
# Set up some groupings for later efficiency
self._beg_groups = [self.__suite_init_group.name,
self._beg_groups = [self.__suite_reg_group.name,
self.__suite_init_group.name,
self.__timestep_init_group.name]
self._end_groups = [self.__suite_final_group.name,
self.__timestep_final_group.name]
# Build hierarchical structure as in SDF
self.__groups.append(self.__suite_reg_group)
self.__groups.append(self.__suite_init_group)
self.__groups.append(self.__timestep_init_group)
for suite_item in suite_xml:
Expand Down Expand Up @@ -560,8 +566,13 @@ def write(self, output_dir, run_env):
outfile.end_module_header()
for group in self.__groups:
if group.name in self._beg_groups:
group.write(outfile, self.__host_arg_list_noloop,
1, const_mod, suite_vars=self, allocate=True)
if group.name == self.__suite_reg_group.name:
group.write(outfile, self.__host_arg_list_noloop,
1, const_mod, suite_vars=self)
else:
group.write(outfile, self.__host_arg_list_noloop,
1, const_mod, suite_vars=self, allocate=True)
# end if
elif group.name in self._end_groups:
group.write(outfile, self.__host_arg_list_noloop,
1, const_mod, suite_vars=self, deallocate=True)
Expand Down Expand Up @@ -615,7 +626,7 @@ class API(VarDictionary):
'kind':'len=*', 'units':'',
'dimensions':'()'}, _API_SOURCE, _API_DUMMY_RUN_ENV)

def __init__(self, sdfs, host_model, scheme_headers, run_env, dyn_const_dict={}):
def __init__(self, sdfs, host_model, scheme_headers, run_env):
"""Initialize this API.
<sdfs> is the list of Suite Definition Files to be parsed for
data needed by the CCPP cap.
Expand All @@ -624,14 +635,11 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env, dyn_const_dict={})
<scheme_headers> is the list of parsed physics scheme metadata files.
Every scheme referenced by an SDF in <sdfs> MUST be in this list,
however, unused schemes are allowed.
<dyn_const_dict> is the dictionary (key = scheme name) of dynamic
constituent routine names
<run_env> is the CCPPFrameworkEnv object for this framework run.
"""
self.__module = 'ccpp_physics_api'
self.__host = host_model
self.__suites = list()
self.__dyn_const_dict = dyn_const_dict
super().__init__(self.module, run_env, parent_dict=self.host_model)
# Create a usable library out of scheme_headers
# Structure is dictionary of dictionaries
Expand Down Expand Up @@ -1187,11 +1195,6 @@ def suites(self):
"Return the list of this API's suites"
return self.__suites

@property
def dyn_const_dict(self):
"""Return the dynamic constituent routine dictionary"""
return self.__dyn_const_dict

###############################################################################
if __name__ == "__main__":
try:
Expand Down
Loading

0 comments on commit 5ec3e42

Please sign in to comment.