From 234a26a9cd5287289292e105ce3b3be5c0233c20 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 21 Oct 2025 16:15:25 +0100 Subject: [PATCH 01/22] #868 add text describing nlevels and ndata --- doc/user_guide/lfric.rst | 55 ++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index e449270675..85584d6f24 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -162,24 +162,30 @@ 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. +Typically, a field will have the same number of vertical layers as the +model mesh. However, this is not a requirement and the number of layers +can be as few as one (a 2D field). + .. _lfric-field-vector: Field Vector @@ -919,8 +925,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. @@ -1718,7 +1724,6 @@ be found in ``examples/lfric/eg5``. 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 required for inter-grid kernels which perform prolongation or @@ -1748,6 +1753,36 @@ 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 argument to a kernel has a number of vertical levels +that is not the same as the extruded mesh then this must be specified using +the ``NLEVELS`` option to GH_FIELD, e.g.:: + + arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLEVELS=1) + +The value specified for ``NLEVELS`` may be a literal if it is known at +compile time. Alternatively, it may be given the special value +``GH_RUNTIME`` which means that the number of levels is to be determined +at runtime (in the generated PSy layer). + + +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 specified for ``NDATA`` may be a literal if it is known at +compile time. Alternatively, it may be given the special value +``GH_RUNTIME`` which means that the number of data values at each DoF is to be +determined at runtime (in the generated PSy layer). + + Column-wise Operators (CMA) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 060a1ea9ace723971e86454bf4064a3679c630e0 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 21 Oct 2025 17:10:12 +0100 Subject: [PATCH 02/22] #868 update the rules for kernel arguments --- doc/user_guide/lfric.rst | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 85584d6f24..ddef8e2cb8 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -1642,7 +1642,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 @@ -1722,7 +1722,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 @@ -1754,7 +1754,7 @@ mesh must also be on the same function space. Number of Layers Metadata -------------------------- +""""""""""""""""""""""""" If a particular field argument to a kernel has a number of vertical levels that is not the same as the extruded mesh then this must be specified using @@ -1769,7 +1769,7 @@ at runtime (in the generated PSy layer). 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 @@ -2115,7 +2115,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 @@ -2141,21 +2146,26 @@ conventions, are: the data type and kind specifed 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 +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) + lexicographic order). Note that if a field on a given function space has a + non-standard number of vertical levels, it requires that a dofmap be supplied + (because the number of vertical levels alters 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. From 716c908dcea6cf35d153f1f19eb6d27d9eb37dad Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 22 Oct 2025 12:00:35 +0100 Subject: [PATCH 03/22] #868 change nlevels to nlayers --- doc/user_guide/lfric.rst | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index ddef8e2cb8..5072894dd0 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -1758,14 +1758,18 @@ Number of Layers Metadata If a particular field argument to a kernel has a number of vertical levels that is not the same as the extruded mesh then this must be specified using -the ``NLEVELS`` option to GH_FIELD, e.g.:: +the ``NLAYERS`` option to ``GH_FIELD``, e.g.:: - arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLEVELS=1) + arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS=1) -The value specified for ``NLEVELS`` may be a literal if it is known at +The value specified for ``NLAYERS`` may be a literal if it is known at compile time. Alternatively, it may be given the special value -``GH_RUNTIME`` which means that the number of levels is to be determined -at runtime (in the generated PSy layer). +``GH_RUNTIME`` which means that the number of levels is to be +determined at runtime by querying the field object (in the generated +PSy layer). If two different field arguments are on the same function +space but both have ``NLAYERS=GH_RUNTIME`` then it is assumed that +they may have *different* values of ``NLAYERS`` and hence a separate +dofmap is passed to the kernel for each. Multi-Data Metadata @@ -1779,8 +1783,9 @@ the optional ``NDATA`` argument to GH_FIELD, e.g.:: The value specified for ``NDATA`` may be a literal if it is known at compile time. Alternatively, it may be given the special value -``GH_RUNTIME`` which means that the number of data values at each DoF is to be -determined at runtime (in the generated PSy layer). +``GH_RUNTIME`` which means that the number of data values at each DoF +is to be determined at runtime by querying the field object (in the +generated PSy layer). Column-wise Operators (CMA) @@ -2488,8 +2493,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``. From e878688566a1be274eb7709ad0630da832b2fa3f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 5 Nov 2025 13:18:59 +0000 Subject: [PATCH 04/22] #868 extend docs to allow for naming of nlayers values --- doc/user_guide/lfric.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 5072894dd0..4f04456a63 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -1763,13 +1763,17 @@ the ``NLAYERS`` option to ``GH_FIELD``, e.g.:: arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS=1) The value specified for ``NLAYERS`` may be a literal if it is known at -compile time. Alternatively, it may be given the special value -``GH_RUNTIME`` which means that the number of levels is to be +compile time. Alternatively, it may be given a named value (one of +``GH_NLAYERSM1`` TODO) or the special value +``GH_RUNTIME``. A named value means that the number of levels is to be determined at runtime by querying the field object (in the generated PSy layer). If two different field arguments are on the same function space but both have ``NLAYERS=GH_RUNTIME`` then it is assumed that they may have *different* values of ``NLAYERS`` and hence a separate -dofmap is passed to the kernel for each. +dofmap is passed to the kernel for each. However, if two or more field +arguments are on the same function space and have the same, named number +of layers which is not ``GH_RUNTIME`` then only one dofmap is passed to +the kernel for those arguments. Multi-Data Metadata @@ -2155,10 +2159,10 @@ conventions, are: 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 a field on a given function space has a - non-standard number of vertical levels, it requires that a dofmap be supplied - (because the number of vertical levels alters the *values* within the map). For - each required DoF map: + lexicographic order). Note that if two fields on a given function space have + differing numbers of vertical layers, then each requires that a + dofmap be supplied (because the number of vertical layers alters 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 From 3d4e9b9fa6b0b9b7ecf6cab9eb277e9be0548fce Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 2 Apr 2026 14:50:19 +0100 Subject: [PATCH 05/22] #868 update nlayers docs --- doc/user_guide/lfric.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 587f043af1..9955109cfb 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -182,9 +182,11 @@ primitive type. In the LFRic infrastructure, these fields are represented by instances of the ``field_type`` and ``integer_field_type`` classes, respectively. -Typically, a field will have the same number of vertical layers as the -model mesh. However, this is not a requirement and the number of layers -can be as few as one (a 2D field). +Different fields may be defined on different numbers of vertical layers. +The the number of layers can be as few as one (a 2D field). Unfortunately, +the number of layers affects the numbering of the DoFs of a field. Therefore, +a distinct DoF map is required for each unique combination of function +space and number of vertical levels. .. _lfric-field-vector: @@ -1456,7 +1458,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). @@ -1756,9 +1758,9 @@ mesh must also be on the same function space. Number of Layers Metadata """"""""""""""""""""""""" -If a particular field argument to a kernel has a number of vertical levels -that is not the same as the extruded mesh then this must be specified using -the ``NLAYERS`` option to ``GH_FIELD``, e.g.:: +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``, e.g.:: arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS=1) From 1bfa1af3a04de202d90093a96c1adbd273733c36 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 8 Apr 2026 12:25:36 +0100 Subject: [PATCH 06/22] #868 update the nlayers text to make it clear that named values are just tags --- doc/user_guide/lfric.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 9955109cfb..059e4c865b 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -1760,23 +1760,28 @@ 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``, e.g.:: +this must be specified using the ``NLAYERS`` option to ``GH_FIELD``/ +``GH_OPERATOR``, e.g.:: arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS=1) The value specified for ``NLAYERS`` may be a literal if it is known at -compile time. Alternatively, it may be given a named value (one of -``GH_NLAYERSM1`` TODO) or the special value -``GH_RUNTIME``. A named value means that the number of levels is to be +compile time. Alternatively, it may be given a name (e.g. +``GH_NLAYERS_SHIFTED``) or the special tag ``GH_RUNTIME``. A named value +means that the number of levels is to be determined at runtime by querying the field object (in the generated PSy layer). If two different field arguments are on the same function space but both have ``NLAYERS=GH_RUNTIME`` then it is assumed that they may have *different* values of ``NLAYERS`` and hence a separate dofmap is passed to the kernel for each. However, if two or more field arguments are on the same function space and have the same, named number -of layers which is not ``GH_RUNTIME`` then only one dofmap is passed to -the kernel for those arguments. +of layers which is not ``GH_RUNTIME`` then only one dofmap (that of the +first such field listed in the metadata) is passed to the kernel for +those arguments. +(Since run-time value of ``NLAYERS`` is looked-up from the corresponding +kernel argument, 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 """"""""""""""""""" From a6833a1cc7f249cabf6c246dce3f670d13a6d73a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 8 Apr 2026 15:34:38 +0100 Subject: [PATCH 07/22] #868 add initial code to get nlevels metadata for a field arg --- .../domain/lfric/lfric_arg_descriptor.py | 45 +++++++++++++++---- src/psyclone/parse/kernel.py | 21 +++++++++ .../domain/lfric/lfric_field_mdata_test.py | 25 ++++++++--- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index d1df5712ff..3e9f7100ac 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_nlevels) from psyclone.parse.utils import ParseError # API configuration @@ -109,6 +110,7 @@ def __init__(self, arg_type, operates_on, metadata_index): self._function_space2 = None self._stencil = None self._mesh = None + self._nlevels = "" self._nargs = 0 # Check for the correct argument type descriptor @@ -400,10 +402,11 @@ 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: + num_args = len(arg_type.args) + if num_args > 4: + # The optional 5th argument is either a stencil specification + # or a mesh identifier (for inter-grid kernels) + prop_ind = 4 try: if "stencil" in str(arg_type.args[prop_ind]): self._stencil = get_stencil( @@ -412,15 +415,33 @@ def _init_field(self, arg_type, operates_on): 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_nlevels(arg_type.args[prop_ind]) 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 + f", a number of levels or a mesh identifier (for inter-" + f"grid kernels). However, entry '{arg_type}' raised the " + f"following error: {err}.") from err + + if num_args > 5: + # If there are this many arguments then the last one must be + # nlevels. + prop_ind = 5 + try: + if "nlevels" in str(arg_type.args[prop_ind]): + self._nlevels = get_nlevels(arg_type.args[prop_ind]) + 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 a number of levels. However entry " + f"'{arg_type}' raised the following error: {err}." + ) from err # Test allowed accesses for fields field_disc_accesses = [AccessType.READ, AccessType.WRITE, @@ -794,6 +815,14 @@ def function_spaces(self): raise InternalError(f"Expected a valid argument type but got " f"'{self._argument_type}'.") + @property + def nlevels(self) -> str: + ''' + :returns: a label identifying the number of vertical levels + associated with this argument. + ''' + return self._nlevels + @property def vector_size(self): ''' diff --git a/src/psyclone/parse/kernel.py b/src/psyclone/parse/kernel.py index b87c366c8f..a389bc090c 100644 --- a/src/psyclone/parse/kernel.py +++ b/src/psyclone/parse/kernel.py @@ -400,6 +400,27 @@ def get_stencil(metadata, valid_types): return {"type": stencil_type, "extent": stencil_extent} +def get_nlevels(metadata: expr.NamedArg) -> str: + ''' + Returns the number of levels described by the supplied meta-data + + :param metadata: node in fparser1 ast holding the meta-data. + + :return: a label identifying the number of vertical levels. + + :raises ParseError: if the supplied ast does not correspond to + `nlevels="some-label"`. + ''' + if (not isinstance(metadata, expr.NamedArg) or + metadata.name.lower() != "nlevels"): + raise ParseError( + f"{metadata} is not a valid mesh identifier (expected " + f"nlevels='label')") + mesh = metadata.value.lower() + + return mesh + + class Descriptor(): ''' A description of how a kernel argument is accessed, constructed from 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..1bb2e46408 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. ''' @@ -399,6 +392,24 @@ def test_arg_descriptor_field(): assert field_descriptor.vector_size == 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 an empty string. + field_descriptor = mdata.arg_descriptors[5] + assert field_descriptor.nlevels == "" + # The seventh argument has nlevels specified as "double" + field_descriptor = mdata.arg_descriptors[6] + assert field_descriptor.nlevels == "double" + + def test_invalid_vector_operator(): ''' Tests that an error is raised when a field vector does not use "*" as its operator. ''' From 892245fe71559bcdd860b90a2887104a64f69269 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 10 Apr 2026 12:07:00 +0100 Subject: [PATCH 08/22] #868 update nlayers documentation to remove special gh_runtime tag --- doc/user_guide/lfric.rst | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 059e4c865b..31ab9a1020 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -1765,22 +1765,16 @@ this must be specified using the ``NLAYERS`` option to ``GH_FIELD``/ arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS=1) -The value specified for ``NLAYERS`` may be a literal if it is known at -compile time. Alternatively, it may be given a name (e.g. -``GH_NLAYERS_SHIFTED``) or the special tag ``GH_RUNTIME``. A named value -means that the number of levels is to be -determined at runtime by querying the field object (in the generated -PSy layer). If two different field arguments are on the same function -space but both have ``NLAYERS=GH_RUNTIME`` then it is assumed that -they may have *different* values of ``NLAYERS`` and hence a separate -dofmap is passed to the kernel for each. However, if two or more field -arguments are on the same function space and have the same, named number -of layers which is not ``GH_RUNTIME`` then only one dofmap (that of the +The value specified for ``NLAYERS`` may be an integer literal if it is known +at compile time. Alternatively, it may be given a name (e.g. +``GH_NLAYERS_SHIFTED``). 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 run-time value of ``NLAYERS`` is looked-up from the corresponding -kernel argument, the labels given in the kernel metadata are just that +(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 From 748d38d79549f7b5d35da023857e2b67ad696d1d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 5 Jun 2026 15:03:24 +0100 Subject: [PATCH 09/22] #868 update docs for nlayers and ndata to use string values --- doc/user_guide/lfric.rst | 56 +++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 54352e8d1c..dc1b2e0d99 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -183,10 +183,12 @@ 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 the number of layers can be as few as one (a 2D field). Unfortunately, -the number of layers affects the numbering of the DoFs of a field. Therefore, +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 and number of vertical levels. +space, number of vertical levels and number of data values. .. _lfric-field-vector: @@ -1761,17 +1763,19 @@ 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``, e.g.:: +``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) + arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS="1") -The value specified for ``NLAYERS`` may be an integer literal if it is known -at compile time. Alternatively, it may be given a name (e.g. -``GH_NLAYERS_SHIFTED``). 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. +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 @@ -1784,13 +1788,19 @@ 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) + 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. -The value specified for ``NDATA`` may be a literal if it is known at -compile time. Alternatively, it may be given the special value -``GH_RUNTIME`` which means that the number of data values at each DoF -is to be determined at runtime by querying the field object (in the -generated PSy layer). +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) @@ -2158,11 +2168,11 @@ conventions, are: 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, then each requires that a - dofmap be supplied (because the number of vertical layers alters the + 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) From 66617c4acacf0b087f455b5994e3b8f9f7edb368 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 8 Jun 2026 21:56:19 +0100 Subject: [PATCH 10/22] #868 begin adding support for ndata metadata --- .../domain/lfric/lfric_arg_descriptor.py | 72 +++++++++---------- src/psyclone/parse/kernel.py | 35 +++++++-- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index 3e9f7100ac..b12ef8feba 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -52,7 +52,7 @@ from psyclone.errors import InternalError import psyclone.expression as expr from psyclone.parse.kernel import ( - Descriptor, get_stencil, get_mesh, get_nlevels) + Descriptor, get_stencil, get_mesh, get_ndata, get_nlevels) from psyclone.parse.utils import ParseError # API configuration @@ -404,44 +404,29 @@ def _init_field(self, arg_type, operates_on): num_args = len(arg_type.args) if num_args > 4: - # The optional 5th argument is either a stencil specification - # or a mesh identifier (for inter-grid kernels) - prop_ind = 4 - 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_nlevels(arg_type.args[prop_ind]) - 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", a number of levels or a mesh identifier (for inter-" - f"grid kernels). However, entry '{arg_type}' raised the " - f"following error: {err}.") from err - - if num_args > 5: - # If there are this many arguments then the last one must be - # nlevels. - prop_ind = 5 - try: - if "nlevels" in str(arg_type.args[prop_ind]): - self._nlevels = get_nlevels(arg_type.args[prop_ind]) - 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 a number of levels. However entry " - f"'{arg_type}' raised the following error: {err}." - ) from err + 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_nlevels(arg_type.args[prop_ind]) + elif "ndata" in str(arg_type.args[prop_ind]): + self._ndata = get_ndata(arg_type.args[prop_ind]) + 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, @@ -823,6 +808,15 @@ def nlevels(self) -> str: ''' 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): ''' diff --git a/src/psyclone/parse/kernel.py b/src/psyclone/parse/kernel.py index a389bc090c..a04ae0bff4 100644 --- a/src/psyclone/parse/kernel.py +++ b/src/psyclone/parse/kernel.py @@ -402,23 +402,46 @@ def get_stencil(metadata, valid_types): def get_nlevels(metadata: expr.NamedArg) -> str: ''' - Returns the number of levels described by the supplied meta-data + Returns the number of levels specified by the supplied meta-data :param metadata: node in fparser1 ast holding the meta-data. :return: a label identifying the number of vertical levels. :raises ParseError: if the supplied ast does not correspond to - `nlevels="some-label"`. + `nlevels="some-label | int"`. ''' if (not isinstance(metadata, expr.NamedArg) or metadata.name.lower() != "nlevels"): raise ParseError( - f"{metadata} is not a valid mesh identifier (expected " - f"nlevels='label')") - mesh = metadata.value.lower() + f"{metadata} is not a valid nlevels specifier (expected " + f"nlevels='label | int')") + nlevels = metadata.value.lower() - return mesh + return nlevels + + +def get_ndata(metadata: expr.NamedArg) -> str: + ''' + Returns the number of data values per DoF specified by the supplied + meta-data. + + :param metadata: node in fparser1 ast holding the meta-data. + + :return: a label (or int as a string) specifying the number of data + values per DoF. + + :raises ParseError: if the supplied ast does not correspond to + `ndata="some-label | int"`. + ''' + if (not isinstance(metadata, expr.NamedArg) or + metadata.name.lower() != "ndata"): + raise ParseError( + f"{metadata} is not a valid ndata specifier (expected " + f"ndata='label | int')") + ndata = metadata.value.lower() + + return ndata class Descriptor(): From 7a8c9e9d840f10929fd287ccad0f340786470b54 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 9 Jun 2026 13:04:06 +0100 Subject: [PATCH 11/22] #868 WIP on initial implementation [skip ci] --- src/psyclone/parse/kernel.py | 38 +++++++------ .../domain/lfric/lfric_field_mdata_test.py | 2 + src/psyclone/tests/parse/kernel_test.py | 54 +++++++++++++++++-- src/psyclone/tests/parse/parse_test.py | 25 --------- 4 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/psyclone/parse/kernel.py b/src/psyclone/parse/kernel.py index a04ae0bff4..292c8dd41f 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,24 @@ def get_stencil(metadata, valid_types): return {"type": stencil_type, "extent": stencil_extent} +def _get_char_value(metadata: expr.NamedArg, keyword: str) -> str: + ''' + ''' + 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.value[0] == metadata.value[-1] and + metadata.value[0] in ["'", '"']): + raise ParseError( + f"The value of {keyword} must be specified as a quoted string " + f"but got {metadata}") + result = metadata.value[1:-2].lower() + + return result + + def get_nlevels(metadata: expr.NamedArg) -> str: ''' Returns the number of levels specified by the supplied meta-data @@ -411,14 +429,7 @@ def get_nlevels(metadata: expr.NamedArg) -> str: :raises ParseError: if the supplied ast does not correspond to `nlevels="some-label | int"`. ''' - if (not isinstance(metadata, expr.NamedArg) or - metadata.name.lower() != "nlevels"): - raise ParseError( - f"{metadata} is not a valid nlevels specifier (expected " - f"nlevels='label | int')") - nlevels = metadata.value.lower() - - return nlevels + return _get_char_value(metadata, "nlevels") def get_ndata(metadata: expr.NamedArg) -> str: @@ -434,14 +445,7 @@ def get_ndata(metadata: expr.NamedArg) -> str: :raises ParseError: if the supplied ast does not correspond to `ndata="some-label | int"`. ''' - if (not isinstance(metadata, expr.NamedArg) or - metadata.name.lower() != "ndata"): - raise ParseError( - f"{metadata} is not a valid ndata specifier (expected " - f"ndata='label | int')") - ndata = metadata.value.lower() - - return ndata + return _get_char_value(metadata, "ndata") class Descriptor(): 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 1bb2e46408..5e0a4bb986 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py @@ -390,6 +390,8 @@ 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(): diff --git a/src/psyclone/tests/parse/kernel_test.py b/src/psyclone/tests/parse/kernel_test.py index 6435e1c88d..cf0a5ae9b3 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_ndata, get_nlevels, 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_nlevels(): + ''' + Tests for the get_nlevels() routine. + ''' + enode = ExpressionNode(["1"]) + with pytest.raises(ParseError) as err: + _ = get_nlevels(enode) + 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_nlevels(node) + assert ("nlevels must be specified as a quoted string but got nlevels=1" + in str(err.value)) + node2 = NamedArg(["nlevels", "=", "'1'"]) + value = get_nlevels(node2) + 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 From 707db35a46e152f8261d82207561f5e9a0c2f174 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 9 Jun 2026 14:16:04 +0100 Subject: [PATCH 12/22] #868 tidy implementation --- .../domain/lfric/lfric_arg_descriptor.py | 14 ++++-- src/psyclone/parse/kernel.py | 50 ++++++------------- .../domain/lfric/lfric_field_mdata_test.py | 4 +- src/psyclone/tests/parse/kernel_test.py | 12 ++--- 4 files changed, 33 insertions(+), 47 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index b12ef8feba..60eed8cd6c 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -52,7 +52,7 @@ from psyclone.errors import InternalError import psyclone.expression as expr from psyclone.parse.kernel import ( - Descriptor, get_stencil, get_mesh, get_ndata, get_nlevels) + Descriptor, get_stencil, get_mesh, get_char_value) from psyclone.parse.utils import ParseError # API configuration @@ -110,7 +110,11 @@ def __init__(self, arg_type, operates_on, metadata_index): self._function_space2 = None self._stencil = None self._mesh = None - self._nlevels = "" + # 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 @@ -414,9 +418,11 @@ def _init_field(self, arg_type, operates_on): 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_nlevels(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_ndata(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: diff --git a/src/psyclone/parse/kernel.py b/src/psyclone/parse/kernel.py index 292c8dd41f..489377a91f 100644 --- a/src/psyclone/parse/kernel.py +++ b/src/psyclone/parse/kernel.py @@ -400,52 +400,32 @@ def get_stencil(metadata, valid_types): return {"type": stencil_type, "extent": stencil_extent} -def _get_char_value(metadata: expr.NamedArg, keyword: str) -> str: +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.value[0] == metadata.value[-1] and - metadata.value[0] in ["'", '"']): + if not metadata.is_string: raise ParseError( f"The value of {keyword} must be specified as a quoted string " f"but got {metadata}") - result = metadata.value[1:-2].lower() - - return result - - -def get_nlevels(metadata: expr.NamedArg) -> str: - ''' - Returns the number of levels specified by the supplied meta-data - - :param metadata: node in fparser1 ast holding the meta-data. - :return: a label identifying the number of vertical levels. - - :raises ParseError: if the supplied ast does not correspond to - `nlevels="some-label | int"`. - ''' - return _get_char_value(metadata, "nlevels") - - -def get_ndata(metadata: expr.NamedArg) -> str: - ''' - Returns the number of data values per DoF specified by the supplied - meta-data. - - :param metadata: node in fparser1 ast holding the meta-data. - - :return: a label (or int as a string) specifying the number of data - values per DoF. - - :raises ParseError: if the supplied ast does not correspond to - `ndata="some-label | int"`. - ''' - return _get_char_value(metadata, "ndata") + return metadata.value.lower() class Descriptor(): 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 5e0a4bb986..2b30ef24ae 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py @@ -404,9 +404,9 @@ def test_fld_nlevels(): ast = fpapi.parse(code, ignore_comments=False) name = "testkern_field_type" mdata = LFRicKernMetadata(ast, name=name) - # By default, nlevels is left as an empty string. + # By default, nlevels is left as None. field_descriptor = mdata.arg_descriptors[5] - assert field_descriptor.nlevels == "" + 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" diff --git a/src/psyclone/tests/parse/kernel_test.py b/src/psyclone/tests/parse/kernel_test.py index cf0a5ae9b3..d38606e3d3 100644 --- a/src/psyclone/tests/parse/kernel_test.py +++ b/src/psyclone/tests/parse/kernel_test.py @@ -52,7 +52,7 @@ from psyclone.parse.kernel import ( KernelType, get_kernel_metadata, get_kernel_interface, KernelProcedure, Descriptor, BuiltInKernelTypeFactory, get_kernel_filepath, get_kernel_ast, - get_ndata, get_nlevels, get_stencil) + get_char_value, get_stencil) from psyclone.parse.utils import ParseError from psyclone.errors import InternalError @@ -712,22 +712,22 @@ def test_get_stencil(): in str(excinfo.value)) -def test_get_nlevels(): +def test_get_char_value(): ''' - Tests for the get_nlevels() routine. + Tests for the get_char_value() routine. ''' enode = ExpressionNode(["1"]) with pytest.raises(ParseError) as err: - _ = get_nlevels(enode) + _ = 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_nlevels(node) + _ = 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_nlevels(node2) + value = get_char_value(node2, "nlevels") assert value == "1" From 268985b2ce203643f490eefae4539d2f731a3d93 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 9 Jun 2026 14:59:25 +0100 Subject: [PATCH 13/22] #868 WIP updating psyir-metadata handling classes [skip ci] --- .../lfric/kernel/common_arg_metadata.py | 11 +++++++ .../domain/lfric/kernel/field_arg_metadata.py | 29 +++++++++++++------ .../lfric/kernel/field_vector_arg_metadata.py | 29 ++++++++++++------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index f2175145b5..2fa9003eb0 100644 --- a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py @@ -150,5 +150,16 @@ def get_arg(fparser2_tree, index): # Metadata at the specified index does not exist. return None + @staticmethod + def get_named_arg(fparser2_tree, name: str): + ''' + TODO + ''' + for child in fparser2_tree.children[1].children: + if child.children and child.children[0].tostr() == name: + return child.children[1].tostr() + + return None + __all__ = ["CommonArgMetadata"] diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index 815c7facb6..bba082bdcb 100644 --- a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py @@ -68,11 +68,13 @@ 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): + def __init__(self, datatype, access, function_space, stencil=None, + nlevels=None, + ndata=1): super().__init__(datatype, access) self.function_space = function_space self.stencil = stencil @@ -84,21 +86,24 @@ def _get_metadata(cls, fparser2_tree): 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` | \ + :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. + :rtype: Tuple[str, str, str, + Optional[str], Optional[str], Optional[str]] ''' 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) + 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): @@ -128,6 +133,12 @@ def get_stencil(cls, fparser2_tree): stencil = raw_stencil_text[8:-1] return stencil + @classmethod + def get_nlevels(cls, fparser2_tree): + ''' + ''' + return cls.get_named_arg(fparser2_tree, "nlevels") + def fortran_string(self): ''' :returns: the metadata represented by this class as Fortran. 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): ''' From 4b8ea0802eea17779be7b36abc7fa2aec7f21c3f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Jun 2026 14:17:25 +0100 Subject: [PATCH 14/22] #868 WIP extending metadata handling [skip ci] --- .../lfric/kernel/common_arg_metadata.py | 43 ++++++++++++--- .../domain/lfric/kernel/common_metadata.py | 17 +++--- .../domain/lfric/kernel/field_arg_metadata.py | 55 ++++++++++++------- .../lfric/kernel/inter_grid_arg_metadata.py | 51 +++++++++-------- .../lfric/kernel/common_arg_metadata_test.py | 9 +-- .../kernel/common_meta_arg_metadata_test.py | 11 ++-- .../lfric/kernel/field_arg_metadata_test.py | 22 +++++--- .../kernel/field_vector_arg_metadata_test.py | 9 ++- 8 files changed, 131 insertions(+), 86 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index 2fa9003eb0..9a369c5f00 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 from fparser.two import Fortran2003 +from fparser.two.utils import walk as fp_walk 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): @@ -103,11 +105,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(...). ''' @@ -151,15 +153,38 @@ def get_arg(fparser2_tree, index): return None @staticmethod - def get_named_arg(fparser2_tree, name: str): - ''' - TODO + def get_named_arg(fparser2_tree: Fortran2003.Component_Spec, + name: str) -> Optional[str]: ''' - for child in fparser2_tree.children[1].children: - if child.children and child.children[0].tostr() == name: - return child.children[1].tostr() + Searches the supplied metadata for 'name=value' expressions and + returns the value corresponding to the supplied name if found. + Otherwise returns None. + + :param fparser2_tree: the parse tree of the metadata element. + :param name: the name of the metadata element that we want. + :returns: the value of the named metadata element or None. + + ''' + for child in fp_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] + return text return None + @staticmethod + def _validate_keyword_args(fparser2_tree: Fortran2003.Component_Spec, + valid_names: list[str]) -> None: + ''' + ''' + for child in fp_walk(fparser2_tree, Fortran2003.Component_Spec): + if child.children[0].tostr().lower() not in valid_names: + raise ValueError("TODO") + __all__ = ["CommonArgMetadata"] diff --git a/src/psyclone/domain/lfric/kernel/common_metadata.py b/src/psyclone/domain/lfric/kernel/common_metadata.py index 724f639a13..4b60b942ca 100644 --- a/src/psyclone/domain/lfric/kernel/common_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_metadata.py @@ -40,6 +40,7 @@ from abc import ABC, abstractmethod 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 +100,8 @@ 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: 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,18 +110,13 @@ 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 \ + :param 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` - :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 diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index bba082bdcb..dd17a0378a 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 @@ -72,15 +74,21 @@ class FieldArgMetadata(ScalarArgMetadata): # and max values). nargs = (4, 5) - def __init__(self, datatype, access, function_space, stencil=None, - nlevels=None, - ndata=1): + def __init__(self, datatype: str, access: str, function_space: str, + stencil: Optional[str] = None, + nlevels=None, ndata=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 @@ -88,43 +96,48 @@ def _get_metadata(cls, fparser2_tree): :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, stencil, nlevels and ndata metadata. - :rtype: Tuple[str, str, str, - Optional[str], Optional[str], Optional[str]] ''' 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) + super()._validate_keyword_args(fparser2_tree, + ["nlevels", "ndata", "mesh"]) 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): 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..b4e50693df 100644 --- a/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py @@ -114,42 +114,41 @@ def _get_metadata(cls, fparser2_tree): return (datatype, access, function_space, mesh_arg, stencil) @staticmethod - def get_mesh_arg(fparser2_tree, mesh_arg_index): + def get_mesh_arg(fparser2_tree: Fortran2003.Structure_Constructor, + mesh_arg_index: int) -> str: '''Retrieves the mesh_arg metadata value from the supplied fparser2 tree. - :param fparser2_tree: fparser2 tree capturing the metadata for \ + :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. + :param 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 \ + raises ValueError: if the metadata is not in the form 'mesh_arg = '. ''' - 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 + return FieldArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") + #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): ''' 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..89f5b30477 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 " 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..96e59c5d9b 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,13 +134,13 @@ 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 " + "Structure_Constructor object but found type 'NoneType' with " + "value 'None'." in str(info.value)) # check_remaining_args called fparser2_tree = ScalarArgMetadata.create_fparser2( - "hello(x)", Fortran2003.Part_Ref) + "hello(x)", Fortran2003.Structure_Constructor) with pytest.raises(ValueError) as info: _ = ScalarArgMetadata.create_from_fparser2(fparser2_tree) @@ -150,7 +150,8 @@ def test_create_from_fparser2(): # expected class returned fparser2_tree = ScalarArgMetadata.create_fparser2( - "arg_type(GH_SCALAR, GH_REAL, GH_READ)", Fortran2003.Part_Ref) + "arg_type(GH_SCALAR, GH_REAL, GH_READ)", + Fortran2003.Structure_Constructor) obj = ScalarArgMetadata.create_from_fparser2(fparser2_tree) assert isinstance(obj, ScalarArgMetadata) assert obj.form == "gh_scalar" 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..848a68b09d 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 @@ -97,22 +97,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, "", ""), + ("arg_type(GH_FIELD, GH_REAL, GH_READ, W0, stencil(region))", + "region", "", ""), + ('arg_type(GH_FIELD, GH_REAL, GH_READ, W0, nlevels="big")', + None, "big", "")]) +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(): 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", [ From 81d39f0d86725e5d366e2119f47a9f8e4a6def3a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 16 Jun 2026 16:53:40 +0100 Subject: [PATCH 15/22] #868 more work extending metadata support --- .../lfric/kernel/common_arg_metadata.py | 4 +- .../domain/lfric/kernel/field_arg_metadata.py | 8 +-- .../lfric/kernel/inter_grid_arg_metadata.py | 50 ++++++------------- .../lfric/kernel/meta_funcs_arg_metadata.py | 4 ++ .../lfric/kernel/meta_mesh_arg_metadata.py | 4 ++ .../kernel/meta_ref_element_arg_metadata.py | 4 ++ .../lfric/kernel/scalar_arg_metadata.py | 3 ++ .../lfric/kernel/field_arg_metadata_test.py | 6 +-- .../kernel/inter_grid_arg_metadata_test.py | 18 ++----- 9 files changed, 43 insertions(+), 58 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index 9a369c5f00..4e4110d33b 100644 --- a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py @@ -178,8 +178,8 @@ def get_named_arg(fparser2_tree: Fortran2003.Component_Spec, return None @staticmethod - def _validate_keyword_args(fparser2_tree: Fortran2003.Component_Spec, - valid_names: list[str]) -> None: + def _validate_named_args(fparser2_tree: Fortran2003.Component_Spec, + valid_names: list[str]) -> None: ''' ''' for child in fp_walk(fparser2_tree, Fortran2003.Component_Spec): diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index dd17a0378a..f0a2593019 100644 --- a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py @@ -74,6 +74,8 @@ class FieldArgMetadata(ScalarArgMetadata): # and max values). nargs = (4, 5) + fparser2_class = (Fortran2003.Structure_Constructor, Fortran2003.Part_Ref) + def __init__(self, datatype: str, access: str, function_space: str, stencil: Optional[str] = None, nlevels=None, ndata=1): @@ -105,12 +107,12 @@ def _get_metadata( function_space = cls.get_arg( fparser2_tree, cls.function_space_arg_index) stencil = cls.get_stencil(fparser2_tree) - super()._validate_keyword_args(fparser2_tree, - ["nlevels", "ndata", "mesh"]) + super()._validate_named_args(fparser2_tree, + ["nlevels", "ndata", "mesh"]) 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, 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 b4e50693df..fce3d99996 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,15 +49,15 @@ class InterGridArgMetadata(FieldArgMetadata): '''Class to capture LFRic kernel metadata information for an intergrid argument. - :param str datatype: the datatype of this InterGrid argument \ + :param str datatype: the datatype of this InterGrid argument (GH_INTEGER, ...). - :param str access: the way the kernel accesses this intergrid \ + :param str access: the way the kernel accesses this intergrid argument (GH_WRITE, ...). - :param str function_space: the function space that this \ + :param str function_space: the function space that this InterGrid is on (W0, ...). - :param str mesh_arg: the type of mesh that this InterGrid arg \ + :param str 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 Optional[str] stencil: the type of stencil used by the kernel when accessing this InterGrid arg. ''' @@ -104,56 +105,33 @@ def _get_metadata(cls, fparser2_tree): datatype, access = cls._get_datatype_access_metadata(fparser2_tree) function_space = cls.get_arg( fparser2_tree, cls.function_space_arg_index) + cls._validate_named_args(fparser2_tree, ["mesh_arg"]) 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) + mesh_arg = cls.get_mesh_arg(fparser2_tree) return (datatype, access, function_space, mesh_arg, stencil) @staticmethod - def get_mesh_arg(fparser2_tree: Fortran2003.Structure_Constructor, - mesh_arg_index: int) -> str: + def get_mesh_arg( + fparser2_tree: Fortran2003.Structure_Constructor) -> Optional[str]: '''Retrieves the mesh_arg metadata value from the supplied fparser2 tree. :param fparser2_tree: fparser2 tree capturing the metadata for an InterGrid argument. - :param mesh_arg_index: the index at which to find the metadata. - :returns: the metadata mesh value extracted from the fparser2 tree. - - raises ValueError: if the metadata is not in the form - 'mesh_arg = '. + :returns: the metadata mesh value extracted from the fparser2 tree or + None if it isn't present. ''' return FieldArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") - #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): + + 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}, " 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/tests/domain/lfric/kernel/field_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py index 848a68b09d..712b039a56 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 @@ -98,11 +98,11 @@ def test_init_invalid_stencil(): @pytest.mark.parametrize( "metadata,expected_stencil,expected_nlevels,expected_ndata", - [("arg_type(GH_FIELD, GH_REAL, GH_READ, W0)", None, "", ""), + [("arg_type(GH_FIELD, GH_REAL, GH_READ, W0)", None, None, None), ("arg_type(GH_FIELD, GH_REAL, GH_READ, W0, stencil(region))", - "region", "", ""), + "region", None, None), ('arg_type(GH_FIELD, GH_REAL, GH_READ, W0, nlevels="big")', - None, "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 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..d5a53f32ec 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 @@ -121,26 +121,16 @@ def test_get_mesh_arg(): 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_mesh_arg(fparser2_tree) 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_mesh_arg(fparser2_tree) is None @pytest.mark.parametrize("fortran_string", [ From 22c474ddacbffb88af6a630c133b72fc8009692a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Jun 2026 09:20:59 +0100 Subject: [PATCH 16/22] #868 allow for tuple of fparser classes --- .../domain/lfric/kernel/common_metadata.py | 28 +++++++++++++------ .../kernel/common_meta_arg_metadata_test.py | 7 ++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_metadata.py b/src/psyclone/domain/lfric/kernel/common_metadata.py index 4b60b942ca..38b699618a 100644 --- a/src/psyclone/domain/lfric/kernel/common_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_metadata.py @@ -38,6 +38,7 @@ ''' from abc import ABC, abstractmethod +from typing import Union from fparser.common.readfortran import FortranStringReader from fparser.two import Fortran2003 @@ -101,7 +102,8 @@ def validate_scalar_value(value, valid_values, name): @staticmethod def create_fparser2(fortran_string: str, - encoding: Fortran2003.Base) -> Fortran2003.Base: + 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 @@ -111,8 +113,8 @@ def create_fparser2(fortran_string: str, to metadata processing. :param fortran_string: a string containing the metadata in Fortran. - :param encoding: the parent class with which we will encode the - Fortran string. + :param encoding: the fparser2 class(es) with which we will attempt + to match the Fortran string. :returns: an fparser2 tree containing a metadata argument. @@ -122,15 +124,23 @@ def create_fparser2(fortran_string: str, 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/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/common_meta_arg_metadata_test.py index 96e59c5d9b..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 @@ -135,12 +135,12 @@ def test_create_from_fparser2(): with pytest.raises(TypeError) as info: ScalarArgMetadata.create_from_fparser2(None) assert ("Expected kernel metadata to be encoded as an fparser2 " - "Structure_Constructor object but found type 'NoneType' with " + "Part_Ref object but found type 'NoneType' with " "value 'None'." in str(info.value)) # check_remaining_args called fparser2_tree = ScalarArgMetadata.create_fparser2( - "hello(x)", Fortran2003.Structure_Constructor) + "hello(x)", Fortran2003.Part_Ref) with pytest.raises(ValueError) as info: _ = ScalarArgMetadata.create_from_fparser2(fparser2_tree) @@ -150,8 +150,7 @@ def test_create_from_fparser2(): # expected class returned fparser2_tree = ScalarArgMetadata.create_fparser2( - "arg_type(GH_SCALAR, GH_REAL, GH_READ)", - Fortran2003.Structure_Constructor) + "arg_type(GH_SCALAR, GH_REAL, GH_READ)", Fortran2003.Part_Ref) obj = ScalarArgMetadata.create_from_fparser2(fparser2_tree) assert isinstance(obj, ScalarArgMetadata) assert obj.form == "gh_scalar" From 2f4820c3dd0de9e285360e0f56e8453d9e47a1af Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 17 Jun 2026 20:54:36 +0100 Subject: [PATCH 17/22] #868 rm unused utility and improve tests --- .../domain/lfric/kernel/field_arg_metadata.py | 12 +++------ .../lfric/kernel/field_arg_metadata_test.py | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index f0a2593019..0c6f38c14f 100644 --- a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py @@ -78,7 +78,8 @@ class FieldArgMetadata(ScalarArgMetadata): def __init__(self, datatype: str, access: str, function_space: str, stencil: Optional[str] = None, - nlevels=None, ndata=1): + nlevels: Optional[str] = None, + ndata: Optional[str] = "1"): super().__init__(datatype, access) self.function_space = function_space self.stencil = stencil @@ -148,16 +149,9 @@ def get_stencil( stencil = raw_stencil_text[8:-1] return stencil - @classmethod - def get_nlevels(cls, fparser2_tree): - ''' - ''' - return cls.get_named_arg(fparser2_tree, "nlevels") - - def fortran_string(self): + 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}, " 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 712b039a56..6745559c70 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(): From 3f420a3dc6a7e3bde4af6a74cffb91d3536c2d01 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Jun 2026 11:19:27 +0100 Subject: [PATCH 18/22] #868 simplify intergrid --- .../lfric/kernel/inter_grid_arg_metadata.py | 43 ++++++------------- .../kernel/inter_grid_arg_metadata_test.py | 9 ++-- 2 files changed, 17 insertions(+), 35 deletions(-) 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 fce3d99996..5fab7f0921 100644 --- a/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py @@ -86,49 +86,31 @@ def __init__(self, datatype, access, function_space, mesh_arg, 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]]: '''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 \ + :returns: a tuple containing the datatype, access, function space, mesh and stencil metadata. - :rtype: Tuple[str, str, str, str, Optional[str]] ''' datatype, access = cls._get_datatype_access_metadata(fparser2_tree) function_space = cls.get_arg( fparser2_tree, cls.function_space_arg_index) + cls._validate_named_args(fparser2_tree, ["mesh_arg"]) - try: - stencil = cls.get_stencil(fparser2_tree) - except TypeError: - stencil = None - mesh_arg = cls.get_mesh_arg(fparser2_tree) + stencil = cls.get_stencil(fparser2_tree) + mesh_arg = cls.get_named_arg(fparser2_tree, "mesh_arg") return (datatype, access, function_space, mesh_arg, stencil) - @staticmethod - def get_mesh_arg( - fparser2_tree: Fortran2003.Structure_Constructor) -> Optional[str]: - '''Retrieves the mesh_arg metadata value from the supplied fparser2 - tree. - - :param fparser2_tree: fparser2 tree capturing the metadata for - an InterGrid argument. - - :returns: the metadata mesh value extracted from the fparser2 tree or - None if it isn't present. - - ''' - return FieldArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") - def fortran_string(self) -> str: ''' :returns: the metadata represented by this class as Fortran. @@ -141,10 +123,9 @@ def fortran_string(self) -> str: f"{self.function_space}, 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 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 d5a53f32ec..69c105bdf2 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 @@ -114,14 +114,14 @@ def test_get_metadata_stencil(): 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) + 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' @@ -130,7 +130,8 @@ def test_get_mesh_arg(): fparser2_tree = InterGridArgMetadata.create_fparser2( "arg_type(GH_FIELD, GH_REAL, GH_READ, W0, invalid=GH_COARSE)", encoding=Fortran2003.Structure_Constructor) - assert InterGridArgMetadata.get_mesh_arg(fparser2_tree) is None + assert (InterGridArgMetadata.get_named_arg(fparser2_tree, "mesh_arg") + is None) @pytest.mark.parametrize("fortran_string", [ From 887a9d5fafde150ba79929e4af09cd2aff9af099 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Jun 2026 11:48:32 +0100 Subject: [PATCH 19/22] #868 fix cov of common_arg_metadata --- .../lfric/kernel/common_arg_metadata.py | 29 +++++++++----- .../lfric/kernel/common_arg_metadata_test.py | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index 4e4110d33b..7508e0425b 100644 --- a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py @@ -40,7 +40,7 @@ ''' from typing import Optional from fparser.two import Fortran2003 -from fparser.two.utils import walk as fp_walk +from fparser.two import utils as fp_utils from psyclone.domain.lfric.kernel.common_metadata import CommonMetadata @@ -153,20 +153,21 @@ def get_arg(fparser2_tree, index): return None @staticmethod - def get_named_arg(fparser2_tree: Fortran2003.Component_Spec, - name: str) -> Optional[str]: + 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. - :param fparser2_tree: the parse tree of the metadata element. + :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. + :returns: the value of the named metadata element or None if not found. ''' - for child in fp_walk(fparser2_tree, Fortran2003.Component_Spec): + 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], @@ -178,13 +179,21 @@ def get_named_arg(fparser2_tree: Fortran2003.Component_Spec, return None @staticmethod - def _validate_named_args(fparser2_tree: Fortran2003.Component_Spec, + 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_walk(fparser2_tree, Fortran2003.Component_Spec): - if child.children[0].tostr().lower() not in valid_names: - raise ValueError("TODO") + 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/tests/domain/lfric/kernel/common_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/common_arg_metadata_test.py index 89f5b30477..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 @@ -130,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)) From 58d9787cac95dd213ae4724876bf594cb3fbfc79 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Jun 2026 13:19:18 +0100 Subject: [PATCH 20/22] #868 more coverage --- .../domain/lfric/lfric_arg_descriptor.py | 6 ++-- .../domain/lfric/lfric_field_mdata_test.py | 28 +++++++++++++++---- .../tests/domain/lfric/lfric_stencil_test.py | 6 ++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index 60eed8cd6c..feab308df5 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -114,7 +114,7 @@ def __init__(self, arg_type, operates_on, metadata_index): # 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._ndata = "1" self._nargs = 0 # Check for the correct argument type descriptor @@ -378,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 " 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 2b30ef24ae..4e9d656670 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py @@ -166,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)) @@ -259,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. ''' @@ -391,7 +391,7 @@ def test_arg_descriptor_field(): assert field_descriptor.stencil is None assert field_descriptor.vector_size == 1 assert field_descriptor.nlevels is None - assert field_descriptor.ndata == 1 + assert field_descriptor.ndata == "1" def test_fld_nlevels(): @@ -412,6 +412,24 @@ def test_fld_nlevels(): 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(): ''' Tests that an error is raised when a field vector does not use "*" as its 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)) From d56eebb78e6347ff5168715e987eef59e2cf1878 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Jun 2026 17:58:48 +0100 Subject: [PATCH 21/22] #868 add type hints --- .../domain/lfric/lfric_arg_descriptor.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index feab308df5..0ac90994a8 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -809,8 +809,8 @@ def function_spaces(self): @property def nlevels(self) -> str: ''' - :returns: a label identifying the number of vertical levels - associated with this argument. + :returns: a label (or integer, encoded as a string) identifying the + number of vertical levels associated with this argument. ''' return self._nlevels @@ -818,45 +818,41 @@ def nlevels(self) -> str: 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. + number of data values associated with each DoF of the argument. ''' return self._ndata @property - def vector_size(self): + 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. From 36c031af7450c5eca287e953fa9524407277245f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 18 Jun 2026 18:55:20 +0100 Subject: [PATCH 22/22] #868 extend support for inter-grid and vector inter-grid --- .../lfric/kernel/common_arg_metadata.py | 21 +++--- .../domain/lfric/kernel/field_arg_metadata.py | 15 ++-- .../lfric/kernel/inter_grid_arg_metadata.py | 68 +++++++++---------- .../kernel/inter_grid_vector_arg_metadata.py | 25 ++++--- .../domain/lfric/kernel/meta_args_metadata.py | 24 +++---- .../lfric/kernel/field_arg_metadata_test.py | 4 +- .../kernel/inter_grid_arg_metadata_test.py | 10 ++- .../inter_grid_vector_arg_metadata_test.py | 12 ++-- 8 files changed, 94 insertions(+), 85 deletions(-) diff --git a/src/psyclone/domain/lfric/kernel/common_arg_metadata.py b/src/psyclone/domain/lfric/kernel/common_arg_metadata.py index 7508e0425b..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,7 @@ creation, modification and Fortran output of such an argument. ''' -from typing import Optional +from typing import Optional, Union from fparser.two import Fortran2003 from fparser.two import utils as fp_utils @@ -66,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). ''' @@ -159,7 +157,8 @@ def get_named_arg(fparser2_tree: fp_utils.Base, ''' Searches the supplied metadata for 'name=value' expressions and returns the value corresponding to the supplied name if found. - Otherwise returns None. + 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. @@ -174,7 +173,7 @@ def get_named_arg(fparser2_tree: fp_utils.Base, Fortran2003.Char_Literal_Constant): # TODO fparser/#295 - fparser keeps the quotation marks # in character strings. - return text[1:-1] + return text[1:-1].lower() return text return None diff --git a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py index 0c6f38c14f..1f6a54657f 100644 --- a/src/psyclone/domain/lfric/kernel/field_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/field_arg_metadata.py @@ -109,7 +109,7 @@ def _get_metadata( fparser2_tree, cls.function_space_arg_index) stencil = cls.get_stencil(fparser2_tree) super()._validate_named_args(fparser2_tree, - ["nlevels", "ndata", "mesh"]) + ["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) @@ -153,11 +153,16 @@ def fortran_string(self) -> str: ''' :returns: the metadata represented by this class as Fortran. ''' + 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/inter_grid_arg_metadata.py b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py index 5fab7f0921..026f0edf6c 100644 --- a/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py +++ b/src/psyclone/domain/lfric/kernel/inter_grid_arg_metadata.py @@ -49,47 +49,44 @@ 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: Fortran2003.Structure_Constructor - ) -> tuple[str, str, str, str, Optional[str]]: + ) -> 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). @@ -98,29 +95,31 @@ def _get_metadata( for this argument. :returns: a tuple containing the datatype, access, function - space, mesh and stencil metadata. + 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) + function_space = cls.get_arg(fparser2_tree, + cls.function_space_arg_index) - cls._validate_named_args(fparser2_tree, ["mesh_arg"]) + 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) + return (datatype, access, function_space, mesh_arg, stencil, nlevels, + ndata) def fortran_string(self) -> str: ''' :returns: the metadata represented by this class as Fortran. ''' - 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) -> str: @@ -130,10 +129,9 @@ def mesh_arg(self) -> 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/tests/domain/lfric/kernel/field_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/field_arg_metadata_test.py index 6745559c70..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 @@ -182,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/inter_grid_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/inter_grid_arg_metadata_test.py index 69c105bdf2..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,13 +106,15 @@ 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(): @@ -137,7 +141,7 @@ def test_get_mesh_arg(): @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", [