Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
234a26a
#868 add text describing nlevels and ndata
arporter Oct 21, 2025
060a1ea
#868 update the rules for kernel arguments
arporter Oct 21, 2025
716c908
#868 change nlevels to nlayers
arporter Oct 22, 2025
5a04ba3
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Nov 5, 2025
e878688
#868 extend docs to allow for naming of nlayers values
arporter Nov 5, 2025
5cdb284
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Apr 2, 2026
3d4e9b9
#868 update nlayers docs
arporter Apr 2, 2026
a0f3f15
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Apr 8, 2026
5db865a
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Apr 8, 2026
a0132df
Merge branch '868_nlayers_ndata_mdata' of github.com:stfc/PSyclone in…
arporter Apr 8, 2026
1bfa1af
#868 update the nlayers text to make it clear that named values are j…
arporter Apr 8, 2026
a6833a1
#868 add initial code to get nlevels metadata for a field arg
arporter Apr 8, 2026
0f54ac6
Merge branch '868_nlayers_ndata_mdata' of github.com:stfc/PSyclone in…
arporter Apr 8, 2026
b5751bb
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Apr 8, 2026
6e5b79a
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Apr 10, 2026
892245f
#868 update nlayers documentation to remove special gh_runtime tag
arporter Apr 10, 2026
1c18889
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Jun 5, 2026
07e8a29
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Jun 5, 2026
748d38d
#868 update docs for nlayers and ndata to use string values
arporter Jun 5, 2026
529158b
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Jun 8, 2026
66617c4
#868 begin adding support for ndata metadata
arporter Jun 8, 2026
64c2d26
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Jun 8, 2026
7a8c9e9
#868 WIP on initial implementation [skip ci]
arporter Jun 9, 2026
707db35
#868 tidy implementation
arporter Jun 9, 2026
268985b
#868 WIP updating psyir-metadata handling classes [skip ci]
arporter Jun 9, 2026
4b8ea08
#868 WIP extending metadata handling [skip ci]
arporter Jun 16, 2026
81d39f0
#868 more work extending metadata support
arporter Jun 16, 2026
9082088
Merge branch 'master' into 868_nlayers_ndata_mdata
arporter Jun 17, 2026
22c474d
#868 allow for tuple of fparser classes
arporter Jun 17, 2026
2f4820c
#868 rm unused utility and improve tests
arporter Jun 17, 2026
3f420a3
#868 simplify intergrid
arporter Jun 18, 2026
887a9d5
#868 fix cov of common_arg_metadata
arporter Jun 18, 2026
58d9787
#868 more coverage
arporter Jun 18, 2026
d56eebb
#868 add type hints
arporter Jun 18, 2026
36c031a
#868 extend support for inter-grid and vector inter-grid
arporter Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 87 additions & 21 deletions doc/user_guide/lfric.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,34 @@ least rank (number of dimensions) one. Scalar arrays are identified with
Field
+++++

LFRic API fields, identified with ``GH_FIELD`` metadata, represent
FEM discretisations of various dynamical core prognostic and diagnostic
LFRic API fields, identified with ``GH_FIELD`` metadata, represent FEM
discretisations of various dynamical core prognostic and diagnostic
variables. In FEM, variables are discretised by placing them into a
function space (see :ref:`lfric-function-space`) from which they
inherit a polynomial expansion via the basis functions of that space.
Field values at points within a cell are evaluated as the sum of a set
of basis functions multiplied by coefficients which are the data points.
Points of evaluation are determined by a quadrature object
of basis functions multiplied by coefficients which are the data
points. Points of evaluation are determined by a quadrature object
(:ref:`lfric-quadrature`) and are independent of the function space
the field is on. Placement of field data points, also called degrees of
freedom (hereafter "DoFs"), is determined by the function space the field
is on.
the field is on. Placement of field data points, also called degrees
of freedom (hereafter "DoFs"), is determined by the function space the
field is on. An LFRic multi-data field can have more than one value
associated with each data point.

LFRic fields passed as arguments to any :ref:`LFRic kernel
<lfric-kernel-valid-data-type>` can be of ``real`` or ``integer``
primitive type. In the LFRic infrastructure, these fields are
represented by instances of the ``field_type`` and ``integer_field_type``
classes, respectively.

Different fields may be defined on different numbers of vertical layers.
The number of layers can be as few as one (a 2D field). Additionally,
LFRic has the concet of multi-data fields where multiple data values can be
associated with each DoF. Unfortunately, both the number of layers and the
number of data values affects the numbering of the DoFs of a field. Therefore,
a distinct DoF map is required for each unique combination of function
space, number of vertical levels and number of data values.

.. _lfric-field-vector:

Field Vector
Expand Down Expand Up @@ -919,8 +929,8 @@ All three CMA-related kernel types must obey the following rules:
1) Since a CMA operator only acts within a single column of data,
stencil operations are not permitted.

2) No vector quantities (e.g. ``GH_FIELD*3`` - see below) are
permitted as arguments.
2) No vector quantities (e.g. ``GH_FIELD*3`` - see below) or
multi-data fields are permitted as arguments.

3) The kernel must operate on cell-columns.

Expand Down Expand Up @@ -1450,7 +1460,7 @@ Supported Function Spaces
As mentioned in the :ref:`lfric-field` and :ref:`lfric-field-vector`
sections, the function space of an argument specifies how it maps
onto the underlying topology and, additionally, whether the data at a
point is a vector. In LFRic API the dimension of the basis function
point is a vector. In the LFRic API the dimension of the basis function
set for the scalar function spaces is 1 and for the vector function spaces
is 3 (see the table in :ref:`lfric-stub-generation-rules` for the
dimensions of the basis and differential basis functions).
Expand Down Expand Up @@ -1636,7 +1646,7 @@ to have stencil accesses, these two options are mutually exclusive.
The metadata for each case is described in the following sections.

Stencil Metadata
________________
""""""""""""""""


Stencil metadata specifies that the corresponding field argument is accessed
Expand Down Expand Up @@ -1716,8 +1726,7 @@ be found in ``examples/lfric/eg5``.
.. _lfric-intergrid-mdata:

Inter-Grid Metadata
___________________

"""""""""""""""""""

The alternative form of the optional fifth metadata argument for a
field specifies which mesh the associated field is on. This is
Expand Down Expand Up @@ -1748,6 +1757,52 @@ meshes cannot be on the same function space while those on the same
mesh must also be on the same function space.


Number of Layers Metadata
"""""""""""""""""""""""""

If a particular field/operator kernel argument has a number of vertical
levels that is *not* the same as the first field/operator argument then
this must be specified using the ``NLAYERS`` option to ``GH_FIELD``/
``GH_OPERATOR``. The value specified for ``NLAYERS`` may be an integer
literal encoded as a string if it is known at compile time, e.g.::

arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS="1")

Alternatively, it may be given a name, e.g.::

arg_type(GH_FIELD, GH_REAL, GH_READ, W3, NLAYERS="some name")

If two or more field/operator arguments are on the same function space
and have the same number of layers (whether a literal or a name) then
only one dofmap (that of the first such field listed in the metadata)
is passed to the kernel for those arguments.

(Since the value of ``NLAYERS`` is looked-up from the corresponding kernel
argument at run time, the labels given in the kernel metadata are just that
- they do not have to correspond to anything in the LFRic infrastructure.)

Multi-Data Metadata
"""""""""""""""""""

A multi-data field is the same as a standard field apart from having multiple
values associated with each DoF. This is indicated in the field metadata by
the optional ``NDATA`` argument to GH_FIELD, e.g.::

arg_type(GH_FIELD, GH_REAL, GH_READ, W2, NDATA="4")

The value contained in the string specified for ``NDATA`` may be a literal if
it is known at compile time. Alternatively, it may be a name in which
case the number of data values at each DoF is determined at runtime by
querying the field object (in the generated PSy layer). As with ``NLAYERS``,
this name is just a label and does not have to correspond to anything in the
LFRic infrastructure.

Since the data in an LFRic field object is stored as a 1D array, having more
than one data value associated with each DoF affects the dofmap. This is
handled by passing the appropriate dofmap to the kernel - see
:ref:`lfric-stub-generation-rules`.


Column-wise Operators (CMA)
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -2080,7 +2135,12 @@ conventions, are:
4) If the field entry stencil access is of type ``XORY1D`` then
add an additional ``integer`` direction argument of kind
``i_def`` and with intent ``in``.

5) If the field is multi-data then the kernel must be passed the
value of ``NDATA``: add an additional ``integer``, scalar
argument of kind ``i_def`` and intent ``in``.
6) If the field has a custom number of vertical levels then pass this as
an additional ``integer``, scalar argument of kind ``i_def`` and
intent ``in``.
3) If the current entry is a field vector then for each dimension
of the vector, include a field array. The field array name is
specified as
Expand All @@ -2106,21 +2166,26 @@ conventions, are:
the data type and kind specified in the metadata. The ScalarArray
must be denoted with intent ``in`` to match its read-only nature.

4) For each function space in the order they appear in the metadata arguments
(the ``to`` function space of an operator is considered to be before the
``from`` function space of the same operator as it appears first in
lexicographic order)
4) DoF maps for function spaces are handled in the order they appear in the
metadata arguments (the ``to`` function space of an operator is considered
to be before the ``from`` function space of the same operator as it appears
first in lexicographic order). Note that if two fields on a given function
space have differing numbers of vertical layers and/or ``NDATA`` values,
then each requires that a dofmap be supplied (because both the number of
vertical layers *and* the number of data values per DoF alter the
*values* within the map). For each required DoF map:

1) Include the number of local degrees of freedom (i.e. number per-cell)
for the function space. This is an ``integer`` of kind ``i_def`` and
has intent ``in``. The name of this argument is
``"ndf_"<field_function_space>``.

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_"<field_function_space>``.
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.

Expand Down Expand Up @@ -2443,8 +2508,9 @@ dofmap for both the to- and from-function spaces of the CMA
operator. Since it does not have any LMA operator arguments it does
not require the ``ncell_3d`` and ``nlayers`` scalar arguments. (Since a
column-wise operator is, by definition, assembled for a whole column,
there is no loop over levels when applying it.)
The full set of rules is then:
there is no loop over levels when applying it.) Note that fields with
non-standard ``nlayers`` or ``ndata > 1`` cannot be supplied as
arguments to CMA kernels. The full set of rules is:

1) Include the ``cell`` argument. ``cell`` is an ``integer`` of kind
``i_def`` and has intent ``in``.
Expand Down
66 changes: 55 additions & 11 deletions src/psyclone/domain/lfric/kernel/common_arg_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
creation, modification and Fortran output of such an argument.

'''
from typing import Optional, Union
from fparser.two import Fortran2003
from fparser.two import utils as fp_utils

from psyclone.domain.lfric.kernel.common_metadata import CommonMetadata

Expand All @@ -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):
Expand All @@ -64,19 +66,17 @@ def check_boolean(value, name):
f"'{type(value).__name__}'.")

@staticmethod
def check_nargs(fparser2_tree, nargs):
def check_nargs(fparser2_tree: Union[Fortran2003.Part_Ref,
Fortran2003.Structure_Constructor],
nargs: Union[int, tuple[int, int]]) -> None:
'''Checks that the metadata has the number of arguments specified
by the 'nargs' argument, otherwise an exception is raised.

:param fparser2_tree: fparser2 tree capturing a metadata argument.
:type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | \
:py:class:`fparser.two.Fortran2003.Structure_Constructor`
:param nargs: the number of expected arguments. This can \
either be a single value or a list containing a lower and an \
upper value.
:type nargs: int or Tuple[int, int]
:param nargs: the number of expected arguments. This can either be
a single value or a list containing a lower and an upper value.

:raises ValueError: if the kernel metadata does not contain \
:raises ValueError: if the kernel metadata does not contain
the expected number of arguments (nargs).

'''
Expand All @@ -103,11 +103,11 @@ def check_fparser2_arg(cls, fparser2_tree, type_name):
Structure_Constructor which captures a metadata argument.

:param fparser2_tree: fparser2 tree capturing a metadata argument.
:type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` | \
:type fparser2_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` |
:py:class:`fparser.two.Fortran2003.Structure_Constructor`
:param str type_name: the name of the argument datatype.

:raises ValueError: if the kernel metadata is not in \
:raises ValueError: if the kernel metadata is not in
the form arg_type(...).

'''
Expand Down Expand Up @@ -150,5 +150,49 @@ def get_arg(fparser2_tree, index):
# Metadata at the specified index does not exist.
return None

@staticmethod
def get_named_arg(fparser2_tree: fp_utils.Base,
name: str
) -> Optional[str]:
'''
Searches the supplied metadata for 'name=value' expressions and
returns the value corresponding to the supplied name if found.
Otherwise returns None. If the value is a string then it is
lower-cased.

:param fparser2_tree: the parse tree of the metadata.
:param name: the name of the metadata element that we want.

:returns: the value of the named metadata element or None if not found.

'''
for child in fp_utils.walk(fparser2_tree, Fortran2003.Component_Spec):
if child.children[0].tostr().lower() == name:
text = child.children[1].tostr()
if isinstance(child.children[1],
Fortran2003.Char_Literal_Constant):
# TODO fparser/#295 - fparser keeps the quotation marks
# in character strings.
return text[1:-1].lower()
return text
return None

@staticmethod
def _validate_named_args(fparser2_tree: fp_utils.Base,
valid_names: list[str]) -> None:
'''
Checks that any named arguments in the supplied parse tree match
with the names in `valid_names`.

:raises ValueError: if an unsupported named argument is found in
the supplied metadata.
'''
for child in fp_utils.walk(fparser2_tree, Fortran2003.Component_Spec):
name = child.children[0].tostr().lower()
if name not in valid_names:
raise ValueError(
f"Kernel metadata contains keyword argument '{name}' "
f"which is not one of the valid options: {valid_names}.")


__all__ = ["CommonArgMetadata"]
41 changes: 24 additions & 17 deletions src/psyclone/domain/lfric/kernel/common_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@

'''
from abc import ABC, abstractmethod
from typing import Union

from fparser.common.readfortran import FortranStringReader
from fparser.two import Fortran2003
from fparser.two.parser import ParserFactory
from fparser.two.utils import NoMatchError, FortranSyntaxError

Expand Down Expand Up @@ -99,7 +101,9 @@ def validate_scalar_value(value, valid_values, name):
f"but found '{value}'.")

@staticmethod
def create_fparser2(fortran_string, encoding):
def create_fparser2(fortran_string: str,
encoding: Union[tuple[Fortran2003.Base],
Fortran2003.Base]) -> Fortran2003.Base:
'''Creates an fparser2 tree from a Fortran string. The resultant
parent node of the tree will be the same type as the encoding
argument if the string conforms to the encoding, otherwise an
Expand All @@ -108,32 +112,35 @@ def create_fparser2(fortran_string, encoding):
TODO: issue #1965: relocate this method as it is not specific
to metadata processing.

:param str fortran_string: a string containing the metadata in \
Fortran.
:param encoding: the parent class with which we will encode the \
Fortran string.
:type encoding: subclass of :py:class:`fparser.two.Fortran2003.Base`
:param fortran_string: a string containing the metadata in Fortran.
:param encoding: the fparser2 class(es) with which we will attempt
to match the Fortran string.

:returns: an fparser2 tree containing a metadata \
argument.
:rtype: subclass of :py:class:`fparser.two.Fortran2003.Base`
:returns: an fparser2 tree containing a metadata argument.

:raises ValueError: if the Fortran string is not in the \
expected form.
:raises ValueError: if the Fortran string is not in the expected form.

'''
std = Config.get().fortran_standard
_ = ParserFactory().create(std=std)
reader = FortranStringReader(fortran_string)
match = True
try:
fparser2_tree = encoding(reader)
except (NoMatchError, FortranSyntaxError):
match = False
match = False
if isinstance(encoding, tuple):
classes = encoding
else:
classes = [encoding]
for enc in classes:
try:
fparser2_tree = enc(reader)
match = True
break
except (NoMatchError, FortranSyntaxError):
continue
if not match or not fparser2_tree:
text = " or ".join(enc.__name__ for enc in classes)
raise ValueError(
f"Expected kernel metadata to be a Fortran "
f"{encoding.__name__}, but found '{fortran_string}'.")
f"{text}, but found '{fortran_string}'.")
return fparser2_tree

@classmethod
Expand Down
Loading
Loading