diff --git a/doc/user_guide/lfric.rst b/doc/user_guide/lfric.rst index 31efcee9c3..b5cfa7c513 100644 --- a/doc/user_guide/lfric.rst +++ b/doc/user_guide/lfric.rst @@ -32,7 +32,7 @@ .. POSSIBILITY OF SUCH DAMAGE. .. ----------------------------------------------------------------------------- .. Written by R. W. Ford and A. R. Porter, STFC Daresbury Lab -.. Modified by I. Kavcic, A. Coughtrie and O. Brunt Met Office +.. Modified by I. Kavcic, A. Coughtrie, O. Brunt and L. Turner, Met Office .. highlight:: fortran @@ -86,24 +86,31 @@ use of each of these arguments: :: - real(kind=r_def) :: rscalar - integer(kind=i_def) :: iscalar, halo_depth - logical(kind=l_def) :: lscalar - integer(kind=i_def) :: stencil_extent - type(field_type) :: field1, field2, field3 - type(field_type) :: field5(3), field6(3) - type(integer_field_type) :: field7 - type(quadrature_type) :: qr - type(operator_type) :: operator1 - type(columnwise_operator_type) :: cma_op1 + + real(kind=r_def) :: rscalar + integer(kind=i_def) :: iscalar, halo_depth + logical(kind=l_def) :: lscalar + real(kind=r_def), dimension(50, 100) :: real_array + integer(kind=i_def), dimension(10) :: integer_array + logical(kind=l_def), dimension(2,5,10,8) :: logical_array + integer(kind=i_def) :: stencil_extent + type(field_type) :: field1, field2, field3 + type(field_type) :: field5(3), field6(3) + type(integer_field_type) :: field7 + type(quadrature_type) :: qr + type(operator_type) :: operator1 + type(columnwise_operator_type) :: cma_op1 ... - call invoke( kernel1(field1, field2, operator1, qr), & - builtin1(rscalar, field2, field3), & - int_builtin2(iscalar, field7), & - kernel2(field1, stencil_extent, field3, lscalar), & - kernel3(field1, halo_depth), & - assembly_kernel(cma_op1, operator1), & - name="some_calculation") + call invoke( kernel1(field1, field2, operator1, real_array, qr), & + builtin1(rscalar, field2, field3), & + int_builtin2(iscalar, field7), & + kernel2(field1, stencil_extent, field3, lscalar), & + assembly_kernel(cma_op1, operator1), & + kernel3(field1, halo_depth), & + kernel4(field3, integer_array, logical_array), & + name="some_calculation" & + ) + Each of these argument types is described in more detail in the next :ref:`section `. @@ -132,6 +139,17 @@ with ``GH_SCALAR`` metadata. Scalar arguments can have ``real``, ` (``logical`` data type is not supported in the :ref:`LFRic Built-ins `). +.. _lfric-array: + +Scalar Array +++++++++++++ + +In the LFRic API a scalar array represents a Fortran array of scalars, of at +least rank (number of dimensions) one. Scalar arrays are identified with +``GH_SCALAR_ARRAY`` metadata. As with scalars, array arguments can have +``real``, ``integer`` or ``logical`` data type in +:ref:`user-defined Kernels `. + .. _lfric-field: Field @@ -406,7 +424,7 @@ Mixed Precision The LFRic API supports the ability to specify the precision required by the model via precision variables. To make use of this, the code -developer must declare scalars, fields and operators in the algorithm +developer must declare scalars, arrays, fields and operators in the algorithm layer with the required LFRic-supported precision. In the current implementation there are two supported precisions for ``REAL`` data and one each for ``INTEGER`` and ``LOGICAL`` data. The actual precision used in @@ -420,50 +438,50 @@ associated kernel metadata description and their precision: .. tabularcolumns:: |l|l|l| -+--------------------------+---------------------------------+-----------+ -| Data Type | Kernel Metadata | Precision | -+==========================+=================================+===========+ -| REAL(R_DEF) | GH_SCALAR, GH_REAL | R_DEF | -+--------------------------+---------------------------------+-----------+ -| REAL(R_BL) | GH_SCALAR, GH_REAL | R_BL | -+--------------------------+---------------------------------+-----------+ -| REAL(R_PHYS) | GH_SCALAR, GH_REAL | R_PHYS | -+--------------------------+---------------------------------+-----------+ -| REAL(R_SOLVER) | GH_SCALAR, GH_REAL | R_SOLVER | -+--------------------------+---------------------------------+-----------+ -| REAL(R_TRAN) | GH_SCALAR, GH_REAL | R_TRAN | -+--------------------------+---------------------------------+-----------+ -| INTEGER(I_DEF) | GH_SCALAR, GH_INTEGER | I_DEF | -+--------------------------+---------------------------------+-----------+ -| LOGICAL(L_DEF) | GH_SCALAR, GH_LOGICAL | L_DEF | -+--------------------------+---------------------------------+-----------+ -| FIELD_TYPE | GH_FIELD, GH_REAL | R_DEF | -+--------------------------+---------------------------------+-----------+ -| R_BL_FIELD_TYPE | GH_FIELD, GH_REAL | R_BL | -+--------------------------+---------------------------------+-----------+ -| R_PHYS_FIELD_TYPE | GH_FIELD, GH_REAL | R_PHYS | -+--------------------------+---------------------------------+-----------+ -| R_SOLVER_FIELD_TYPE | GH_FIELD, GH_REAL | R_SOLVER | -+--------------------------+---------------------------------+-----------+ -| R_TRAN_FIELD_TYPE | GH_FIELD, GH_REAL | R_TRAN | -+--------------------------+---------------------------------+-----------+ -| INTEGER_FIELD_TYPE | GH_FIELD, GH_INTEGER | I_DEF | -+--------------------------+---------------------------------+-----------+ -| OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_DEF | -+--------------------------+---------------------------------+-----------+ -| R_SOLVER_OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_SOLVER | -+--------------------------+---------------------------------+-----------+ -| R_TRAN_OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_TRAN | -+--------------------------+---------------------------------+-----------+ -| COLUMNWISE_OPERATOR_TYPE | GH_COLUMNWISE_OPERATOR, GH_REAL | R_SOLVER | -+--------------------------+---------------------------------+-----------+ ++--------------------------+---------------------------------------+-----------+ +| Data Type | Kernel Metadata | Precision | ++==========================+=======================================+===========+ +| REAL(R_DEF) | GH_SCALAR/GH_SCALAR_ARRAY, GH_REAL | R_DEF | ++--------------------------+---------------------------------------+-----------+ +| REAL(R_BL) | GH_SCALAR/GH_SCALAR_ARRAY, GH_REAL | R_BL | ++--------------------------+---------------------------------------+-----------+ +| REAL(R_PHYS) | GH_SCALAR/GH_SCALAR_ARRAY, GH_REAL | R_PHYS | ++--------------------------+---------------------------------------+-----------+ +| REAL(R_SOLVER) | GH_SCALAR/GH_SCALAR_ARRAY, GH_REAL | R_SOLVER | ++--------------------------+---------------------------------------+-----------+ +| REAL(R_TRAN) | GH_SCALAR/GH_SCALAR_ARRAY, GH_REAL | R_TRAN | ++--------------------------+---------------------------------------+-----------+ +| INTEGER(I_DEF) | GH_SCALAR/GH_SCALAR_ARRAY, GH_INTEGER | I_DEF | ++--------------------------+---------------------------------------+-----------+ +| LOGICAL(L_DEF) | GH_SCALAR/GH_SCALAR_ARRAY, GH_LOGICAL | L_DEF | ++--------------------------+---------------------------------------+-----------+ +| FIELD_TYPE | GH_FIELD, GH_REAL | R_DEF | ++--------------------------+---------------------------------------+-----------+ +| R_BL_FIELD_TYPE | GH_FIELD, GH_REAL | R_BL | ++--------------------------+---------------------------------------+-----------+ +| R_PHYS_FIELD_TYPE | GH_FIELD, GH_REAL | R_PHYS | ++--------------------------+---------------------------------------+-----------+ +| R_SOLVER_FIELD_TYPE | GH_FIELD, GH_REAL | R_SOLVER | ++--------------------------+---------------------------------------+-----------+ +| R_TRAN_FIELD_TYPE | GH_FIELD, GH_REAL | R_TRAN | ++--------------------------+---------------------------------------+-----------+ +| INTEGER_FIELD_TYPE | GH_FIELD, GH_INTEGER | I_DEF | ++--------------------------+---------------------------------------+-----------+ +| OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_DEF | ++--------------------------+---------------------------------------+-----------+ +| R_SOLVER_OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_SOLVER | ++--------------------------+---------------------------------------+-----------+ +| R_TRAN_OPERATOR_TYPE | GH_OPERATOR, GH_REAL | R_TRAN | ++--------------------------+---------------------------------------+-----------+ +| COLUMNWISE_OPERATOR_TYPE | GH_COLUMNWISE_OPERATOR, GH_REAL | R_SOLVER | ++--------------------------+---------------------------------------+-----------+ As can be seen from the above table, the kernel metadata does not capture all of the precision options. For example, from the metadata it is not possible to determine whether a ``REAL`` scalar, ``REAL`` field or ``REAL`` operator has precision ``R_DEF``, ``R_SOLVER`` or ``R_TRAN``. -If a scalar, field, or operator is specified with a particular +If a scalar, array, field, or operator is specified with a particular precision in the algorithm layer then any associated kernels that it is passed to must have been written so that they support this precision. If a kernel needs to support data that can be stored with @@ -1059,20 +1077,20 @@ meta_args ######### The ``meta_args`` array specifies information about data that the -kernel code expects to be passed to it via its argument list. There is -one entry in the ``meta_args`` array for each **scalar**, **field**, +kernel code expects to be passed to it via its argument list. There is one +entry in the ``meta_args`` array for each **scalar**, **array**, **field**, or **operator** passed into the Kernel and the order that these occur in the ``meta_args`` array must be the same as they are expected in the kernel code argument list. The entry must be of ``arg_type`` which -itself contains metadata about the associated argument. The size of -the ``meta_args`` array must correspond to the number of **scalars**, +itself contains metadata about the associated argument. The size of the +``meta_args`` array must correspond to the number of **scalars**, **arrays** **fields** and **operators** passed into the Kernel. -.. note:: It makes no sense for a Kernel to have only **scalar** arguments - (because the PSy layer will call a Kernel for each point in the - spatial domain) and PSyclone will reject such Kernels. +.. note:: It makes no sense for a Kernel to have only **scalar** or **array** + arguments (because the PSy layer will call a Kernel for each point + in the spatial domain) and PSyclone will reject such Kernels. -For example, if there are a total of 2 **scalar** / **field** / +For example, if there are a total of 2 **scalar** / **array** / **field** / **operator** entities being passed to the Kernel then the ``meta_args`` array will be of size 2 and there will be two ``arg_type`` entries:: @@ -1082,28 +1100,30 @@ array will be of size 2 and there will be two ``arg_type`` entries:: /) Argument metadata (information contained within the brackets of an -``arg_type`` entry), describes either a **scalar**, a **field** or an -**operator** (either LMA or CMA). +``arg_type`` entry), describes either a **scalar**, an **array**, a **field** +or an **operator** (either LMA or CMA). The first argument-metadata entry describes whether the data that is -being passed is for a scalar (``GH_SCALAR``), a field (``GH_FIELD``) or -an operator (either ``GH_OPERATOR`` for LMA or ``GH_COLUMNWISE_OPERATOR`` -for CMA). This information is mandatory. +being passed is for a scalar (``GH_SCALAR``), an array (``GH_SCALAR_ARRAY``), a +field (``GH_FIELD``) or an operator (either ``GH_OPERATOR`` for LMA or +``GH_COLUMNWISE_OPERATOR`` for CMA). This information is mandatory. Additionally, argument metadata can be used to describe a vector of fields (see the :ref:`lfric-field-vector` section for more details). -As an example, the following ``meta_args`` metadata describes 4 -entries, the first is a scalar, the next two are fields and the -fourth is an operator. The third entry is a field vector of size 3. +As an example, the following ``meta_args`` metadata describes 5 +entries, the first is a scalar, the second is an array, the next two +are fields and the fifth is an operator. The fourth entry is a field vector +of size 3. :: - type(arg_type) :: meta_args(4) = (/ & + type(arg_type) :: meta_args(5) = (/ & arg_type(GH_SCALAR, GH_REAL, ...), & - arg_type(GH_FIELD, GH_INTEGER, ... ), & - arg_type(GH_FIELD*3, GH_REAL, ... ), & + arg_type(GH_SCALAR_ARRAY, GH_LOGICAL, ...), & + arg_type(GH_FIELD, GH_INTEGER, ...), & + arg_type(GH_FIELD*3, GH_REAL, ...), & arg_type(GH_OPERATOR, GH_REAL, ...) & /) @@ -1155,13 +1175,14 @@ combinations are specified later in this section (see For example:: - type(arg_type) :: meta_args(6) = (/ & - arg_type(GH_OPERATOR, GH_REAL, GH_READ, ... ), & - arg_type(GH_FIELD*3, GH_REAL, GH_WRITE, ... ), & - arg_type(GH_FIELD, GH_REAL, GH_READWRITE, ... ), & - arg_type(GH_FIELD, GH_INTEGER, GH_INC, ... ), & - arg_type(GH_FIELD, GH_REAL, GH_READINC, ... ), & - arg_type(GH_SCALAR, GH_REAL, GH_SUM) & + type(arg_type) :: meta_args(7) = (/ & + arg_type(GH_OPERATOR, GH_REAL, GH_READ, ... ), & + arg_type(GH_FIELD*3, GH_REAL, GH_WRITE, ... ), & + arg_type(GH_FIELD, GH_REAL, GH_READWRITE, ... ), & + arg_type(GH_FIELD, GH_INTEGER, GH_INC, ... ), & + arg_type(GH_FIELD, GH_REAL, GH_READINC, ... ), & + arg_type(GH_SCALAR_ARRAY, GH_LOGICAL, GH_READ, ... ), & + arg_type(GH_SCALAR, GH_REAL, GH_SUM) & /) .. warning:: It is important that ``GH_INC`` is not incorrectly used @@ -1204,16 +1225,17 @@ For example:: does not yet support ``integer`` and ``logical`` reductions. For a scalar, the argument metadata contains only these three entries. -However, fields and operators require further entries specifying -function-space information. -The meaning of these further entries differs depending on whether a -field or an operator is being described. - -In the case of an operator, the fourth and fifth arguments describe -the ``to`` and ``from`` function spaces respectively. In the case of a -field the fourth argument specifies the function space that the field -lives on. More details about the supported function spaces are in -subsection :ref:`lfric-function-space`. +However, fields, operators and scalar arrays require further entries specifying +function-space information or dimensionality. The meaning of these further +entries differs depending on whether a field, an operator or a scalar array is +being described. + +In the case of a field the fourth argument specifies the function space that the +field lives on. In the case of an operator, the fourth and fifth arguments +describe the ``to`` and ``from`` function spaces respectively. In the case of a +scalar array, the fourth argument specifies the number of dimensions the array +has. More details about the supported function spaces are in subsection +:ref:`lfric-function-space`. For example, the metadata for a kernel that applies a column-wise operator to a field might look like:: @@ -1298,6 +1320,8 @@ the :ref:`LFRic fields `): +========================+=================================+ | GH_SCALAR | GH_REAL, GH_INTEGER, GH_LOGICAL | +------------------------+---------------------------------+ +| GH_SCALAR_ARRAY | GH_REAL, GH_INTEGER, GH_LOGICAL | ++------------------------+---------------------------------+ | GH_FIELD | GH_REAL, GH_INTEGER | +------------------------+---------------------------------+ | GH_OPERATOR | GH_REAL | @@ -1313,7 +1337,7 @@ Valid Access Modes As mentioned earlier, not all combinations of metadata are valid. Valid combinations for each argument type in user-defined Kernels are summarised here. All argument types -(``GH_SCALAR``, ``GH_FIELD``, ``GH_OPERATOR`` and +(``GH_SCALAR``, ``GH_SCALAR_ARRAY``, ``GH_FIELD``, ``GH_OPERATOR`` and ``GH_COLUMNWISE_OPERATOR``) may be read within a Kernel and this is specified in metadata using ``GH_READ``. At least one kernel argument must be listed as being modified. When data is *modified* @@ -1329,6 +1353,8 @@ modes depend upon the argument type and the function space it is on: +========================+==============================+====================+ | GH_SCALAR | n/a | GH_READ | +------------------------+------------------------------+--------------------+ +| GH_SCALAR_ARRAY | n/a | GH_READ | ++------------------------+------------------------------+--------------------+ | GH_FIELD | Discontinuous | GH_READ, GH_WRITE, | | | | GH_READWRITE | +------------------------+------------------------------+--------------------+ @@ -1399,6 +1425,19 @@ checks (when generating the PSy layer) that any kernels which read operator values do not do so beyond the level-1 halo. If any such accesses are found then PSyclone aborts. +.. _lfric-array-sizes: + +Array sizes +^^^^^^^^^^^ + +The size of a :ref:`scalar array ` is described by ````, +where *n > 0* is the number of Fortran ranks representing the dimension of the +array, e.g. a logical, scalar array of rank three would be specified as: + +:: + + arg_type(GH_SCALAR_ARRAY, GH_LOGICAL, GH_READ, 3) + .. _lfric-function-space: Supported Function Spaces @@ -1972,7 +2011,7 @@ conventions, are: is an ``integer`` of kind ``i_def`` and has intent ``in``. PSyclone will obtain the value of ``nlayers`` to use for a particular kernel from the first field or operator in the argument list. -3) For each scalar/field/vector_field/operator in the order specified by +3) For each scalar/field/vector_field/operator/ScalarArray in the order specified by the meta_args metadata: 1) If the current entry is a scalar quantity then include the Fortran @@ -2012,7 +2051,7 @@ conventions, are: 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 being using + specified as ``"field_""_""_v"``. A field array in a field vector is declared in the same way as a field array (described in the previous step). @@ -2027,6 +2066,13 @@ conventions, are: freedom for the ``to`` and ``from`` function spaces, respectively. Again the intent is determined from the metadata (see :ref:`lfric-api-meta-args`). + 5) If the current entry is a ScalarArray then first include a rank-1 + ``integer`` array of kind ``i_def`` and size ``nranks_`` + containing the upper bounds for each rank, ``dims_`` + (the lower bound is assumed to be 1 as this is how Fortran passes + array slices to subroutines by default). Then pass the array of + 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 diff --git a/src/psyclone/domain/lfric/kernel/__init__.py b/src/psyclone/domain/lfric/kernel/__init__.py index e81444a90d..e72bba56a4 100644 --- a/src/psyclone/domain/lfric/kernel/__init__.py +++ b/src/psyclone/domain/lfric/kernel/__init__.py @@ -32,10 +32,13 @@ # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- # Author R. W. Ford, STFC Daresbury Lab +# Modified L. Turner, Met Office '''Module for Kernels in the LFRic domain.''' +from psyclone.domain.lfric.kernel.scalar_array_arg_metadata import \ + ScalarArrayArgMetadata from psyclone.domain.lfric.kernel.columnwise_operator_arg_metadata import \ ColumnwiseOperatorArgMetadata from psyclone.domain.lfric.kernel.common_arg_metadata import CommonArgMetadata diff --git a/src/psyclone/domain/lfric/kernel/scalar_array_arg_metadata.py b/src/psyclone/domain/lfric/kernel/scalar_array_arg_metadata.py new file mode 100644 index 0000000000..af9da5595b --- /dev/null +++ b/src/psyclone/domain/lfric/kernel/scalar_array_arg_metadata.py @@ -0,0 +1,165 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023-2024, Science and Technology Facilities Council +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author L. Turner, Met Office +# Modified A. Pirrie, Met Office + +'''Module containing the ScalarArrayArgMetadata class which captures the +metadata associated with a ScalarArray argument. Supports the creation, +modification and Fortran output of a ScalarArray argument. + +''' +from typing import Union +from fparser.two import Fortran2003 +from psyclone.domain.lfric.kernel.common_arg_metadata import CommonArgMetadata +from psyclone.domain.lfric.kernel.scalar_arg_metadata import ScalarArgMetadata + + +class ScalarArrayArgMetadata(ScalarArgMetadata): + '''Class to capture LFRic kernel metadata information for a ScalarArray + argument. + + :param str datatype: the datatype of this ScalarArray (GH_INTEGER, ...). + :param str access: the way the kernel accesses this Scalar Array (GH_READ). + :param str array_ndims: the rank (number of dimensions) of this + ScalarArray. + + ''' + # The name used to specify a ScalarArray argument in LFRic metadata. + form = "gh_scalar_array" + # The relative positions of LFRic metadata. Metadata for a ScalarArray + # argument is provided in the following format 'arg_type(form, + # datatype, access, array_ndims)'. 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 + array_ndims_arg_index = 3 + # The name to use for any exceptions. + check_name = "array" + # The number of arguments in the language-level metadata (min and + # max values). + nargs = (4, 4) + + def __init__(self, datatype, access, array_ndims): + super().__init__(datatype, access) + self.array_ndims = array_ndims + + @classmethod + def _get_metadata(cls, fparser2_tree: Union[Fortran2003.Part_Ref, + Fortran2003.Structure_Constructor] + ) -> tuple[str, str, int]: + + '''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 + for this argument. + + :returns: a tuple containing the datatype, access and array ndims + metadata. + + ''' + datatype, access = super()._get_metadata(fparser2_tree) + array_ndims = cls.get_array_ndims(fparser2_tree) + return (datatype, access, array_ndims) + + def fortran_string(self) -> str: + ''' + :returns: the metadata represented by this class as Fortran. + ''' + return (f"arg_type({self.form}, {self.datatype}, {self.access}, " + f"{self.array_ndims})") + + @property + def array_ndims(self) -> int: + ''' + :returns: the number of dimensions for this ScalarArray argument. + ''' + return self._array_ndims + + @array_ndims.setter + def array_ndims(self, value): + ''' + :param str value: set the number of dimensions to the specified value. + + :raises TypeError: if value is not an integer type. + :raises ValueError: if value is less than 1. + + ''' + if not isinstance(value, int): + raise TypeError(f"The type of value must be an integer, but " + f"found input of type {type(value)}.") + + if value < 1: + raise ValueError(f"The number of dimensions of a ScalarArray " + f"should be an integer greater than or " + f"equal to 1 but found {value}.") + + self._array_ndims = value + + @classmethod + def get_array_ndims(cls, fparser2_tree) -> int: + '''Retrieves the array ndims metadata value found within the + supplied fparser2 tree 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 array ndims value extracted from the fparser2 tree, + converted to an int. + + :raises ValueError: if the array ndims is not a string. + :raises ValueError: if the array ndims is less than 1. + + ''' + array_datatype = CommonArgMetadata.get_arg( + fparser2_tree, cls.array_ndims_arg_index) + array_ndims = array_datatype.strip() + try: + int_value = int(array_ndims) + except ValueError as info: + raise ValueError(f"The number of dimensions of a ScalarArray " + f"should be a string containing an integer, " + f"but found '{array_ndims}'.") from info + + if int_value < 1: + raise ValueError(f"The number of dimensions of a ScalarArray " + f"should be an integer greater than or " + f"equal to 1 but found {int_value}.") + return int_value + + +__all__ = ["ScalarArrayArgMetadata"] diff --git a/src/psyclone/domain/lfric/lfric_arg_descriptor.py b/src/psyclone/domain/lfric/lfric_arg_descriptor.py index 1f62846c3c..98ce90d48e 100644 --- a/src/psyclone/domain/lfric/lfric_arg_descriptor.py +++ b/src/psyclone/domain/lfric/lfric_arg_descriptor.py @@ -32,7 +32,7 @@ # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- # Authors R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab -# Modified I. Kavcic and A. Coughtrie, Met Office +# Modified I. Kavcic, A. Coughtrie and L. Turner, Met Office # Modified by J. Henrichs, Bureau of Meteorology ''' @@ -63,26 +63,26 @@ class LFRicArgDescriptor(Descriptor): This class captures the information specified in one of LFRic API argument descriptors (scalars, fields and operators). - :param arg_type: LFRic API valid argument type (scalar, \ + :param arg_type: LFRic API valid argument type (scalar, field or operator). - :type arg_type: :py:class:`psyclone.expression.FunctionVar` or \ + :type arg_type: :py:class:`psyclone.expression.FunctionVar` or :py:class:`psyclone.expression.BinaryOperator` - :param str operates_on: value of operates_on from the parsed kernel \ + :param str operates_on: value of operates_on from the parsed kernel metadata (used for validation). - :param int metadata_index: position of this argument in the list of \ + :param int metadata_index: position of this argument in the list of arguments specified in the metadata. :raises ParseError: if a 'meta_arg' entry is not of 'arg_type' type. - :raises ParseError: if the first argument of a 'meta_arg' entry is not \ + :raises ParseError: if the first argument of a 'meta_arg' entry is not one of LFRic API valid argument types. - :raises ParseError: if the second argument of a 'meta_arg' entry is not \ + :raises ParseError: if the second argument of a 'meta_arg' entry is not one of LFRic API valid data types. :raises ParseError: if a 'meta_arg' entry has fewer than 3 args. - :raises ParseError: if the third 'meta_arg' entry is not a valid \ + :raises ParseError: if the third 'meta_arg' entry is not a valid access descriptor. - :raises InternalError: if the operates_on from the parsed kernel \ + :raises InternalError: if the operates_on from the parsed kernel metadata is not 'cell_column' or 'dof'. - :raises InternalError: if all the metadata checks fail to catch an \ + :raises InternalError: if all the metadata checks fail to catch an invalid argument type. ''' @@ -102,6 +102,7 @@ def __init__(self, arg_type, operates_on, metadata_index): self._function_spaces = [] # Set vector size to 1 (scalars set it to 0 in their validation) self._vector_size = 1 + self._array_ndims = 1 # Initialise other internal arguments self._access_type = None self._function_space1 = None @@ -201,6 +202,10 @@ def __init__(self, arg_type, operates_on, metadata_index): # Validate scalar arguments self._init_scalar(arg_type) + elif self._argument_type in const.VALID_ARRAY_NAMES: + # Validate ScalarArray arguments + self._init_array(arg_type) + else: # We should never get to here if the checks are tight enough raise InternalError( @@ -222,14 +227,14 @@ def _validate_vector_size(self, separator, arg_type): :param arg_type: LFRic API field (vector) argument type. :type arg_type: :py:class:`psyclone.expression.FunctionVar` - :raises ParseError: if the field vector notation does not use \ + :raises ParseError: if the field vector notation does not use the '*' operator. - :raises ParseError: if the field vector notation is not in the \ - correct format '(field*n)' where 'n' is \ + :raises ParseError: if the field vector notation is not in the + correct format '(field*n)' where 'n' is an integer. - :raises ParseError: if the field vector notation is used for the \ + :raises ParseError: if the field vector notation is used for the vector size of less than 2. - :raises ParseError: if the field vector notation is used for an \ + :raises ParseError: if the field vector notation is used for an argument that is not a field. ''' @@ -270,6 +275,44 @@ def _validate_vector_size(self, separator, arg_type): f"{const.VALID_FIELD_NAMES} argument types but found " f"'{arg_type.args[0]}'.") + def _validate_array_ndims(self, arg_type): + ''' + Validates descriptors for ScalarArray arguments and populates + properties accordingly. + + :param str separator: operator in a binary expression. + :param arg_type: LFRic API ScalarArray argument type. + :type arg_type: :py:class:`psyclone.expression.FunctionVar` + + :raises ParseError: if the ScalarArray notation is not in the + correct format 'n' where 'n' is + an integer. + :raises ParseError: if the specified number of ScalarArray + dimensions is less than 1. + :raises ParseError: if the ScalarArray notation is used for an + argument that is not a ScalarArray. + + ''' + # Try to find the array size for a ScalarArray and raise + # an error if it is not an integer number... + try: + array_ndims = int(arg_type.args[3]) + except ValueError as err: + raise ParseError( + f"In the LFRic API, the ScalarArray notation must be " + f"in the format 'n' where 'n' is an integer, " + f"but '{arg_type.args[3]}' was found in " + f"'{arg_type}'.") from err + + # ... or it is less than 1... + if array_ndims < 1: + raise ParseError( + f"In the LFRic API, the ScalarArray notation must be " + f"in the format 'n' where 'n' is an integer >= 1. " + f"However, found n = '{array_ndims}' in '{arg_type}'.") + # ... and set the ScalarArray size if all checks pass + self._array_ndims = array_ndims + def _init_field(self, arg_type, operates_on): ''' Validates metadata descriptors for field arguments and @@ -277,36 +320,36 @@ def _init_field(self, arg_type, operates_on): :param arg_type: LFRic API field (vector) argument type. :type arg_type: :py:class:`psyclone.expression.FunctionVar` - :param operates_on: value of operates_on from the parsed kernel \ + :param operates_on: value of operates_on from the parsed kernel metadata (used for validation). :type operates_on: str - :raises InternalError: if argument type other than a field is \ + :raises InternalError: if argument type other than a field is passed in. :raises ParseError: if there are fewer than 4 metadata arguments. :raises ParseError: if there are more than 5 metadata arguments. :raises ParseError: if a field argument has an invalid data type. :raises ParseError: if the 4th argument is not a valid function space. - :raises ParseError: if the optional 5th argument is not a stencil \ - specification or a mesh identifier (for \ + :raises ParseError: if the optional 5th argument is not a stencil + specification or a mesh identifier (for inter-grid kernels). - :raises ParseError: if a field passed to a kernel that operates on \ - DoFs does not have a valid access \ + :raises ParseError: if a field passed to a kernel that operates on + DoFs does not have a valid access (one of [READ, WRITE, READWRITE]). - :raises ParseError: if a field on a discontinuous function space \ - passed to a kernel that operates on cell-columns \ - does not have a valid access (one of \ + :raises ParseError: if a field on a discontinuous function space + passed to a kernel that operates on cell-columns + does not have a valid access (one of [READ, WRITE, READWRITE]). - :raises ParseError: if a field on a continuous function space \ - passed to a kernel that operates on cell-columns \ - does not have a valid access (one of [READ, WRITE,\ + :raises ParseError: if a field on a continuous function space + passed to a kernel that operates on cell-columns + does not have a valid access (one of [READ, WRITE, INC, READINC]). - :raises ParseError: if the kernel operates on the domain and is \ + :raises ParseError: if the kernel operates on the domain and is passed a field on a continuous space. - :raises InternalError: if an invalid value for operates_on is \ + :raises InternalError: if an invalid value for operates_on is passed in. :raises ParseError: if a field with a stencil access is not read-only. - :raises ParseError: if a field with a stencil access is passed to a \ + :raises ParseError: if a field with a stencil access is passed to a kernel that operates on the domain. ''' @@ -466,13 +509,13 @@ def _init_operator(self, arg_type): :param arg_type: LFRic API operator argument type. :type arg_type: :py:class:`psyclone.expression.FunctionVar` - :raises InternalError: if argument type other than an operator is \ + :raises InternalError: if argument type other than an operator is passed in. :raises ParseError: if there are not exactly 5 metadata arguments. :raises ParseError: if an operator argument has an invalid data type. - :raises ParseError: if the function space to- is not one of the \ + :raises ParseError: if the function space to- is not one of the valid function spaces. - :raises ParseError: if the function space from- is not one of the \ + :raises ParseError: if the function space from- is not one of the valid function spaces. :raises ParseError: if the operator argument has an invalid access. @@ -546,13 +589,13 @@ def _init_scalar(self, arg_type): :param arg_type: LFRic API scalar argument type. :type arg_type: :py:class:`psyclone.expression.FunctionVar` - :raises InternalError: if argument type other than a scalar is \ + :raises InternalError: if argument type other than a scalar is passed in. :raises ParseError: if there are not exactly 3 metadata arguments. :raises InternalError: if a scalar argument has an invalid data type. :raises ParseError: if scalar arguments do not have a read-only or a reduction access. - :raises ParseError: if a scalar argument that is not a real \ + :raises ParseError: if a scalar argument that is not a real scalar has a reduction access. ''' @@ -600,7 +643,65 @@ def _init_scalar(self, arg_type): f"with a real scalar argument, but a scalar argument with " f"'{self._data_type}' data type was found in '{arg_type}'.") - # Scalars don't have vector size + # Scalars don't have vector size or array size + self._vector_size = 0 + self._array_ndims = 0 + + def _init_array(self, arg_type): + ''' + Validates metadata descriptors for ScalarArray arguments and + initialises ScalarArray argument properties accordingly. + + :param arg_type: LFRic API ScalarArray argument type. + :type arg_type: :py:class:`psyclone.expression.FunctionVar` + + :raises InternalError: if argument type other than a ScalarArray is + passed in. + :raises ParseError: if there are not exactly 4 metadata arguments. + :raises InternalError: if the ScalarArray argument has an invalid data + type. + :raises ParseError: if ScalarArray argument does not have read-only + access. + + ''' + const = LFRicConstants() + # Check whether something other than a scalar is passed in + if self._argument_type not in const.VALID_ARRAY_NAMES: + raise InternalError( + f"Expected a ScalarArray argument but got an argument of type " + f"'{arg_type.args[0]}'.") + + # There must be 4 arguments + nargs_array = 4 + if self._nargs != nargs_array: + raise ParseError( + "In the LFRic API a 'meta_arg' entry must have " + f"{nargs_array} arguments if its first argument is of " + f"{const.VALID_ARRAY_NAMES} type, but found {self._nargs} in " + f"'{arg_type}'.") + + # Check whether an invalid data type for a ScalarArray argument is + # passed in. + if self._data_type not in const.VALID_ARRAY_DATA_TYPES: + raise InternalError( + f"Expected one of {const.VALID_ARRAY_DATA_TYPES} as the " + f"ScalarArray data type but got '{self._data_type}'.") + + # Test allowed accesses for ScalarArrays (read_only) + array_accesses = [AccessType.READ] + # Convert generic access types to GH_* names for error messages + api_config = Config.get().api_conf(API) + rev_access_mapping = api_config.get_reverse_access_mapping() + if self._access_type not in array_accesses: + api_specific_name = rev_access_mapping[self._access_type] + raise ParseError( + f"In the LFRic API, ScalarArray arguments must have read-only " + f"('gh_read') access but found '{api_specific_name}' " + f"in '{arg_type}'.") + + self._validate_array_ndims(arg_type) + + # ScalarArrays don't have vector size self._vector_size = 0 @property @@ -659,8 +760,8 @@ def function_space(self): depending on the argument type: a single function space for a field, function_space_from for an operator and nothing for a scalar. - :returns: function space relating to this kernel argument or \ - None (for a scalar). + :returns: function space relating to this kernel argument or + None (for a scalar or ScalarArray). :rtype: str or NoneType :raises InternalError: if an invalid argument type is passed in. @@ -671,7 +772,8 @@ def function_space(self): return self._function_space1 if self._argument_type in const.VALID_OPERATOR_NAMES: return self._function_space2 - if self._argument_type in const.VALID_SCALAR_NAMES: + if self._argument_type in (const.VALID_ARRAY_NAMES + + const.VALID_SCALAR_NAMES): return None raise InternalError(f"Expected a valid argument type but got " f"'{self._argument_type}'.") @@ -696,7 +798,8 @@ def function_spaces(self): if self._argument_type in const.VALID_OPERATOR_NAMES: # Return to before from to maintain expected ordering return [self.function_space_to, self.function_space_from] - if self._argument_type in const.VALID_SCALAR_NAMES: + if self._argument_type in (const.VALID_ARRAY_NAMES + + const.VALID_SCALAR_NAMES): return [] raise InternalError(f"Expected a valid argument type but got " f"'{self._argument_type}'.") @@ -714,6 +817,19 @@ def vector_size(self): ''' return self._vector_size + @property + def array_ndims(self): + ''' + 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): ''' Creates a string representation of the argument descriptor. This @@ -739,6 +855,9 @@ def __str__(self): if self._argument_type in const.VALID_FIELD_NAMES: res += (f" function_space[3]='{self._function_space1}'" + os.linesep) + elif self._argument_type in const.VALID_ARRAY_NAMES: + res += (f" array_ndims[3]='{self._array_ndims}'" + + os.linesep) elif self._argument_type in const.VALID_OPERATOR_NAMES: res += (f" function_space_to[3]='{self._function_space1}'" + os.linesep) diff --git a/src/psyclone/domain/lfric/lfric_constants.py b/src/psyclone/domain/lfric/lfric_constants.py index 9e95a698ea..b492694bc0 100644 --- a/src/psyclone/domain/lfric/lfric_constants.py +++ b/src/psyclone/domain/lfric/lfric_constants.py @@ -32,7 +32,7 @@ # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- # Author: J. Henrichs, Bureau of Meteorology -# Modified: I. Kavcic, Met Office +# Modified: I. Kavcic and L, Turner, Met Office # A. R. Porter, STFC Daresbury Laboratory # R. W. Ford, STFC Daresbury Laboratory @@ -78,12 +78,14 @@ def __init__(self): # Supported LFRic API argument types (scalars, fields, operators) LFRicConstants.VALID_SCALAR_NAMES = ["gh_scalar"] + LFRicConstants.VALID_ARRAY_NAMES = ["gh_scalar_array"] LFRicConstants.VALID_FIELD_NAMES = ["gh_field"] LFRicConstants.VALID_OPERATOR_NAMES = ["gh_operator", "gh_columnwise_operator"] LFRicConstants.VALID_ARG_TYPE_NAMES = \ LFRicConstants.VALID_FIELD_NAMES + \ LFRicConstants.VALID_OPERATOR_NAMES + \ + LFRicConstants.VALID_ARRAY_NAMES + \ LFRicConstants.VALID_SCALAR_NAMES # Mapping from argument type to the suffix used when creating @@ -100,6 +102,8 @@ def __init__(self): ["gh_real", "gh_integer", "gh_logical"] LFRicConstants.VALID_SCALAR_DATA_TYPES = \ LFRicConstants.VALID_ARG_DATA_TYPES + LFRicConstants.VALID_ARRAY_DATA_TYPES = \ + LFRicConstants.VALID_ARG_DATA_TYPES LFRicConstants.VALID_FIELD_DATA_TYPES = ["gh_real", "gh_integer"] LFRicConstants.VALID_OPERATOR_DATA_TYPES = ["gh_real"] @@ -108,6 +112,7 @@ def __init__(self): # Supported access types # gh_sum for scalars is restricted to iterates_over == 'dof' LFRicConstants.VALID_SCALAR_ACCESS_TYPES = ["gh_read", "gh_sum"] + LFRicConstants.VALID_ARRAY_ACCESS_TYPES = ["gh_read"] LFRicConstants.VALID_FIELD_ACCESS_TYPES = [ "gh_read", "gh_write", "gh_readwrite", "gh_inc", "gh_readinc"] LFRicConstants.VALID_OPERATOR_ACCESS_TYPES = [ @@ -153,6 +158,9 @@ def __init__(self): LFRicConstants.VALID_FIELD_INTRINSIC_TYPES = ["real", "integer", "logical"] + LFRicConstants.VALID_ARRAY_INTRINSIC_TYPES = ["real", "integer", + "logical"] + # ---------- Mapping from metadata data_type to Fortran intrinsic type LFRicConstants.MAPPING_DATA_TYPES = \ OrderedDict(zip(LFRicConstants.VALID_ARG_DATA_TYPES, diff --git a/src/psyclone/tests/domain/constants_test.py b/src/psyclone/tests/domain/constants_test.py index 802b326d8b..8652b04825 100644 --- a/src/psyclone/tests/domain/constants_test.py +++ b/src/psyclone/tests/domain/constants_test.py @@ -32,7 +32,7 @@ # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- # Author: J. Henrichs, Bureau of Meteorology -# Modified: I. Kavcic, Met Office +# Modified: I. Kavcic and L. Turner, Met Office # Modified: R. W. Ford, STFC Daresbury Lab # Modified: S. Siso, STFC Daresbury Lab @@ -55,7 +55,7 @@ def test_lfric_const(): # Don't test intrinsic_types, which comes from the config file assert lfric_const.VALID_ARG_TYPE_NAMES == ["gh_field", "gh_operator", "gh_columnwise_operator", - "gh_scalar"] + "gh_scalar_array", "gh_scalar"] assert lfric_const.VALID_SCALAR_NAMES == ["gh_scalar"] @@ -67,7 +67,7 @@ def test_lfric_const(): assert lfric_const.VALID_INTRINSIC_TYPES == "INVALID" assert lfric_const.VALID_ARG_TYPE_NAMES == ["gh_field", "gh_operator", "gh_columnwise_operator", - "gh_scalar"] + "gh_scalar_array", "gh_scalar"] assert lfric_const.VALID_SCALAR_NAMES == ["gh_scalar"] assert lfric_const.VALID_ARG_DATA_TYPES == ["gh_real", "gh_integer", "gh_logical"] 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 cd7d891902..edb07e069c 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 @@ -32,6 +32,7 @@ # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- # Author R. W. Ford, STFC Daresbury Lab +# Modifier L. Turner, Met Office '''Module containing tests for the CommonMetaArgMetadata class. @@ -58,10 +59,10 @@ def test_init_error(): # We split the check to accomodate for this. assert ("Can't instantiate abstract class CommonMetaArgMetadata with" in str(info.value)) - assert ("abstract methods" in str(info.value)) - assert ("_get_metadata" in str(info.value)) - assert ("check_access" in str(info.value)) - assert ("check_datatype" in str(info.value)) + assert "abstract methods" in str(info.value) + assert "_get_metadata" in str(info.value) + assert "check_access" in str(info.value) + assert "check_datatype" in str(info.value) # pylint: enable=abstract-class-instantiated diff --git a/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py new file mode 100644 index 0000000000..6bd3a841d8 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/kernel/scalar_array_arg_metadata_test.py @@ -0,0 +1,151 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2022-2024, Science and Technology Facilities Council +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author L. Turner, Met Office +# Modified A. Pirrie, Met Office + +'''Module containing tests for the ScalarArrayArgMetadata class. + +''' +import pytest + +from fparser.two import Fortran2003 + +from psyclone.domain.lfric.kernel import ScalarArrayArgMetadata + + +@pytest.mark.parametrize("datatype, access, array_ndims", [ + ("GH_REAL", "GH_READ", 1), ("gh_real", "gh_read", 1)]) +def test_create(datatype, access, array_ndims): + '''Test that an instance of ScalarArrayArgMetadata can be created + successfully. Also test that the arguments are case insensitive. + + ''' + array_arg = ScalarArrayArgMetadata(datatype, access, array_ndims) + assert isinstance(array_arg, ScalarArrayArgMetadata) + assert array_arg.form == "gh_scalar_array" + assert array_arg.datatype == "gh_real" + assert array_arg.access == "gh_read" + assert array_arg.array_ndims == 1 + + +@pytest.mark.parametrize("metadata", + ["arg_type(GH_SCALAR_ARRAY, GH_REAL, GH_READ, 2)"]) +def test_get_metadata(metadata): + '''Test that the _get_metadata class method works as expected ''' + fparser2_tree = ScalarArrayArgMetadata.create_fparser2( + metadata, Fortran2003.Part_Ref) + datatype, access, array_ndims = ScalarArrayArgMetadata._get_metadata( + fparser2_tree) + assert datatype == "GH_REAL" + assert access == "GH_READ" + assert array_ndims == 2 + + +@pytest.mark.parametrize("fortran_string", [ + "arg_type(GH_SCALAR_ARRAY, GH_REAL, GH_READ, 5)"]) +def test_fortran_string(fortran_string): + '''Test that the fortran_string method works as expected.''' + array_arg = ScalarArrayArgMetadata.create_from_fortran_string( + fortran_string) + result = array_arg.fortran_string() + assert result == fortran_string.lower() + + +@pytest.mark.parametrize("datatype", ["GH_REAL", "GH_INTEGER", "GH_LOGICAL"]) +def test_check_datatype(datatype): + '''Test the check_datatype method works as expected.''' + ScalarArrayArgMetadata.check_datatype(datatype) + with pytest.raises(ValueError) as info: + ScalarArrayArgMetadata.check_datatype("invalid") + assert ("The 'datatype descriptor' metadata should be a recognised value " + "(one of ['gh_real', 'gh_integer', 'gh_logical']) but found " + "'invalid'." in str(info.value)) + + +def test_check_access(): + '''Test the check_access method works as expected.''' + ScalarArrayArgMetadata.check_access("GH_READ") + with pytest.raises(ValueError) as info: + ScalarArrayArgMetadata.check_access("invalid") + assert ("The 'access descriptor' metadata should be a recognised value " + "(one of ['gh_read', 'gh_sum']) but found 'invalid'." + in str(info.value)) + + +def test_get_array_ndims(): + '''Test that the get_array_ndims method in the + ScalarArrayArgMetadata class works as expected. + + ''' + + fparser_tree = ScalarArrayArgMetadata.create_fparser2( + "arg_type(GH_SCALAR_ARRAY, GH_REAL, GH_READ, invalid)", + Fortran2003.Part_Ref) + with pytest.raises(ValueError) as info: + _ = ScalarArrayArgMetadata.get_array_ndims(fparser_tree) + assert ("The number of dimensions of a ScalarArray should be a string " + "containing an integer, but found 'invalid'." in str(info.value)) + + fparser_tree = ScalarArrayArgMetadata.create_fparser2( + "arg_type(GH_SCALAR_ARRAY, GH_REAL, GH_READ, 0)", Fortran2003.Part_Ref) + with pytest.raises(ValueError) as info: + _ = ScalarArrayArgMetadata.get_array_ndims(fparser_tree) + assert ("The number of dimensions of a ScalarArray should be an integer " + "greater than or equal to 1 but found 0." in str(info.value)) + + fparser_tree = ScalarArrayArgMetadata.create_fparser2( + "arg_type(GH_SCALAR_ARRAY, GH_REAL, GH_READ, 3)", Fortran2003.Part_Ref) + array_length = ScalarArrayArgMetadata.get_array_ndims(fparser_tree) + assert array_length == 3 + + +def test_array_ndims_setter_getter(): + '''Test that the array_ndims setter and getter work as expected, + including raising an exception if the value is invalid. + + ''' + array_arg = ScalarArrayArgMetadata("GH_REAL", "GH_READ", 1) + + with pytest.raises(TypeError) as info: + test_value = float(1.5) + array_arg.array_ndims = test_value + assert ("The type of value must be an integer, but found " + "input of type ." in str(info.value)) + + with pytest.raises(ValueError) as info: + test_value = -1 + array_arg.array_ndims = test_value + assert ("The number of dimensions of a ScalarArray should be an" + " integer greater than or equal to 1 but found -1." + in str(info.value)) diff --git a/src/psyclone/tests/domain/lfric/lfric_array_mdata_test.py b/src/psyclone/tests/domain/lfric/lfric_array_mdata_test.py new file mode 100644 index 0000000000..32fbd60182 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_array_mdata_test.py @@ -0,0 +1,313 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author L. Turner, Met Office + +''' +Module containing pytest tests for the general LFRic array arguments +functionality (e.g. metadata, parsing, invoke calls). +''' + +import os +import pytest +import fparser +from fparser import api as fpapi +from psyclone.domain.lfric import (LFRicArgDescriptor, LFRicConstants, + LFRicKernMetadata) +from psyclone.errors import InternalError +from psyclone.parse.utils import ParseError + +# Constants +BASE_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))), + "test_files", "lfric") +TEST_API = "lfric" + + +ARRAY_CODE = ''' +module testkern_mod + + type, extends(kernel_type) :: testkern_array_type + type(arg_type), meta_args(5) = & + (/ arg_type(gh_scalar_array, gh_real, gh_read, 1), & + arg_type(gh_scalar_array, gh_integer, gh_read, 2), & + arg_type(gh_scalar_array, gh_logical, gh_read, 4), & + arg_type(gh_operator, gh_real, gh_read, w2, w2), & + arg_type(gh_field, gh_real, gh_write, w3) & + /) + integer :: operates_on = cell_column + contains + procedure, nopass :: code => testkern_code + end type testkern_array_type +contains + subroutine testkern_code(a, b, c, d) + end subroutine testkern_code +end module testkern_mod +''' + + +def test_ad_array_init_wrong_argument_type(): + ''' Test that an error is raised if something other than a ScalarArray + is passed to the LFRicArgDescriptor._init_array() method. ''' + ast = fpapi.parse(ARRAY_CODE, ignore_comments=False) + name = "testkern_array_type" + metadata = LFRicKernMetadata(ast, name=name) + # Get an argument which is not a ScalarArray + wrong_arg = metadata._inits[3] + with pytest.raises(InternalError) as excinfo: + LFRicArgDescriptor( + wrong_arg, metadata.iterates_over, 0)._init_array(wrong_arg) + assert ("Expected a ScalarArray argument but got an argument of type " + "'gh_operator'." in str(excinfo.value)) + + +def test_ad_array_type_wrong_num_of_args(): + ''' Tests that an error is raised when the ScalarArray argument + descriptor metadata for a ScalarArray has fewer than 4 args. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_read)", 1) + ast = fpapi.parse(code, ignore_comments=False) + name = "testkern_array_type" + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("a 'meta_arg' entry must have 4 arguments if its first " + "argument is of ['gh_scalar_array'] type" in str(excinfo.value)) + + +def test_ad_array_invalid_data_type(): + ''' Tests that an error is raised when the argument descriptor + metadata for a scalar has an invalid data type. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + # check real array + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_unreal, gh_read, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + const = LFRicConstants() + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert (f"In the LFRic API the 2nd argument of a 'meta_arg' entry should " + f"be a valid data type (one of {const.VALID_ARRAY_DATA_TYPES}), " + f"but found 'gh_unreal' in 'arg_type(gh_scalar_array, gh_unreal, " + f"gh_read, 1)'." in str(excinfo.value)) + # check integer array + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_integer, gh_read, 2)", + "arg_type(gh_scalar_array, gh_frac, gh_read, 2)", 1) + ast = fpapi.parse(code, ignore_comments=False) + const = LFRicConstants() + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert (f"In the LFRic API the 2nd argument of a 'meta_arg' entry should " + f"be a valid data type (one of {const.VALID_ARRAY_DATA_TYPES}), " + f"but found 'gh_frac' in 'arg_type(gh_scalar_array, gh_frac, " + f"gh_read, 2)'." in str(excinfo.value)) + # check logical array + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_logical, gh_read, 4)", + "arg_type(gh_scalar_array, gh_illogical, gh_read, 4)", 1) + ast = fpapi.parse(code, ignore_comments=False) + const = LFRicConstants() + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert (f"In the LFRic API the 2nd argument of a 'meta_arg' entry should " + f"be a valid data type (one of {const.VALID_ARRAY_DATA_TYPES}), " + f"but found 'gh_illogical' in 'arg_type(gh_scalar_array, " + f"gh_illogical, gh_read, 4)'." in str(excinfo.value)) + + +def test_ad_array_init_wrong_data_type(monkeypatch): + ''' Test that an error is raised if an invalid data type + is passed to the LFRicArgDescriptor._init_array() method. ''' + ast = fpapi.parse(ARRAY_CODE, ignore_comments=False) + name = "testkern_array_type" + metadata = LFRicKernMetadata(ast, name=name) + # Get a ScalarArray argument descriptor and set a wrong data type + scalar_arg = metadata._inits[0] + scalar_arg.args[1].name = "gh_double" + const = LFRicConstants() + # Now try to trip the error by making the initial test think + # that 'gh_double' is actually a valid data type + monkeypatch.setattr( + target=LFRicConstants, name="VALID_ARG_DATA_TYPES", + value=LFRicConstants.VALID_ARG_DATA_TYPES + ["gh_double"]) + with pytest.raises(InternalError) as excinfo: + LFRicArgDescriptor( + scalar_arg, metadata.iterates_over, 0)._init_scalar(scalar_arg) + assert (f"Expected one of {const.VALID_ARRAY_DATA_TYPES} as the " + f"ScalarArray data type but got " + f"'gh_double'." in str(excinfo.value)) + + +def test_ad_array_type_no_write(): + ''' Tests that an error is raised when the argument descriptor + metadata for a scalar specifies 'GH_WRITE' access. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_write, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("ScalarArray arguments must have read-only ('gh_read') " + "access but found 'gh_write'" in str(excinfo.value)) + + +def test_ad_array_type_no_inc(): + ''' Tests that an error is raised when the argument descriptor + metadata for a scalar specifies 'GH_INC' access. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_inc, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("ScalarArray arguments must have read-only ('gh_read') " + "access but found 'gh_inc'" in str(excinfo.value)) + + +def test_ad_array_type_no_readwrite(): + ''' Tests that an error is raised when the argument descriptor + metadata for an array specifies 'GH_READWRITE' access. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_readwrite, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("ScalarArray arguments must have read-only ('gh_read') " + "access but found 'gh_readwrite'" in str(excinfo.value)) + + +def test_ad_array_type_no_sum(): + ''' Tests that an error is raised when the argument descriptor + metadata for an array specifies 'GH_SUM' access (reduction). ''' + fparser.logging.disable(fparser.logging.CRITICAL) + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_sum, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + name = "testkern_array_type" + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("ScalarArray arguments must have read-only ('gh_read') " + "access but found 'gh_sum'" in str(excinfo.value)) + + +def test_no_vector_array(): + ''' Tests that we raise an error when kernel metadata erroneously + specifies a vector scalar argument. ''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array*3, gh_real, gh_read, 1)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("vector notation is only supported for ['gh_field'] argument " + "types but found 'gh_scalar_array * 3'" in str(excinfo.value)) + + +@pytest.mark.parametrize("array_ind, array_type, array_ndims", [ + (0, "gh_real", 1), (1, "gh_integer", 2), (2, "gh_logical", 4)]) +def test_arg_descriptor_array(array_ind, array_type, array_ndims): + ''' Test that the LFRicArgDescriptor argument representation works + as expected for all three types of valid ScalarArray argument: + 'real', 'integer' and 'logical'. + + ''' + fparser.logging.disable(fparser.logging.CRITICAL) + ast = fpapi.parse(ARRAY_CODE, ignore_comments=False) + metadata = LFRicKernMetadata(ast, name="testkern_array_type") + array_descriptor = metadata.arg_descriptors[array_ind] + + # Assert correct string representation from LFRicArgDescriptor + result = str(array_descriptor) + expected_output = ( + f"LFRicArgDescriptor object\n" + f" argument_type[0]='gh_scalar_array'\n" + f" data_type[1]='{array_type}'\n" + f" access_descriptor[2]='gh_read'\n" + f" array_ndims[3]='{array_ndims}'") + assert expected_output in result + + # Check LFRicArgDescriptor argument properties + assert array_descriptor.argument_type == "gh_scalar_array" + assert array_descriptor.data_type == array_type + assert array_descriptor.array_ndims == array_ndims + assert array_descriptor.function_spaces == [] + assert str(array_descriptor.access) == "READ" + assert array_descriptor.mesh is None + assert array_descriptor.stencil is None + + +def test_n_not_integer(): + ''' Tests that we raise an error when n is not an integer''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_read, 0.5)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("the ScalarArray notation must be in the format 'n' " + "where 'n' is an integer, but '0.5' was found in " + "'arg_type(gh_scalar_array, gh_real, gh_read, 0.5)'." + in str(excinfo.value)) + + +def test_n_less_than_one(): + ''' Tests that we raise an error when n is less than 1''' + fparser.logging.disable(fparser.logging.CRITICAL) + name = "testkern_array_type" + code = ARRAY_CODE.replace( + "arg_type(gh_scalar_array, gh_real, gh_read, 1)", + "arg_type(gh_scalar_array, gh_real, gh_read, 0)", 1) + ast = fpapi.parse(code, ignore_comments=False) + with pytest.raises(ParseError) as excinfo: + _ = LFRicKernMetadata(ast, name=name) + assert ("the ScalarArray notation must be in the format 'n' " + "where 'n' is an integer >= 1. However, found n = '0' in " + "'arg_type(gh_scalar_array, gh_real, gh_read, 0)'." + in str(excinfo.value))