Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 76 additions & 20 deletions doc/user_guide/lfric.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,32 @@ 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 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:

Field Vector
Expand Down Expand Up @@ -919,8 +927,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 +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).
Expand Down Expand Up @@ -1636,7 +1644,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 +1724,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 +1755,44 @@ 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``, e.g.::

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.

(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 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).
Comment on lines +1790 to +1793
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For LFRic atmosphere, It would be useful to have the same flexibility for giving named values as is done for NLAYERS: in fact, it is more useful to have the flexibility here than for NLAYERS as there is less mixing and matching of layer numbers.

We have a lot of physics kernels with ~100 fields with a handful of multidata choices distinguished by ANY_DISCONTINUOUS_SPACE_1, ANY_DISCONTINUOUS_SPACE_2 etc. If they are converted to GH_RUNTIME we would need separate dofmaps etc. for each of the many fields rather than for each of the ANY_DISCONTINUOUS_SPACE choices. Example kernel here [MOSRS password protected]:

https://code.metoffice.gov.uk/trac/lfric_apps/browser/main/trunk/interfaces/physics_schemes_interface/source/kernel/bl_exp_kernel_mod.F90

Copy link
Copy Markdown
Collaborator

@tommbendall tommbendall Nov 6, 2025

Choose a reason for hiding this comment

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

Just commenting that I agree with this point! In fact I think the most common situation is that NDATA will be a runtime variable, but there might be many multidata fields being passed as arguments the same kernel, but all using the same NDATA value.

I'm not sure what the best solution to this is... because the NDATA values are science-related, I don't think we want any specific values hard-coded in arguments_mod.

Could we define a local variable for NDATA to take? e.g.

type(ndata_type) :: LAND_TILES
type(ndata_types) :: NUM_AEROSOLS

arg_type(GH_FIELD, GH_REAL, GH_READ, W2, NDATA=4)
type(arg_type) :: meta_args(4) = (/  &
     arg_type(GH_FIELD, GH_REAL, GH_READWRITE, W3),  &
     arg_type(GH_FIELD, GH_REAL, GH_READ,  W3, NDATA=LAND_TILES), &
     arg_type(GH_FIELD, GH_REAL, GH_READ,  WTHETA, NDATA=NUM_AEROSOLS), &
     arg_type(GH_FIELD, GH_REAL, GH_READ,  W3, NDATA=NUM_AEROSOLS)  &
/)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the sort of thing I had envisaged (though originally I was thinking of a direct but more meaningful replacement for ANY_*SPACE entries).

One of the challenges of moving away from ANY_DISCONTINUOUS_SPACE settings was deciding how and where to name scientifically-meaningful versions given that arguments_mod is in core. One could have a physics module to store them which would enable a common set to be used by several kernels.

I think they would need to be initialised so as not to cause any compiler warnings? If so, that's another reason for hiding them in a module.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great, that's the same as I was imagining for NLAYERS so it's straightforward to do from a PSyclone point of view. I can see your issue with names in core and physics but I'll let you wrangle that :-)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Something else to consider that Tom M has just mentioned is that we have two types of multidata field. Those with ndata_first and those without. We may also need to capture that



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

Expand Down Expand Up @@ -2080,7 +2125,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 +2156,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
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 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
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 +2498,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
45 changes: 37 additions & 8 deletions src/psyclone/domain/lfric/lfric_arg_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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):
'''
Expand Down
21 changes: 21 additions & 0 deletions src/psyclone/parse/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 18 additions & 7 deletions src/psyclone/tests/domain/lfric/lfric_field_mdata_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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. '''
Expand Down Expand Up @@ -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. '''
Expand Down
Loading