Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support (and tests) for optional arguments in ccpp_prebuild #552

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cd5eae8
Add optional attribute to variable properties in ccpp_prebuild, parse…
climbfuji Mar 20, 2024
28b15cc
Update test_blocked_data, test_chunked_data to run in debug mode; pro…
climbfuji Mar 22, 2024
853f43d
Add test_prebuild/test_opt_arg
climbfuji Mar 22, 2024
6556d35
Add wrapper to run all prebuild tests
climbfuji Mar 22, 2024
5e4bae1
Fix target_link_libraries for ccpp-framework in src/CMakeLists.txt
climbfuji Mar 22, 2024
363e8e5
Revert me: nasty workaround to inject missing optional attribute int …
climbfuji Mar 22, 2024
f4d4d7f
Add 'optional' attribute in scripts/mkcap.py, update print_def_* rout…
climbfuji Mar 22, 2024
413e9ec
Add support for optional variables and pointers in auto-generated cap…
climbfuji Mar 22, 2024
61b0c61
Merge branch 'main' of https://github.com/NCAR/ccpp-framework into fe…
climbfuji Apr 3, 2024
8709a48
Comment out workaround to inject optional attribute into scheme metadata
climbfuji Apr 3, 2024
f9976d6
Bug fixes and workarounds for allocatable strings with GNU in scripts…
climbfuji Apr 18, 2024
01e2d8d
Issue warning if scheme variable is optional but variable is uncondit…
climbfuji Apr 19, 2024
8138e22
Declare maximum CCPP thread count and use arrays of pointers in ccpp_…
climbfuji Apr 27, 2024
a6e9f04
Merge branch 'main' of https://github.com/NCAR/ccpp-framework into fe…
climbfuji May 10, 2024
f2b9a55
Address self-review
climbfuji May 11, 2024
259a494
Update prebuild tests
climbfuji May 11, 2024
fba4ce0
Update CI
dustinswales May 14, 2024
0e00c17
Update CI w/ GNU v9 and v13
dustinswales May 14, 2024
7d2af1d
Adding additional matrix param for both os's and excluding invalid co…
mwaxmonsky May 15, 2024
c84c9f0
Removing spaces.
mwaxmonsky May 15, 2024
bb20702
Fixing syntax.
mwaxmonsky May 15, 2024
d769bb8
Nullify pointers in declaration in mkcap.py
climbfuji May 16, 2024
b8b754c
Merge pull request #3 from mwaxmonsky/feature/more_gnu_versions_in_CI
dustinswales May 16, 2024
87f9323
Update CI
dustinswales May 16, 2024
caae916
Update CI
dustinswales May 16, 2024
788d251
Update CI
dustinswales May 16, 2024
ba27abd
Update CI
dustinswales May 16, 2024
f73bbe8
Update CI
dustinswales May 16, 2024
3839c63
Update CI
dustinswales May 16, 2024
f1fde4a
Merge branch 'feature/prebuild_optarg_based_on_merge_feature_capgen_i…
dustinswales May 16, 2024
c298399
Merge pull request #3 from dustinswales/feature/more_gnu_versions_in_CI
climbfuji May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions scripts/ccpp_prebuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,37 @@ 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':
# DH*
#for var in metadata_request[var_name]:
for idx in range(len(metadata_request[var_name])):
var = metadata_request[var_name][idx]
# *DH
if var.optional == 'F':
# DH* THIS IS WHAT IT SHOULD BE
logging.error("Conditionally allocated host-model variable {0} is not optional in {1}".format(
var_name, var.container))
success = False
### # NASTY WORKAROUND TO KEEP GOING INSTEAD
### logging.warning("Conditionally allocated host-model variable {0} is not optional in {1} - inject optional attribute in metadata".format(
### var_name, var.container))
### metadata_request[var_name][idx].optional = 'T'
# *DH
# DH*
# TEMPORARY CHECK? IF THE VARIABLE IS ALWAYS ALLOCATED, THE SCHEME VARIABLE SHOULDN'T BE OPTIONAL
else:
# DH*
#for var in metadata_request[var_name]:
for idx in range(len(metadata_request[var_name])):
var = metadata_request[var_name][idx]
# *DH
if var.optional == 'T':
logging.warn("Unconditionally allocated host-model variable {0} is optional in {1}".format(
var_name, var.container))
#success = False
# *DH
# Construct the actual target variable and list of modules to use from the information in 'container'
var = metadata_define[var_name][0]
target = ''
Expand All @@ -483,6 +514,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':
Expand Down
2 changes: 2 additions & 0 deletions scripts/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
21 changes: 18 additions & 3 deletions scripts/metadata_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,31 @@ def read_new_metadata(filename, module_name, table_name, scheme_name = None, sub
"for variable {} in table {}".format(standard_name,table_name))
# *DH 2020-05-26

# DH* TODO Explain active attribute
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 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())

# DH* TODO Explain optional attribute
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'
Comment on lines +213 to +221
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly for the optional property:

Suggested change
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'
optional_prop = new_var.get_prop_value('optional')
if not optional_prop in [False, True]:
raise Exception(f"Unexpected result: no optional attribute received from metadata parser for {standard_name} / {table_name}")
elif not scheme_name and optional_prop:
raise Exception(f"Host variable {standard_name} in table {table_name} has metadata attribute optional={optional_prop.lower()}, which is not allowed")
elif optional_prop:
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
Expand All @@ -228,6 +242,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():
Expand Down
163 changes: 129 additions & 34 deletions scripts/mkcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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.'''
Expand Down Expand Up @@ -251,7 +277,10 @@ def dimstring_local_names(self, metadata, assume_shape = False):
dim.lower(), self.standard_name))
dim1 = metadata[dim.lower()][0].local_name
if assume_shape:
# DH* TEST w/o lower dim?
dimstring.append('{}:'.format(dim0))
#dimstring.append(':')
# *DH
else:
dimstring.append('{}:{}'.format(dim0, dim1))
return '({})'.format(','.join(dimstring))
Expand All @@ -270,62 +299,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check if hosts having the optional attribute in Capgen is an error. I don't recall adding this, but maybe it's in there somewhere. @gold2718 Do you know by chance?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where you could have put it.
What exactly is the rule here? I thought the host model is the source of the need for optional variables. That is, if a host model can choose to not allocate one of its variables, it needs to be declared optional by routines that use it.

Is it possible that the distinction is really about not allowing the optional attribute for host or module metadata tables? This could easily be done in metavar.py by moving the VariableProperty, optional from the __spec_props list to the __var_props list. Since HostModel only allows 'module' and 'host' metadata tables, I think this would act as the correct barrier.

I'm not sure where this leaves DDTs. For a host DDT, can an element have the active attribute? What happens if you try that with a DDT that is used in a scheme? We may need to add logic here as well.

Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is about the metadata only. The rule is quite simple (if I understood it correctly):

  1. host models can have the active attribute for a variable or not (default value is true = always allocated)
  2. schemes can have the optional attribute for a variable or not (default value is false = always allocated by the host model)
  3. it is an error of a host model has optional in the metadata attribute
  4. it is an error of a scheme has active in the metadata attribute
  5. if a host model has an active attribute with a value other than true, any scheme using that environment variable must use optional = true (requirement3)
  6. the opposite is not true or at least should not be true: a scheme can have optional = true, because model A requires that, but there can be a model B that doesn't declare it as active in its metadata.

prebuild checks for 1-5 in its own layer after receiving the metadata from capgen. This is because we only harvest information on host metadata and scheme metadata separately (except for passing known host DDTs) and do not rely on the metadata comparison host <--> model from capgen. I will note that at this point, prebuild spits out a warning for 6 if it finds a variable with optional being true but no non-default active attribute in the model. I used this to clean up the metadata, knowing that the current ccpp physics are tied to the GFS data structures used by UFS, SCM, NEPTUNE.

Regarding your pointer question. That gets tricky. The only way out I can see here is that if a host ddt has an active attribute other than the default true, all it's members (if declared explicitly in the metadata) need that to have active to, with the allocation condition being that of the DDT (if the member is always allocated when the DDT is allocated) OR a Fortran logical && of the DDTs allocation condition and its own. We need to be explicit here and have both allocation conditions in the member var's active attribute instead of implicitly inheriting it from the DDT. That would greatly confuse the physics/model developers and users.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@climbfuji, I think your rules 1-6 plus thoughts on DDTs all make sense. We should make sure all of these are tested in capgen, do we need a new issue for that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be good. I'll work with @dustinswales on that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1,2,5, and 6 are applied In the Capgen implementation, with the exception of 3 and 4. I will add these soon, and revisit the DDTs.

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"
climbfuji marked this conversation as resolved.
Show resolved Hide resolved
else:
str = "{s.type}({s._kind}), pointer :: p"
else:
str = "{s.type}({s._kind}) :: {s.local_name}"
if self.rank:
str = "{s.type}, dimension{s.rank}, pointer :: p"
else:
str = "{s.type}, pointer :: p"
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"
else:
str = "type({s.type}), pointer :: p"
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):
Expand Down
Loading
Loading