diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 268560d12a..dc1b2e0d99 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -162,24 +162,34 @@ least rank (number of dimensions) one. Scalar arrays are identified with Field +++++ -LFRic API fields, identified with ``GH_FIELD`` metadata, represent -FEM discretisations of various dynamical core prognostic and diagnostic +LFRic API fields, identified with ``GH_FIELD`` metadata, represent FEM +discretisations of various dynamical core prognostic and diagnostic variables. In FEM, variables are discretised by placing them into a function space (see :ref:`lfric-function-space`) from which they inherit a polynomial expansion via the basis functions of that space. Field values at points within a cell are evaluated as the sum of a set -of basis functions multiplied by coefficients which are the data points. -Points of evaluation are determined by a quadrature object +of basis functions multiplied by coefficients which are the data +points. Points of evaluation are determined by a quadrature object (:ref:`lfric-quadrature`) and are independent of the function space -the field is on. Placement of field data points, also called degrees of -freedom (hereafter "DoFs"), is determined by the function space the field -is on. +the field is on. Placement of field data points, also called degrees +of freedom (hereafter "DoFs"), is determined by the function space the +field is on. An LFRic multi-data field can have more than one value +associated with each data point. + LFRic fields passed as arguments to any :ref:`LFRic kernel ` can be of ``real`` or ``integer`` primitive type. In the LFRic infrastructure, these fields are represented by instances of the ``field_type`` and ``integer_field_type`` classes, respectively. +Different fields may be defined on different numbers of vertical layers. +The number of layers can be as few as one (a 2D field). Additionally, +LFRic has the concet of multi-data fields where multiple data values can be +associated with each DoF. Unfortunately, both the number of layers and the +number of data values affects the numbering of the DoFs of a field. Therefore, +a distinct DoF map is required for each unique combination of function +space, number of vertical levels and number of data values. + .. _lfric-field-vector: Field Vector @@ -919,8 +929,8 @@ All three CMA-related kernel types must obey the following rules: 1) Since a CMA operator only acts within a single column of data, stencil operations are not permitted. -2) No vector quantities (e.g. ``GH_FIELD*3`` - see below) are - permitted as arguments. +2) No vector quantities (e.g. ``GH_FIELD*3`` - see below) or + multi-data fields are permitted as arguments. 3) The kernel must operate on cell-columns. @@ -1450,7 +1460,7 @@ Supported Function Spaces As mentioned in the :ref:`lfric-field` and :ref:`lfric-field-vector` sections, the function space of an argument specifies how it maps onto the underlying topology and, additionally, whether the data at a -point is a vector. In LFRic API the dimension of the basis function +point is a vector. In the LFRic API the dimension of the basis function set for the scalar function spaces is 1 and for the vector function spaces is 3 (see the table in :ref:`lfric-stub-generation-rules` for the dimensions of the basis and differential basis functions). @@ -1636,7 +1646,7 @@ to have stencil accesses, these two options are mutually exclusive. The metadata for each case is described in the following sections. Stencil Metadata -________________ +"""""""""""""""" Stencil metadata specifies that the corresponding field argument is accessed @@ -1716,8 +1726,7 @@ be found in ``examples/lfric/eg5``. .. _lfric-intergrid-mdata: Inter-Grid Metadata -___________________ - +""""""""""""""""""" The alternative form of the optional fifth metadata argument for a field specifies which mesh the associated field is on. This is @@ -1748,6 +1757,52 @@ meshes cannot be on the same function space while those on the same mesh must also be on the same function space. +Number of Layers Metadata +""""""""""""""""""""""""" + +If a particular field/operator kernel argument has a number of vertical +levels that is *not* the same as the first field/operator argument then +this must be specified using the ``NLAYERS`` option to ``GH_FIELD``/ +``GH_OPERATOR``. The value specified for ``NLAYERS`` may be an integer +literal encoded as a string if it is known at compile time, e.g.:: + + arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS="1") + +Alternatively, it may be given a name, e.g.:: + + arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS="some name") + +If two or more field/operator arguments are on the same function space +and have the same number of layers (whether a literal or a name) then +only one dofmap (that of the first such field listed in the metadata) +is passed to the kernel for those arguments. + +(Since the value of ``NLAYERS`` is looked-up from the corresponding kernel +argument at run time, the labels given in the kernel metadata are just that +- they do not have to correspond to anything in the LFRic infrastructure.) + +Multi-Data Metadata +""""""""""""""""""" + +A multi-data field is the same as a standard field apart from having multiple +values associated with each DoF. This is indicated in the field metadata by +the optional ``NDATA`` argument to GH_FIELD, e.g.:: + + arg_type(GH_FIELD, GH_REAL, GH_READ, W2, NDATA="4") + +The value contained in the string specified for ``NDATA`` may be a literal if +it is known at compile time. Alternatively, it may be a name in which +case the number of data values at each DoF is determined at runtime by +querying the field object (in the generated PSy layer). As with ``NLAYERS``, +this name is just a label and does not have to correspond to anything in the +LFRic infrastructure. + +Since the data in an LFRic field object is stored as a 1D array, having more +than one data value associated with each DoF affects the dofmap. This is +handled by passing the appropriate dofmap to the kernel - see +:ref:`lfric-stub-generation-rules`. + + Column-wise Operators (CMA) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2080,7 +2135,12 @@ conventions, are: 4) If the field entry stencil access is of type ``XORY1D`` then add an additional ``integer`` direction argument of kind ``i_def`` and with intent ``in``. - + 5) If the field is multi-data then the kernel must be passed the + value of ``NDATA``: add an additional ``integer``, scalar + argument of kind ``i_def`` and intent ``in``. + 6) If the field has a custom number of vertical levels then pass this as + an additional ``integer``, scalar argument of kind ``i_def`` and + intent ``in``. 3) If the current entry is a field vector then for each dimension of the vector, include a field array. The field array name is specified as @@ -2106,21 +2166,26 @@ conventions, are: the data type and kind specified in the metadata. The ScalarArray must be denoted with intent ``in`` to match its read-only nature. -4) For each function space in the order they appear in the metadata arguments - (the ``to`` function space of an operator is considered to be before the - ``from`` function space of the same operator as it appears first in - lexicographic order) +4) DoF maps for function spaces are handled in the order they appear in the + metadata arguments (the ``to`` function space of an operator is considered + to be before the ``from`` function space of the same operator as it appears + first in lexicographic order). Note that if two fields on a given function + space have differing numbers of vertical layers and/or ``NDATA`` values, + then each requires that a dofmap be supplied (because both the number of + vertical layers *and* the number of data values per DoF alter the + *values* within the map). For each required DoF map: 1) Include the number of local degrees of freedom (i.e. number per-cell) for the function space. This is an ``integer`` of kind ``i_def`` and has intent ``in``. The name of this argument is ``"ndf_"``. + 2) If there is a field on this space 1) Include the unique number of degrees of freedom for the function space. This is an ``integer`` of kind ``i_def`` and has intent ``in``. The name of this argument is ``"undf_"``. - 2) Include the **dofmap** for this function space. This is an ``integer`` + 2) Include the **dofmap** itself. This is an ``integer`` array of kind ``i_def`` with intent ``in``. It has one dimension sized by the local degrees of freedom for the function space. @@ -2443,8 +2508,9 @@ dofmap for both the to- and from-function spaces of the CMA operator. Since it does not have any LMA operator arguments it does not require the ``ncell_3d`` and ``nlayers`` scalar arguments. (Since a column-wise operator is, by definition, assembled for a whole column, -there is no loop over levels when applying it.) -The full set of rules is then: +there is no loop over levels when applying it.) Note that fields with +non-standard ``nlayers`` or ``ndata > 1`` cannot be supplied as +arguments to CMA kernels. The full set of rules is: 1) Include the ``cell`` argument. ``cell`` is an ``integer`` of kind ``i_def`` and has intent ``in``. diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index f2175145b5..dc66906118 100644 --- a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py @@ -38,7 +38,9 @@ creation, modification and Fortran output of such an argument. ''' +from typing import Optional, Union from fparser.two import Fortran2003 +from fparser.two import utils as fp_utils from psyclone.domain.lfric.kernel.common_metadata import CommonMetadata @@ -47,7 +49,7 @@ class CommonArgMetadata(CommonMetadata): '''Class to capture common LFRic kernel argument metadata.''' # The fparser2 class that captures this metadata. - fparser2_class = Fortran2003.Part_Ref + fparser2_class = Fortran2003.Structure_Constructor @staticmethod def check_boolean(value, name): @@ -64,19 +66,17 @@ def check_boolean(value, name): f"'{type(value).__name__}'.") @staticmethod - def check_nargs(fparser2_tree, nargs): + def check_nargs(fparser2_tree: Union[Fortran2003.Part_Ref, + Fortran2003.Structure_Constructor], + nargs: Union[int, tuple[int, int]]) -> None: '''Checks that the metadata has the number of arguments specified by the 'nargs' argument, otherwise an exception is raised. :param fparser2_tree: fparser2 tree capturing a metadata argument. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | \ - :py:class:`fparser.two.Fortran2003.Structure_Constructor` - :param nargs: the number of expected arguments. This can \ - either be a single value or a list containing a lower and an \ - upper value. - :type nargs: int or Tuple[int, int] + :param nargs: the number of expected arguments. This can either be + a single value or a list containing a lower and an upper value. - :raises ValueError: if the kernel metadata does not contain \ + :raises ValueError: if the kernel metadata does not contain the expected number of arguments (nargs). ''' @@ -103,11 +103,11 @@ def check_fparser2_arg(cls, fparser2_tree, type_name): Structure_Constructor which captures a metadata argument. :param fparser2_tree: fparser2 tree capturing a metadata argument. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | \ + :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | :py:class:`fparser.two.Fortran2003.Structure_Constructor` :param str type_name: the name of the argument datatype. - :raises ValueError: if the kernel metadata is not in \ + :raises ValueError: if the kernel metadata is not in the form arg_type(...). ''' @@ -150,5 +150,49 @@ def get_arg(fparser2_tree, index): # Metadata at the specified index does not exist. return None + @staticmethod + def get_named_arg(fparser2_tree: fp_utils.Base, + name: str + ) -> Optional[str]: + ''' + Searches the supplied metadata for 'name=value' expressions and + returns the value corresponding to the supplied name if found. + Otherwise returns None. If the value is a string then it is + lower-cased. + + :param fparser2_tree: the parse tree of the metadata. + :param name: the name of the metadata element that we want. + + :returns: the value of the named metadata element or None if not found. + + ''' + for child in fp_utils.walk(fparser2_tree, Fortran2003.Component_Spec): + if child.children[0].tostr().lower() == name: + text = child.children[1].tostr() + if isinstance(child.children[1], + Fortran2003.Char_Literal_Constant): + # TODO fparser/#295 - fparser keeps the quotation marks + # in character strings. + return text[1:-1].lower() + return text + return None + + @staticmethod + def _validate_named_args(fparser2_tree: fp_utils.Base, + valid_names: list[str]) -> None: + ''' + Checks that any named arguments in the supplied parse tree match + with the names in `valid_names`. + + :raises ValueError: if an unsupported named argument is found in + the supplied metadata. + ''' + for child in fp_utils.walk(fparser2_tree, Fortran2003.Component_Spec): + name = child.children[0].tostr().lower() + if name not in valid_names: + raise ValueError( + f"Kernel metadata contains keyword argument '{name}' " + f"which is not one of the valid options: {valid_names}.") + __all__ = ["CommonArgMetadata"] diff --git a/src/psyclone/domain/lfric/kernel/common_metadata.py b/src/psyclone/domain/lfric/kernel/common_metadata.py index 724f639a13..38b699618a 100644 --- a/src/psyclone/domain/lfric/kernel/common_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_metadata.py @@ -38,8 +38,10 @@ ''' from abc import ABC, abstractmethod +from typing import Union from fparser.common.readfortran import FortranStringReader +from fparser.two import Fortran2003 from fparser.two.parser import ParserFactory from fparser.two.utils import NoMatchError, FortranSyntaxError @@ -99,7 +101,9 @@ def validate_scalar_value(value, valid_values, name): f"but found '{value}'.") @staticmethod - def create_fparser2(fortran_string, encoding): + def create_fparser2(fortran_string: str, + encoding: Union[tuple[Fortran2003.Base], + Fortran2003.Base]) -> Fortran2003.Base: '''Creates an fparser2 tree from a Fortran string. The resultant parent node of the tree will be the same type as the encoding argument if the string conforms to the encoding, otherwise an @@ -108,32 +112,35 @@ def create_fparser2(fortran_string, encoding): TODO: issue #1965: relocate this method as it is not specific to metadata processing. - :param str fortran_string: a string containing the metadata in \ - Fortran. - :param encoding: the parent class with which we will encode the \ - Fortran string. - :type encoding: subclass of :py:class:`fparser.two.Fortran2003.Base` + :param fortran_string: a string containing the metadata in Fortran. + :param encoding: the fparser2 class(es) with which we will attempt + to match the Fortran string. - :returns: an fparser2 tree containing a metadata \ - argument. - :rtype: subclass of :py:class:`fparser.two.Fortran2003.Base` + :returns: an fparser2 tree containing a metadata argument. - :raises ValueError: if the Fortran string is not in the \ - expected form. + :raises ValueError: if the Fortran string is not in the expected form. ''' std = Config.get().fortran_standard _ = ParserFactory().create(std=std) reader = FortranStringReader(fortran_string) - match = True - try: - fparser2_tree = encoding(reader) - except (NoMatchError, FortranSyntaxError): - match = False + match = False + if isinstance(encoding, tuple): + classes = encoding + else: + classes = [encoding] + for enc in classes: + try: + fparser2_tree = enc(reader) + match = True + break + except (NoMatchError, FortranSyntaxError): + continue if not match or not fparser2_tree: + text = " or ".join(enc.__name__ for enc in classes) raise ValueError( f"Expected kernel metadata to be a Fortran " - f"{encoding.__name__}, but found '{fortran_string}'.") + f"{text}, but found '{fortran_string}'.") return fparser2_tree @classmethod diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index 815c7facb6..1f6a54657f 100644 --- a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py @@ -38,9 +38,11 @@ and Fortran output of a Field argument. ''' +from typing import Optional, Union + +from fparser.two import Fortran2003 from psyclone.domain.lfric import LFRicConstants -from psyclone.domain.lfric.kernel.scalar_arg_metadata import \ - ScalarArgMetadata +from psyclone.domain.lfric.kernel.scalar_arg_metadata import ScalarArgMetadata class FieldArgMetadata(ScalarArgMetadata): @@ -49,9 +51,9 @@ class FieldArgMetadata(ScalarArgMetadata): :param str datatype: the datatype of this field (GH_INTEGER, ...). :param str access: the way the kernel accesses this field (GH_WRITE, ...). - :param str function_space: the function space that this field is \ + :param str function_space: the function space that this field is on (W0, ...). - :param Optional[str] stencil: the type of stencil used by the \ + :param Optional[str] stencil: the type of stencil used by the kernel when accessing this field. ''' @@ -59,8 +61,8 @@ class FieldArgMetadata(ScalarArgMetadata): form = "gh_field" # The relative positions of LFRic metadata. Metadata for a field # argument is provided in the following format 'arg_type(form, - # datatype, access, function_space)'. Therefore, for example, the - # index of the form argument (form_arg_index) is 0. + # datatype, access, function_space, nlevels=..., ndata=...)'. Therefore, + # for example, the index of the form argument (form_arg_index) is 0. form_arg_index = 0 datatype_arg_index = 1 access_arg_index = 2 @@ -68,58 +70,77 @@ class FieldArgMetadata(ScalarArgMetadata): stencil_arg_index = 4 # The name to use for any exceptions. check_name = "field" - # The number of arguments in the language-level metadata (min and - # max values). + # The number of positional arguments in the language-level metadata (min + # and max values). nargs = (4, 5) - def __init__(self, datatype, access, function_space, stencil=None): + fparser2_class = (Fortran2003.Structure_Constructor, Fortran2003.Part_Ref) + + def __init__(self, datatype: str, access: str, function_space: str, + stencil: Optional[str] = None, + nlevels: Optional[str] = None, + ndata: Optional[str] = "1"): super().__init__(datatype, access) self.function_space = function_space self.stencil = stencil + self.nlevels = nlevels + self.ndata = ndata @classmethod - def _get_metadata(cls, fparser2_tree): + def _get_metadata( + cls, + fparser2_tree: Union[Fortran2003.Part_Ref, + Fortran2003.Structure_Constructor] + ) -> tuple[str, str, str, Optional[str], Optional[str], Optional[str]]: '''Extract the required metadata from the fparser2 tree and return it as strings. Also check that the metadata is in the expected form (but do not check the metadata values as that is done separately). - :param fparser2_tree: fparser2 tree containing the metadata \ + :param fparser2_tree: fparser2 tree containing the metadata for this argument. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | \ - :py:class:`fparser.two.Fortran2003.Structure_Constructor` - :returns: a tuple containing the datatype, access, function \ - space and stencil metadata. - :rtype: Tuple[str, str, str, Optional[str]] + :returns: a tuple containing the datatype, access, function + space, stencil, nlevels and ndata metadata. ''' datatype, access = super()._get_metadata(fparser2_tree) function_space = cls.get_arg( fparser2_tree, cls.function_space_arg_index) stencil = cls.get_stencil(fparser2_tree) - return (datatype, access, function_space, stencil) + super()._validate_named_args(fparser2_tree, + ["nlevels", "ndata"]) + nlevels = cls.get_named_arg(fparser2_tree, "nlevels") + ndata = cls.get_named_arg(fparser2_tree, "ndata") + return (datatype, access, function_space, stencil, nlevels, ndata) @classmethod - def get_stencil(cls, fparser2_tree): + def get_stencil( + cls, + fparser2_tree: Fortran2003.Structure_Constructor) -> Optional[str]: '''Retrieves the stencil metadata value found within the supplied fparser2 tree (if there is one) and checks that it is valid. :param fparser2_tree: fparser2 tree capturing the required metadata. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` - :returns: the stencil value extracted from the fparser2 tree \ + :returns: the stencil value extracted from the fparser2 tree if there is one, or None if not. - :rtype: Optional[str] - :raises TypeError: if the stencil metadata is not in the \ - expected form. + :raises TypeError: if the stencil metadata is not in the expected form. ''' raw_stencil_text = FieldArgMetadata.get_arg( fparser2_tree, cls.stencil_arg_index) if not raw_stencil_text: return None + + if isinstance( + fparser2_tree.children[1].children[cls.stencil_arg_index], + Fortran2003.Component_Spec): + # This is a keyword=value metadata element and thus not stencil + # information. + return None + raw_stencil_text = raw_stencil_text.strip().lower() if not (raw_stencil_text.startswith("stencil(") and raw_stencil_text.endswith(")") and len(raw_stencil_text) > 9): @@ -128,16 +149,20 @@ def get_stencil(cls, fparser2_tree): stencil = raw_stencil_text[8:-1] return stencil - def fortran_string(self): + def fortran_string(self) -> str: ''' :returns: the metadata represented by this class as Fortran. - :rtype: str ''' + result = (f"arg_type({self.form}, {self.datatype}, {self.access}, " + f"{self.function_space}") if self.stencil: - return (f"arg_type({self.form}, {self.datatype}, {self.access}, " - f"{self.function_space}, stencil({self.stencil}))") - return (f"arg_type({self.form}, {self.datatype}, {self.access}, " - f"{self.function_space})") + result += f", stencil({self.stencil})" + if self.nlevels: + result += f", nlevels='{self.nlevels}'" + if self.ndata and self.ndata != "1": + result += f", ndata='{self.ndata}'" + result += ")" + return result @staticmethod def check_datatype(value): diff --git a/src/psyclone/domain/lfric/kernel/field_vector_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_vector_arg_metadata.py index 53f2a849e8..ef51999f78 100644 --- a/src/psyclone/domain/lfric/kernel/field_vector_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_vector_arg_metadata.py @@ -38,6 +38,8 @@ and Fortran output of a Field Vector argument. ''' +from typing import Optional, Tuple +from fparser.two import Fortran2003 from psyclone.domain.lfric.kernel.field_arg_metadata import FieldArgMetadata @@ -68,30 +70,35 @@ class FieldVectorArgMetadata(FieldArgMetadata): vector = True def __init__(self, datatype, access, function_space, vector_length, - stencil=None): - super().__init__(datatype, access, function_space, stencil=stencil) + stencil=None, nlevels=None, ndata=1): + super().__init__(datatype, access, function_space, stencil=stencil, + nlevels=nlevels, ndata=ndata) self.vector_length = vector_length @classmethod - def _get_metadata(cls, fparser2_tree): + def _get_metadata( + cls, + fparser2_tree: Fortran2003.Part_Ref) -> Tuple[str, str, str, str, + Optional[str], + Optional[str], + Optional[str]]: '''Extract the required metadata from the fparser2 tree and return it as strings. Also check that the metadata is in the expected form (but do not check the metadata values as that is done separately). - :param fparser2_tree: fparser2 tree containing the metadata \ + :param fparser2_tree: fparser2 tree containing the metadata for this argument. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` - :returns: a tuple containing the datatype, access, function \ - space, vector-length and stencil metadata. - :rtype: Tuple[str, str, str, str, Optional[str]] + :returns: a tuple containing the datatype, access, function + space, vector-length, stencil, nlevels and ndata metadata. ''' - datatype, access, function_space, stencil = super()._get_metadata( - fparser2_tree) + (datatype, access, function_space, stencil, + nlevels, ndata) = super()._get_metadata(fparser2_tree) vector_length = cls.get_vector_length(fparser2_tree) - return (datatype, access, function_space, vector_length, stencil) + return (datatype, access, function_space, vector_length, stencil, + nlevels, ndata) def fortran_string(self): ''' diff --git a/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py index b728656aa7..026f0edf6c 100644 --- a/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py @@ -38,6 +38,7 @@ and Fortran output of an intergrid argument. ''' +from typing import Optional from fparser.two import Fortran2003 from psyclone.domain.lfric import LFRicConstants @@ -48,134 +49,89 @@ class InterGridArgMetadata(FieldArgMetadata): '''Class to capture LFRic kernel metadata information for an intergrid argument. - :param str datatype: the datatype of this InterGrid argument \ - (GH_INTEGER, ...). - :param str access: the way the kernel accesses this intergrid \ + :param datatype: the datatype of this InterGrid argument (GH_INTEGER, ...). + :param access: the way the kernel accesses this intergrid argument (GH_WRITE, ...). - :param str function_space: the function space that this \ + :param function_space: the function space that this InterGrid is on (W0, ...). - :param str mesh_arg: the type of mesh that this InterGrid arg \ + :param mesh_arg: the type of mesh that this InterGrid arg is on (coarse or fine). - :param Optional[str] stencil: the type of stencil used by the \ + :param stencil: the type of stencil used by the kernel when accessing this InterGrid arg. ''' - # The relative position of LFRic mesh metadata. Metadata for an - # inter-grid argument is provided in the following format - # 'arg_type(form, datatype, access, function_space, [stencil], - # mesh)'. The stencil argument is optional and its index - # (stencil_arg_index) is therefore 4 if it exists and the index of - # the mesh argument is 4 or 5 depending on whether there is a - # stencil argument. As the mesh argument index value is not known - # beforehand, it is not set. Fixed index values not provided here - # are common to the parent classes and are inherited from them. - stencil_arg_index = 4 # The name to use for any exceptions. check_name = "inter-grid" # The number of arguments in the language-level metadata (min and # max values). - nargs = (5, 6) + nargs = (5, 8) # The fparser2 class that captures this metadata. fparser2_class = Fortran2003.Structure_Constructor - def __init__(self, datatype, access, function_space, mesh_arg, - stencil=None): - super().__init__(datatype, access, function_space, stencil=stencil) + def __init__(self, + datatype: str, + access: str, + function_space: str, + mesh_arg: str, + stencil: Optional[str] = None, + nlevels: Optional[str] = None, + ndata: Optional[str] = "1"): + super().__init__(datatype, access, function_space, stencil=stencil, + nlevels=nlevels, ndata=ndata) self.mesh_arg = mesh_arg @classmethod - def _get_metadata(cls, fparser2_tree): + def _get_metadata( + cls, + fparser2_tree: Fortran2003.Structure_Constructor + ) -> tuple[str, str, str, str, + Optional[str], Optional[str], Optional[str]]: '''Extract the required metadata from the fparser2 tree and return it - as strings. Also check that the metadata is in the expected - form (but do not check the metadata values as that is done - separately). + as strings. Also check that the metadata is in the expected form (but + do not check the metadata values as that is done separately). - :param fparser2_tree: fparser2 tree containing the metadata \ + :param fparser2_tree: fparser2 tree containing the metadata for this argument. - :type fparser2_tree: \ - :py:class:`fparser.two.Fortran2003.Structure_Constructor` - :returns: a tuple containing the datatype, access, function \ - space, mesh and stencil metadata. - :rtype: Tuple[str, str, str, str, Optional[str]] + :returns: a tuple containing the datatype, access, function + space, mesh, stencil, nlevels and ndata metadata. ''' datatype, access = cls._get_datatype_access_metadata(fparser2_tree) - function_space = cls.get_arg( - fparser2_tree, cls.function_space_arg_index) - try: - stencil = cls.get_stencil(fparser2_tree) - mesh_arg = cls.get_mesh_arg(fparser2_tree, 5) - except TypeError: - stencil = None - mesh_arg = cls.get_mesh_arg(fparser2_tree, 4) - - return (datatype, access, function_space, mesh_arg, stencil) - - @staticmethod - def get_mesh_arg(fparser2_tree, mesh_arg_index): - '''Retrieves the mesh_arg metadata value from the supplied fparser2 - tree. - - :param fparser2_tree: fparser2 tree capturing the metadata for \ - an InterGrid argument. - :type fparser2_tree: \ - :py:class:`fparser.two.Fortran2003.Structure_Constructor` - :param int mesh_arg_index: the index at which to find the metadata. - - :returns: the metadata mesh value extracted from the fparser2 tree. - :rtype: str - - raises ValueError: if the metadata is not in the form \ - 'mesh_arg = '. + function_space = cls.get_arg(fparser2_tree, + cls.function_space_arg_index) - ''' - try: - mesh_arg_lhs = fparser2_tree.children[1].\ - children[mesh_arg_index].children[0].tostr() - except IndexError as info: - raise ValueError( - f"At argument index {mesh_arg_index} for metadata " - f"'{fparser2_tree}' expected the metadata to be in the form " - f"'mesh_arg=value' but found " - f"'{fparser2_tree.children[1].children[mesh_arg_index]}'.") \ - from info - - if not mesh_arg_lhs.lower() == "mesh_arg": - raise ValueError( - f"At argument index {mesh_arg_index} for metadata " - f"'{fparser2_tree}' expected the left hand side " - f"to be 'mesh_arg' but found '{mesh_arg_lhs}'.") - mesh_arg = fparser2_tree.children[1].\ - children[mesh_arg_index].children[1].tostr() - return mesh_arg - - def fortran_string(self): + cls._validate_named_args(fparser2_tree, ["mesh_arg", "nlevels", + "ndata"]) + stencil = cls.get_stencil(fparser2_tree) + + mesh_arg = cls.get_named_arg(fparser2_tree, "mesh_arg") + nlevels = cls.get_named_arg(fparser2_tree, "nlevels") + ndata = cls.get_named_arg(fparser2_tree, "ndata") + + return (datatype, access, function_space, mesh_arg, stencil, nlevels, + ndata) + + def fortran_string(self) -> str: ''' :returns: the metadata represented by this class as Fortran. - :rtype: str ''' - if self.stencil: - return (f"arg_type({self.form}, {self.datatype}, {self.access}, " - f"{self.function_space}, stencil({self.stencil}), " - f"mesh_arg={self.mesh_arg})") - return (f"arg_type({self.form}, {self.datatype}, {self.access}, " - f"{self.function_space}, mesh_arg={self.mesh_arg})") + result = super().fortran_string() + # Have to remove closing ')' before adding mesh_arg info. + return (f"{result[:-1]}, mesh_arg={self.mesh_arg})") @property - def mesh_arg(self): + def mesh_arg(self) -> str: ''' :returns: the mesh type for this intergrid argument. - :rtype: str ''' return self._mesh_arg @mesh_arg.setter - def mesh_arg(self, value): + def mesh_arg(self, value: str) -> None: ''' - :param str value: set the mesh type to the \ - specified value. + :param value: set the mesh type to the specified value. ''' const = LFRicConstants() InterGridArgMetadata.validate_scalar_value( diff --git a/src/psyclone/domain/lfric/kernel/inter_grid_vector_arg_metadata.py b/src/psyclone/domain/lfric/kernel/inter_grid_vector_arg_metadata.py index 91aa2c63f0..706ce75831 100644 --- a/src/psyclone/domain/lfric/kernel/inter_grid_vector_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/inter_grid_vector_arg_metadata.py @@ -39,6 +39,8 @@ argument. ''' +from typing import Optional +from fparser.two import Fortran2003 from psyclone.domain.lfric.kernel.inter_grid_arg_metadata import \ InterGridArgMetadata @@ -74,33 +76,34 @@ class InterGridVectorArgMetadata(InterGridArgMetadata): vector = True def __init__(self, datatype, access, function_space, mesh_arg, - vector_length, stencil=None): + vector_length, stencil=None, nlevels=None, ndata="1"): super().__init__( - datatype, access, function_space, mesh_arg, stencil=stencil) + datatype, access, function_space, mesh_arg, stencil=stencil, + nlevels=nlevels, ndata=ndata) self.vector_length = vector_length @classmethod - def _get_metadata(cls, fparser2_tree): + def _get_metadata(cls, + fparser2_tree: Fortran2003.Structure_Constructor + ) -> tuple[str, str, str, str, str, + Optional[str], Optional[str], Optional[str]]: '''Extract the required metadata from the fparser2 tree and return it as strings. Also check that the metadata is in the expected form (but do not check the metadata values as that is done separately). - :param fparser2_tree: fparser2 tree containing the metadata \ + :param fparser2_tree: fparser2 tree containing the metadata for this argument. - :type fparser2_tree: \ - :py:class:`fparser.two.Fortran2003.Structure_Constructor` - :returns: a tuple containing the datatype, access, function \ - space, mesh, vector-length and stencil metadata. - :rtype: Tuple[str, str, str, str, str, Optional[str]] + :returns: a tuple containing the datatype, access, function + space, mesh, vector-length, stencil, nlevels and ndata metadata. ''' - datatype, access, function_space, mesh_arg, stencil = \ + datatype, access, function_space, mesh_arg, stencil, nlevels, ndata = \ super()._get_metadata(fparser2_tree) vector_length = cls.get_vector_length(fparser2_tree) return (datatype, access, function_space, mesh_arg, vector_length, - stencil) + stencil, nlevels, ndata) def fortran_string(self): ''' diff --git a/src/psyclone/domain/lfric/kernel/meta_args_metadata.py b/src/psyclone/domain/lfric/kernel/meta_args_metadata.py index 6d60d37f12..472aac71bb 100644 --- a/src/psyclone/domain/lfric/kernel/meta_args_metadata.py +++ b/src/psyclone/domain/lfric/kernel/meta_args_metadata.py @@ -37,6 +37,7 @@ the values for the LFRic kernel meta_args metadata. ''' +from __future__ import annotations from fparser.two import Fortran2003 from fparser.two.utils import walk @@ -85,20 +86,16 @@ def fortran_string(self): "ARG_TYPE", "META_ARGS", self._meta_args_args) @staticmethod - def create_from_fparser2(fparser2_tree): - '''Create an instance of MetaArgsMetadata from an fparser2 - tree. + def create_from_fparser2( + fparser2_tree: Fortran2003.Data_Component_Def_Stmt + ) -> MetaArgsMetadata: + '''Create an instance of MetaArgsMetadata from an fparser2 tree. - :param fparser2_tree: fparser2 tree capturing the meta \ - args metadata. - :type fparser2_tree: :py:class:`fparser.two.Fortran2003.\ - Data_Component_Def_Stmt` + :param fparser2_tree: fparser2 tree capturing the meta args metadata. :returns: an instance of MetaArgsMetadata. - :rtype: :py:class:`psyclone.domain.lfric.kernel.\ - MetaArgsMetadata` - :raises ParseError: if an unknown MetaArgsArgMetadata argument \ + :raises ParseError: if an unknown MetaArgsArgMetadata argument is found. ''' @@ -121,11 +118,8 @@ def create_from_fparser2(fparser2_tree): nargs = len(meta_arg.children[1].children) intergrid_arg = False if nargs == 5: - fifth_arg = meta_arg.children[1].children[4] - intergrid_arg = ( - fifth_arg.children and - fifth_arg.children[0].string.lower() == "mesh_arg") - + intergrid_arg = FieldArgMetadata.get_named_arg(meta_arg, + "mesh_arg") if intergrid_arg and vector_arg: arg = InterGridVectorArgMetadata.create_from_fparser2( meta_arg) diff --git a/src/psyclone/domain/lfric/kernel/meta_funcs_arg_metadata.py b/src/psyclone/domain/lfric/kernel/meta_funcs_arg_metadata.py index bf06d8c16c..d9dba1f5cd 100644 --- a/src/psyclone/domain/lfric/kernel/meta_funcs_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/meta_funcs_arg_metadata.py @@ -37,6 +37,8 @@ the argument values for the LFRic kernel META_FUNCS metadata. ''' +from fparser.two import Fortran2003 + from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel.common_arg_metadata import CommonArgMetadata from psyclone.parse.utils import ParseError @@ -53,6 +55,8 @@ class MetaFuncsArgMetadata(CommonArgMetadata): function is required. Defaults to False. ''' + fparser2_class = Fortran2003.Part_Ref + def __init__(self, function_space, basis_function=False, diff_basis_function=False): super().__init__() diff --git a/src/psyclone/domain/lfric/kernel/meta_mesh_arg_metadata.py b/src/psyclone/domain/lfric/kernel/meta_mesh_arg_metadata.py index 9ecb005d2b..0d21d541e1 100644 --- a/src/psyclone/domain/lfric/kernel/meta_mesh_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/meta_mesh_arg_metadata.py @@ -38,6 +38,8 @@ META_MESH metadata. ''' +from fparser.two import Fortran2003 + from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel.common_arg_metadata import CommonArgMetadata @@ -49,6 +51,8 @@ class MetaMeshArgMetadata(CommonArgMetadata): :param str mesh: the name of the mesh property. ''' + fparser2_class = Fortran2003.Part_Ref + def __init__(self, mesh): super().__init__() self.mesh = mesh diff --git a/src/psyclone/domain/lfric/kernel/meta_ref_element_arg_metadata.py b/src/psyclone/domain/lfric/kernel/meta_ref_element_arg_metadata.py index 3ad0ce5805..9ea0180bf4 100644 --- a/src/psyclone/domain/lfric/kernel/meta_ref_element_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/meta_ref_element_arg_metadata.py @@ -38,6 +38,8 @@ REFERENCE_ELEMENT metadata. ''' +from fparser.two import Fortran2003 + from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel.common_arg_metadata import CommonArgMetadata @@ -50,6 +52,8 @@ class MetaRefElementArgMetadata(CommonArgMetadata): :param str reference_element: the name of the reference_element property. ''' + fparser2_class = Fortran2003.Part_Ref + def __init__(self, reference_element): super().__init__() self.reference_element = reference_element diff --git a/src/psyclone/domain/lfric/kernel/scalar_arg_metadata.py b/src/psyclone/domain/lfric/kernel/scalar_arg_metadata.py index 4eb415bc5b..c0288d8817 100644 --- a/src/psyclone/domain/lfric/kernel/scalar_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/scalar_arg_metadata.py @@ -38,6 +38,8 @@ and Fortran output of a Scalar argument. ''' +from fparser.two import Fortran2003 + from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel.common_meta_arg_metadata import \ CommonMetaArgMetadata @@ -61,6 +63,7 @@ class ScalarArgMetadata(CommonMetaArgMetadata): check_name = "scalar" # The number of arguments in the language-level metadata. nargs = 3 + fparser2_class = Fortran2003.Part_Ref @classmethod def _get_metadata(cls, fparser2_tree): diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index d1df5712ff..0ac90994a8 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -51,7 +51,8 @@ from psyclone.domain.lfric.lfric_constants import LFRicConstants from psyclone.errors import InternalError import psyclone.expression as expr -from psyclone.parse.kernel import Descriptor, get_stencil, get_mesh +from psyclone.parse.kernel import ( + Descriptor, get_stencil, get_mesh, get_char_value) from psyclone.parse.utils import ParseError # API configuration @@ -109,6 +110,11 @@ def __init__(self, arg_type, operates_on, metadata_index): self._function_space2 = None self._stencil = None self._mesh = None + # No. of vertical levels associated with the argument. Defaults to + # using that of the first field/operator argument to a kernel. + self._nlevels = None + # No. of data values per dof - defaults to 1. + self._ndata = "1" self._nargs = 0 # Check for the correct argument type descriptor @@ -372,8 +378,8 @@ def _init_field(self, arg_type, operates_on): f"{nargs_field_min} arguments if its first argument is of " f"{const.VALID_FIELD_NAMES} type, but found {self._nargs} in " f"'{arg_type}'.") - # There must be at most 5 arguments - nargs_field_max = 5 + # There must be at most 7 arguments + nargs_field_max = 7 if self._nargs > nargs_field_max: raise ParseError( f"In the LFRic API each 'meta_arg' entry must have at most " @@ -400,27 +406,33 @@ def _init_field(self, arg_type, operates_on): f"'{arg_type.args[prop_ind].name}' in '{arg_type}'.") self._function_space1 = arg_type.args[prop_ind].name - # The optional 5th argument is either a stencil specification - # or a mesh identifier (for inter-grid kernels) - prop_ind = 4 - if self._nargs == nargs_field_max: - try: - if "stencil" in str(arg_type.args[prop_ind]): - self._stencil = get_stencil( - arg_type.args[prop_ind], - const.VALID_STENCIL_TYPES) - elif "mesh" in str(arg_type.args[prop_ind]): - self._mesh = get_mesh(arg_type.args[prop_ind], - const.VALID_MESH_TYPES) - else: - raise ParseError("Unrecognised metadata entry") - except ParseError as err: - raise ParseError( - f"In the LFRic API argument {prop_ind+1} of a 'meta_arg' " - f"field entry must be either a valid stencil specification" - f" or a mesh identifier (for inter-grid kernels). However," - f" entry '{arg_type}' raised the following error: " - f"{err}.") from err + num_args = len(arg_type.args) + if num_args > 4: + for prop_ind in range(4, num_args): + try: + if "stencil" in str(arg_type.args[prop_ind]): + self._stencil = get_stencil( + arg_type.args[prop_ind], + const.VALID_STENCIL_TYPES) + elif "mesh" in str(arg_type.args[prop_ind]): + self._mesh = get_mesh(arg_type.args[prop_ind], + const.VALID_MESH_TYPES) + elif "nlevels" in str(arg_type.args[prop_ind]): + self._nlevels = get_char_value(arg_type.args[prop_ind], + "nlevels") + elif "ndata" in str(arg_type.args[prop_ind]): + self._ndata = get_char_value(arg_type.args[prop_ind], + "ndata") + else: + raise ParseError("Unrecognised metadata entry") + except ParseError as err: + raise ParseError( + f"In the LFRic API argument {prop_ind+1} of a " + f"'meta_arg' field entry must be either a valid " + f"stencil specification, a number of levels, a number " + f"of data values per dof or a mesh identifier (for " + f"inter-grid kernels). However, entry '{arg_type}' " + f"raised the following error: {err}.") from err # Test allowed accesses for fields field_disc_accesses = [AccessType.READ, AccessType.WRITE, @@ -795,39 +807,52 @@ def function_spaces(self): f"'{self._argument_type}'.") @property - def vector_size(self): + def nlevels(self) -> str: + ''' + :returns: a label (or integer, encoded as a string) identifying the + number of vertical levels associated with this argument. + ''' + return self._nlevels + + @property + def ndata(self) -> str: + ''' + :returns: a label (or integer, encoded as a string) identifying the + number of data values associated with each DoF of the argument. + ''' + return self._ndata + + @property + def vector_size(self) -> int: ''' Returns the vector size of the argument. This will be 1 if ``*n`` has not been specified for all argument types except scalars (their vector size is set to 0). :returns: vector size of the argument. - :rtype: int ''' return self._vector_size @property - def array_ndims(self): + def array_ndims(self) -> int: ''' Returns the array rank of the argument. This will be 1 if ``*n`` has not been specified for all argument types except scalars (their array rank is set to 0). :returns: array rank of the argument. - :rtype: int ''' return self._array_ndims - def __str__(self): + def __str__(self) -> str: ''' Creates a string representation of the argument descriptor. This is type and access for scalars with the addition of function space(s) for fields and operators. :returns: string representation of the argument descriptor. - :rtype: str :raises InternalError: if an invalid argument type is passed in. diff --git a/src/psyclone/parse/kernel.py b/src/psyclone/parse/kernel.py index b87c366c8f..489377a91f 100644 --- a/src/psyclone/parse/kernel.py +++ b/src/psyclone/parse/kernel.py @@ -315,7 +315,7 @@ def get_mesh(metadata, valid_mesh_types): :return: the name of the mesh :rtype: string - :raises ParseError: if the supplied meta-data is not a recognised \ + :raises ParseError: if the supplied meta-data is not a recognised mesh identifier. :raises ParseError: if the mesh type is unsupported. @@ -400,6 +400,34 @@ def get_stencil(metadata, valid_types): return {"type": stencil_type, "extent": stencil_extent} +def get_char_value(metadata: expr.NamedArg, + keyword: str) -> str: + ''' + :param metadata: node in fparser1 ast holding the meta-data. + :param keyword: the name of the meta-data entry to extract. + + :returns: a label or int (as a string) representing the value + of the metadata element. + + :raises ParseError: if the supplied metadata doesn't represent + a named argument for the specified keyword. + :raises ParseError: if the value associated with the keyword is + not provided as a string. + + ''' + if (not isinstance(metadata, expr.NamedArg) or + metadata.name.lower() != keyword): + raise ParseError( + f"{metadata} is not a valid {keyword} specifier (expected " + f"{keyword}='label | int')") + if not metadata.is_string: + raise ParseError( + f"The value of {keyword} must be specified as a quoted string " + f"but got {metadata}") + + return metadata.value.lower() + + class Descriptor(): ''' A description of how a kernel argument is accessed, constructed from diff --git a/src/psyclone/tests/domain/lfric/kernel/common_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/common_arg_metadata_test.py index 4298390f2b..abdf0c464d 100644 --- a/src/psyclone/tests/domain/lfric/kernel/common_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/common_arg_metadata_test.py @@ -98,12 +98,13 @@ class works as expected. ''' with pytest.raises(TypeError) as info: _ = CommonArgMetadata.check_fparser2_arg(None, None) - assert ("Expected kernel metadata to be encoded as an fparser2 Part_Ref " - "object but found type 'NoneType' with value 'None'." - in str(info.value)) + assert ("Expected kernel metadata to be encoded as an fparser2 " + "Structure_Constructor object but found type 'NoneType' with " + "value 'None'." in str(info.value)) fparser_tree = CommonArgMetadata.create_fparser2( - "braz_type(GH_FIELD, GH_REAL, GH_READ)", Fortran2003.Part_Ref) + "braz_type(GH_FIELD, GH_REAL, GH_READ)", + Fortran2003.Structure_Constructor) with pytest.raises(ValueError) as info: _ = CommonArgMetadata.check_fparser2_arg(fparser_tree, "arg_type") assert ("Expected kernel metadata to have the name 'arg_type' and be in " @@ -129,3 +130,41 @@ def test_get_arg(): assert CommonArgMetadata.get_arg(fparser_tree, 1) == "GH_REAL" assert CommonArgMetadata.get_arg(fparser_tree, 2) == "GH_READ" assert CommonArgMetadata.get_arg(fparser_tree, 3) is None + + +def test_get_named_arg(): + '''Tests for the get_named_arg() method.''' + test_cls = CommonArgMetadata + fparser_tree = CommonArgMetadata.create_fparser2( + "arg_type(GH_FIELD, GH_REAL, GH_READ)", Fortran2003.Part_Ref) + # No named arguments so should return None + assert test_cls.get_named_arg(fparser_tree, "red") is None + # Named argument with a string value + fparser_tree2 = CommonArgMetadata.create_fparser2( + "arg_type(GH_FIELD, GH_REAL, GH_READ, nlevels='crazy')", + Fortran2003.Component_Spec) + assert test_cls.get_named_arg(fparser_tree2, "red") is None + assert test_cls.get_named_arg(fparser_tree2, "nlevels") == "crazy" + # Named argument with a parameter value + fparser_tree3 = test_cls.create_fparser2( + "arg_type(GH_FIELD, GH_REAL, GH_READ, mesh=GH_FINE)", + Fortran2003.Component_Spec) + assert test_cls.get_named_arg(fparser_tree3, "mesh") == "GH_FINE" + + +def test_validate_named_args(): + '''Test that the _validate_named_args() method behaves as expected.''' + fparser_tree = CommonArgMetadata.create_fparser2( + "arg_type(GH_FIELD, GH_REAL, GH_READ)", Fortran2003.Part_Ref) + # No named arguments and no valid names should be fine. + CommonArgMetadata._validate_named_args(fparser_tree, []) + # No named arguments but with list of valid names should be fine. + CommonArgMetadata._validate_named_args(fparser_tree, ["blue"]) + fparser_tree2 = CommonArgMetadata.create_fparser2( + "arg_type(GH_FIELD, GH_REAL, GH_READ, MESH_arg=GH_FINE)", + Fortran2003.Structure_Constructor) + CommonArgMetadata._validate_named_args(fparser_tree2, ["mesh_arg"]) + with pytest.raises(ValueError) as err: + CommonArgMetadata._validate_named_args(fparser_tree2, ["mesh"]) + assert ("metadata contains keyword argument 'mesh_arg' which is not one " + "of the valid options: ['mesh']" in str(err.value)) diff --git a/src/psyclone/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py index c27dd55525..630353eecc 100644 --- a/src/psyclone/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py @@ -134,9 +134,9 @@ def test_create_from_fparser2(): # _get_metadata called with pytest.raises(TypeError) as info: ScalarArgMetadata.create_from_fparser2(None) - assert ("Expected kernel metadata to be encoded as an fparser2 Part_Ref " - "object but found type 'NoneType' with value 'None'." - in str(info.value)) + assert ("Expected kernel metadata to be encoded as an fparser2 " + "Part_Ref object but found type 'NoneType' with " + "value 'None'." in str(info.value)) # check_remaining_args called fparser2_tree = ScalarArgMetadata.create_fparser2( diff --git a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py index 7a2b13ebf7..d5f6823961 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py @@ -58,6 +58,8 @@ def test_create(datatype, access, function_space): assert field_arg._access == "gh_read" assert field_arg._function_space == "w0" assert field_arg._stencil is None + assert field_arg.ndata == "1" + assert field_arg.nlevels is None def test_create_stencil(): @@ -72,6 +74,30 @@ def test_create_stencil(): assert field_arg._access == "gh_read" assert field_arg._function_space == "w0" assert field_arg._stencil == "cross" + assert field_arg.nlevels is None + assert field_arg.ndata == "1" + + +def test_create_nlevels_ndata(): + '''Test that an instance of FieldArgMetadata can be created successfully + with optional ndata and nlevels metadata. + + ''' + fld_arg = FieldArgMetadata("gh_real", "gh_write", "w0", nlevels="1", + stencil="cross") + assert isinstance(fld_arg, FieldArgMetadata) + assert fld_arg.form == "gh_field" + assert fld_arg._datatype == "gh_real" + assert fld_arg._access == "gh_write" + assert fld_arg._function_space == "w0" + assert fld_arg._stencil == "cross" + assert fld_arg.nlevels == "1" + assert fld_arg.ndata == "1" + fld_arg2 = FieldArgMetadata("gh_real", "gh_write", "w0", nlevels="ustar", + stencil="cross", ndata="big_phys") + assert fld_arg2._stencil == "cross" + assert fld_arg2.nlevels == "ustar" + assert fld_arg2.ndata == "big_phys" def test_init_invalid_fs(): @@ -97,22 +123,28 @@ def test_init_invalid_stencil(): @pytest.mark.parametrize( - "metadata,expected_stencil", - [("arg_type(GH_FIELD, GH_REAL, GH_READ, W0)", None), - ("arg_type(GH_FIELD, GH_REAL, GH_READ, W0, stencil(region))", "region")]) -def test_get_metadata(metadata, expected_stencil): + "metadata,expected_stencil,expected_nlevels,expected_ndata", + [("arg_type(GH_FIELD, GH_REAL, GH_READ, W0)", None, None, None), + ("arg_type(GH_FIELD, GH_REAL, GH_READ, W0, stencil(region))", + "region", None, None), + ('arg_type(GH_FIELD, GH_REAL, GH_READ, W0, nlevels="big")', + None, "big", None)]) +def test_get_metadata(metadata, expected_stencil, expected_nlevels, + expected_ndata): '''Test that the _get_metadata class method works as expected, with and without optional stencil metadata. ''' - fparser2_tree = FieldArgMetadata.create_fparser2( - metadata, Fortran2003.Part_Ref) - datatype, access, function_space, stencil = FieldArgMetadata._get_metadata( - fparser2_tree) + encoding = Fortran2003.Structure_Constructor + fparser2_tree = FieldArgMetadata.create_fparser2(metadata, encoding) + (datatype, access, function_space, stencil, + nlevels, ndata) = FieldArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert stencil == expected_stencil + assert nlevels == expected_nlevels + assert ndata == expected_ndata def test_get_stencil(): @@ -150,7 +182,9 @@ def test_get_stencil(): @pytest.mark.parametrize("fortran_string", [ "arg_type(GH_FIELD, GH_REAL, GH_READ, W0)", - "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, STENCIL(REGION))"]) + "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, STENCIL(REGION))", + "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, NDATA='THREE')", + "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, NLEVELS='4')"]) def test_fortran_string(fortran_string): '''Test that the fortran_string method works as expected. Test with and without a stencil. diff --git a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py index a225b3ad8d..4e3e492682 100644 --- a/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/field_vector_arg_metadata_test.py @@ -106,14 +106,17 @@ def test_init_invalid_stencil(): def test_get_metadata(): '''Test that the get_metadata class method works as expected.''' fparser2_tree = FieldVectorArgMetadata.create_fparser2( - "arg_type(GH_FIELD*3, GH_REAL, GH_READ, W0)", Fortran2003.Part_Ref) - datatype, access, function_space, vector_length, stencil = \ - FieldVectorArgMetadata._get_metadata(fparser2_tree) + "arg_type(GH_FIELD*3, GH_REAL, GH_READ, W0)", + Fortran2003.Structure_Constructor) + (datatype, access, function_space, vector_length, stencil, + nlevels, ndata) = FieldVectorArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert vector_length == "3" assert stencil is None + assert nlevels is None + assert ndata is None @pytest.mark.parametrize("fortran_string", [ diff --git a/src/psyclone/tests/domain/lfric/kernel/inter_grid_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/inter_grid_arg_metadata_test.py index 6ff4dcb346..f80766a4df 100644 --- a/src/psyclone/tests/domain/lfric/kernel/inter_grid_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/inter_grid_arg_metadata_test.py @@ -86,13 +86,15 @@ def test_get_metadata(): metadata = "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, mesh_arg=GH_COARSE)" fparser2_tree = InterGridArgMetadata.create_fparser2( metadata, encoding=Fortran2003.Structure_Constructor) - datatype, access, function_space, mesh_arg, stencil = \ + datatype, access, function_space, mesh_arg, stencil, nlevels, ndata = \ InterGridArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert mesh_arg == "GH_COARSE" assert stencil is None + assert nlevels is None + assert ndata is None def test_get_metadata_stencil(): @@ -104,49 +106,42 @@ def test_get_metadata_stencil(): "mesh_arg=GH_COARSE)") fparser2_tree = InterGridArgMetadata.create_fparser2( metadata, encoding=Fortran2003.Structure_Constructor) - datatype, access, function_space, mesh_arg, stencil = \ + datatype, access, function_space, mesh_arg, stencil, nlevels, ndata = \ InterGridArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert mesh_arg == "GH_COARSE" assert stencil == "region" + assert nlevels is None + assert ndata is None def test_get_mesh_arg(): - '''Test that the get_mesh_arg method works as expected. Also check - that it raises the expected error when the metadata is invalid. + '''Test that the get_named_arg method works as expected for "mesh_arg". + Also check that it raises the expected error when the metadata is invalid. ''' fparser2_tree = InterGridArgMetadata.create_fparser2( "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, mesh_arg=GH_COARSE)", encoding=Fortran2003.Structure_Constructor) - mesh_arg = InterGridArgMetadata.get_mesh_arg(fparser2_tree, 4) + mesh_arg = InterGridArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") assert mesh_arg == "GH_COARSE" # Test when metadata is not in the expected 'mesh_arg = value' - # form. For simplicity, just choose the wrong argument index for - # the existing valid metadata. - with pytest.raises(ValueError) as info: - _ = InterGridArgMetadata.get_mesh_arg(fparser2_tree, 3) - assert ("At argument index 3 for metadata 'arg_type(GH_FIELD, GH_REAL, " - "GH_READ, W0, mesh_arg = GH_COARSE)' expected the metadata to be " - "in the form 'mesh_arg=value' but found 'W0'." in str(info.value)) - + # form - this will just mean that mesh_arg isn't found. Validation + # of keyword arguments happens elsewhere. fparser2_tree = InterGridArgMetadata.create_fparser2( "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, invalid=GH_COARSE)", encoding=Fortran2003.Structure_Constructor) - with pytest.raises(ValueError) as info: - _ = InterGridArgMetadata.get_mesh_arg(fparser2_tree, 4) - assert ("At argument index 4 for metadata 'arg_type(GH_FIELD, GH_REAL, " - "GH_READ, W0, invalid = GH_COARSE)' expected the left hand side " - "to be 'mesh_arg' but found 'invalid'." in str(info.value)) + assert (InterGridArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") + is None) @pytest.mark.parametrize("fortran_string", [ "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, mesh_arg=GH_FINE)", "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, STENCIL(X1D), " - "mesh_arg=GH_FINE)"]) + "nlevels='3', mesh_arg=GH_FINE)"]) def test_fortran_string(fortran_string): '''Test that the fortran_string method works as expected. Test with and without a stencil. diff --git a/src/psyclone/tests/domain/lfric/kernel/inter_grid_vector_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/inter_grid_vector_arg_metadata_test.py index cad3676d51..d5ca026502 100644 --- a/src/psyclone/tests/domain/lfric/kernel/inter_grid_vector_arg_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/inter_grid_vector_arg_metadata_test.py @@ -89,14 +89,16 @@ def test_get_metadata(): metadata = "arg_type(GH_FIELD*3, GH_REAL, GH_READ, W0, mesh_arg=GH_COARSE)" fparser2_tree = InterGridVectorArgMetadata.create_fparser2( metadata, encoding=Fortran2003.Structure_Constructor) - datatype, access, function_space, mesh_arg, vector_length, stencil = \ - InterGridVectorArgMetadata._get_metadata(fparser2_tree) + (datatype, access, function_space, mesh_arg, vector_length, stencil, + nlevels, ndata) = InterGridVectorArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert mesh_arg == "GH_COARSE" assert vector_length == "3" assert stencil is None + assert nlevels is None + assert ndata is None def test_get_metadata_stencil(): @@ -108,14 +110,16 @@ def test_get_metadata_stencil(): "mesh_arg=GH_COARSE)") fparser2_tree = InterGridVectorArgMetadata.create_fparser2( metadata, encoding=Fortran2003.Structure_Constructor) - datatype, access, function_space, mesh_arg, vector_length, stencil = \ - InterGridVectorArgMetadata._get_metadata(fparser2_tree) + (datatype, access, function_space, mesh_arg, vector_length, stencil, + nlevels, ndata) = InterGridVectorArgMetadata._get_metadata(fparser2_tree) assert datatype == "GH_REAL" assert access == "GH_READ" assert function_space == "W0" assert mesh_arg == "GH_COARSE" assert vector_length == "3" assert stencil == "xory1d" + assert nlevels is None + assert ndata is None @pytest.mark.parametrize("fortran_string", [ diff --git a/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py b/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py index e8055978e4..4e9d656670 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py @@ -43,7 +43,6 @@ import os import pytest -import fparser from fparser import api as fpapi from psyclone.core.access_type import AccessType from psyclone.domain.lfric import (LFRicArgDescriptor, LFRicConstants, @@ -91,12 +90,6 @@ ''' -@pytest.fixture(name="disable_fparser_logging", scope="function", autouse=True) -def disable_fparser_logging_fixture(): - '''Fixture to automate disabling of fparser logging.''' - fparser.logging.disable(fparser.logging.CRITICAL) - - def test_ad_fld_type_1st_arg(): ''' Tests that an error is raised when the first argument descriptor metadata for a field is invalid. ''' @@ -173,15 +166,15 @@ def test_ad_fld_type_too_few_args(): def test_ad_fld_type_too_many_args(): ''' Tests that an error is raised when the field argument descriptor - metadata has more than 4 args. ''' + metadata has more than 7 args. ''' code = FIELD_CODE.replace( "arg_type(gh_field, gh_real, gh_inc, w1)", - "arg_type(gh_field, gh_real, gh_inc, w1, w1, w2)", 1) + "arg_type(gh_field, gh_real, gh_inc, w1, w1, w2, w3, w3)", 1) ast = fpapi.parse(code, ignore_comments=False) name = "testkern_field_type" with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast, name=name) - assert ("each 'meta_arg' entry must have at most 5 arguments if its " + assert ("each 'meta_arg' entry must have at most 7 arguments if its " "first argument is of ['gh_field'] type" in str(excinfo.value)) @@ -266,7 +259,7 @@ def test_arg_descriptor_invalid_fs(): def test_ad_field_init_wrong_iteration_space(): ''' Test that an error is raised if a wrong iteration space - (other than ['cell_column', 'dof']) is passed to the + (other than ['cell_column', 'dof', ...]) is passed to the LFRicArgDescriptor._init_field() method. ''' @@ -397,6 +390,44 @@ def test_arg_descriptor_field(): assert field_descriptor.mesh is None assert field_descriptor.stencil is None assert field_descriptor.vector_size == 1 + assert field_descriptor.nlevels is None + assert field_descriptor.ndata == "1" + + +def test_fld_nlevels(): + ''' + Test a field argument with the optional 'nlevels' metatadata. + ''' + code = FIELD_CODE.replace( + "arg_type(gh_scalar, gh_integer, gh_read)", + "arg_type(gh_field, gh_real, gh_read, w3, nlevels='double')", 1) + ast = fpapi.parse(code, ignore_comments=False) + name = "testkern_field_type" + mdata = LFRicKernMetadata(ast, name=name) + # By default, nlevels is left as None. + field_descriptor = mdata.arg_descriptors[5] + assert field_descriptor.nlevels is None + # The seventh argument has nlevels specified as "double" + field_descriptor = mdata.arg_descriptors[6] + assert field_descriptor.nlevels == "double" + + +def test_fld_ndata(): + ''' + Test a field argument with the optional 'ndata' metatadata. + ''' + code = FIELD_CODE.replace( + "arg_type(gh_scalar, gh_integer, gh_read)", + "arg_type(gh_field, gh_real, gh_read, w3, ndata='2')", 1) + ast = fpapi.parse(code, ignore_comments=False) + name = "testkern_field_type" + mdata = LFRicKernMetadata(ast, name=name) + # By default, ndata is 1. + field_descriptor = mdata.arg_descriptors[5] + assert field_descriptor.ndata == "1" + # The seventh argument has ndata specified as "2" + field_descriptor = mdata.arg_descriptors[6] + assert field_descriptor.ndata == "2" def test_invalid_vector_operator(): diff --git a/src/psyclone/tests/domain/lfric/lfric_stencil_test.py b/src/psyclone/tests/domain/lfric/lfric_stencil_test.py index 8daf66e1b0..dc4234a530 100644 --- a/src/psyclone/tests/domain/lfric/lfric_stencil_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_stencil_test.py @@ -102,18 +102,18 @@ def test_stencil_metadata(): def test_stencil_field_metadata_too_many_arguments(): - ''' Check that we raise an exception if more than 5 arguments + ''' Check that we raise an exception if more than 7 arguments are provided in the metadata for a 'gh_field' argument type with stencil access. ''' result = STENCIL_CODE.replace( "(gh_field, gh_real, gh_read, w2, stencil(cross))", - "(gh_field, gh_real, gh_read, w2, stencil(cross), w1)", 1) + "(gh_field, gh_real, gh_read, w2, stencil(cross), w1, w1, w2)", 1) ast = fpapi.parse(result, ignore_comments=False) with pytest.raises(ParseError) as excinfo: _ = LFRicKernMetadata(ast) - assert ("each 'meta_arg' entry must have at most 5 arguments" in + assert ("each 'meta_arg' entry must have at most 7 arguments" in str(excinfo.value)) diff --git a/src/psyclone/tests/parse/kernel_test.py b/src/psyclone/tests/parse/kernel_test.py index 6435e1c88d..d38606e3d3 100644 --- a/src/psyclone/tests/parse/kernel_test.py +++ b/src/psyclone/tests/parse/kernel_test.py @@ -46,11 +46,13 @@ from fparser.one.block_statements import BeginSource from fparser.two import Fortran2003 from psyclone.domain.lfric.lfric_builtins import BUILTIN_MAP as builtins -from psyclone.domain.lfric.lfric_builtins import \ - BUILTIN_DEFINITIONS_FILE as fname -from psyclone.parse.kernel import KernelType, get_kernel_metadata, \ - get_kernel_interface, KernelProcedure, Descriptor, \ - BuiltInKernelTypeFactory, get_kernel_filepath, get_kernel_ast +from psyclone.domain.lfric.lfric_builtins import ( + BUILTIN_DEFINITIONS_FILE as fname) +from psyclone.expression import ExpressionNode, FunctionVar, NamedArg +from psyclone.parse.kernel import ( + KernelType, get_kernel_metadata, get_kernel_interface, KernelProcedure, + Descriptor, BuiltInKernelTypeFactory, get_kernel_filepath, get_kernel_ast, + get_char_value, get_stencil) from psyclone.parse.utils import ParseError from psyclone.errors import InternalError @@ -687,6 +689,48 @@ def test_get_integer_variable(): assert tmp.get_integer_variable("Gh_Shape") == "gh_quadrature_face" +def test_get_stencil(): + ''' Check that parse.get_stencil() raises the correct errors when + passed various incorrect inputs. ''' + enode = ExpressionNode(["1"]) + with pytest.raises(ParseError) as excinfo: + _ = get_stencil(enode, ["cross"]) + assert ("Expecting format stencil([,]) but found the " + "literal" in str(excinfo.value)) + node = FunctionVar(["stencil()"]) + with pytest.raises(ParseError) as excinfo: + _ = get_stencil(node, ["cross"]) + assert ("Expecting format stencil([,]) but found stencil()" + in str(excinfo.value)) + node = FunctionVar(["stencil", "cross"]) + # Deliberately break the args member of node in order to trigger an + # internal error + node.args = [True] + with pytest.raises(ParseError) as excinfo: + _ = get_stencil(node, ["cross"]) + assert ("expecting either FunctionVar or str from the expression analyser" + in str(excinfo.value)) + + +def test_get_char_value(): + ''' + Tests for the get_char_value() routine. + ''' + enode = ExpressionNode(["1"]) + with pytest.raises(ParseError) as err: + _ = get_char_value(enode, "nlevels") + assert "not a valid nlevels specifier (expected" in str(err.value) + # Value must be a quoted string + node = NamedArg(["nlevels", "=", "1"]) + with pytest.raises(ParseError) as err: + _ = get_char_value(node, "nlevels") + assert ("nlevels must be specified as a quoted string but got nlevels=1" + in str(err.value)) + node2 = NamedArg(["nlevels", "=", "'1'"]) + value = get_char_value(node2, "nlevels") + assert value == "1" + + def test_get_integer_variable_err(): '''Tests that we raise the expected error if the meta-data contains an integer literal instead of a name. diff --git a/src/psyclone/tests/parse/parse_test.py b/src/psyclone/tests/parse/parse_test.py index 8e9c1575b0..3789ced15b 100644 --- a/src/psyclone/tests/parse/parse_test.py +++ b/src/psyclone/tests/parse/parse_test.py @@ -208,31 +208,6 @@ def test_duplicate_named_invoke_case(): assert "3.4_multi_invoke_name_clash_case_insensitive.f90" in str(err.value) -def test_get_stencil(): - ''' Check that parse.get_stencil() raises the correct errors when - passed various incorrect inputs. ''' - from psyclone.parse.kernel import get_stencil - from psyclone.expression import ExpressionNode, FunctionVar - enode = ExpressionNode(["1"]) - with pytest.raises(ParseError) as excinfo: - _ = get_stencil(enode, ["cross"]) - assert ("Expecting format stencil([,]) but found the " - "literal" in str(excinfo.value)) - node = FunctionVar(["stencil()"]) - with pytest.raises(ParseError) as excinfo: - _ = get_stencil(node, ["cross"]) - assert ("Expecting format stencil([,]) but found stencil()" - in str(excinfo.value)) - node = FunctionVar(["stencil", "cross"]) - # Deliberately break the args member of node in order to trigger an - # internal error - node.args = [True] - with pytest.raises(ParseError) as excinfo: - _ = get_stencil(node, ["cross"]) - assert ("expecting either FunctionVar or str from the expression analyser" - in str(excinfo.value)) - - MDATA = ''' module testkern_eval_mod type, extends(kernel_type) :: testkern_eval_type