# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2017-2025, 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.
# -----------------------------------------------------------------------------
# Authors R. W. Ford, A. R. Porter and S. Siso, STFC Daresbury Lab
# Modified I. Kavcic, A. Coughtrie, L. Turner and O. Brunt, Met Office
# Modified J. Henrichs, Bureau of Meteorology
# Modified A. B. G. Chalk and N. Nobre, STFC Daresbury Lab
''' This module implements the PSyclone LFRic API by 1) specialising the
required base classes in parser.py (KernelType) and
adding a new class (LFRicFuncDescriptor) to capture function descriptor
metadata and 2) specialising the required base classes in psyGen.py
(PSy, Invokes, Invoke, InvokeSchedule, Loop, Kern, Inf, Arguments and
Argument). '''
from __future__ import annotations
import os
from enum import Enum
from collections import OrderedDict, namedtuple
from dataclasses import dataclass
from typing import Any, List, Optional
from psyclone import psyGen
from psyclone.configuration import Config
from psyclone.core import AccessType, Signature, SymbolicMaths
from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn
from psyclone.domain.lfric import (
FunctionSpace, KernCallAccArgList, KernCallArgList, LFRicCollection,
LFRicConstants, LFRicSymbolTable, LFRicKern,
LFRicTypes, LFRicLoop)
from psyclone.domain.lfric.lfric_invoke_schedule import LFRicInvokeSchedule
from psyclone.errors import GenerationError, InternalError, FieldNotFoundError
from psyclone.parse.kernel import getkerneldescriptors
from psyclone.parse.utils import ParseError
from psyclone.psyGen import (Arguments, DataAccess, InvokeSchedule, Kern,
KernelArgument, HaloExchange, GlobalSum)
from psyclone.psyir.frontend.fortran import FortranReader
from psyclone.psyir.nodes import (
Reference, ACCEnterDataDirective, ArrayOfStructuresReference,
StructureReference, Literal, IfBlock, Call, BinaryOperation, IntrinsicCall,
Assignment, ArrayReference, Loop, Container, DataNode, Schedule, Node)
from psyclone.psyir.symbols import (
INTEGER_TYPE, DataSymbol, DataType, DataTypeSymbol, ScalarType,
UnresolvedType, ContainerSymbol, ImportInterface, StructureType,
ArrayType, UnsupportedFortranType, ArgumentInterface)
# pylint: disable=too-many-lines
# --------------------------------------------------------------------------- #
# ========== First section : Parser specialisations and classes ============= #
# --------------------------------------------------------------------------- #
#
# ---------- Functions ------------------------------------------------------ #
def qr_basis_alloc_args(first_dim, basis_fn):
'''
Generate the list of dimensions required to allocate the
supplied basis/diff-basis function
:param str first_dim: the variable name for the first dimension
:param basis_fn: dict holding details on the basis function
we want to allocate
:type basis_fn: dict containing 'shape', 'fspace' and and 'qr_var' keys
holding the quadrature shape, FunctionSpace and name
of the associated quadrature variable (as specified in the
Algorithm layer), respectively
:return: list of dimensions to use to allocate array
:rtype: list of strings
:raises InternalError: if an unrecognised quadrature shape is encountered.
:raises NotImplementedError: if a quadrature shape other than \
"gh_quadrature_xyoz" is supplied.
'''
const = LFRicConstants()
if basis_fn["shape"] not in const.VALID_QUADRATURE_SHAPES:
raise InternalError(
f"Unrecognised shape ('{basis_fn['''shape''']}') specified in "
f"lfric.qr_basis_alloc_args(). Should be one of: "
f"{const.VALID_QUADRATURE_SHAPES}")
qr_var = "_" + basis_fn["qr_var"]
# Dimensionality of the basis arrays depends on the
# type of quadrature...
# if basis_fn["shape"] == "gh_quadrature_xyz":
# alloc_args = [first_dim, basis_fn["fspace"].ndf_name,
# "np_xyz"+"_"+basis_fn["qr_var"]]
if basis_fn["shape"] == "gh_quadrature_xyoz":
alloc_args = [first_dim, basis_fn["fspace"].ndf_name,
"np_xy"+qr_var, "np_z"+qr_var]
# elif basis_fn["shape"] == "gh_quadrature_xoyoz":
# alloc_args = [first_dim, basis_fn["fspace"].ndf_name,
# "np_x"+"_"+basis_fn["qr_var"],
# "np_y"+"_"+basis_fn["qr_var"],
# "np_z"+"_"+basis_fn["qr_var"]]
elif basis_fn["shape"] == "gh_quadrature_face":
alloc_args = [first_dim, basis_fn["fspace"].ndf_name,
"np_xyz"+qr_var, "nfaces"+qr_var]
elif basis_fn["shape"] == "gh_quadrature_edge":
alloc_args = [first_dim, basis_fn["fspace"].ndf_name,
"np_xyz"+qr_var, "nedges"+qr_var]
else:
raise NotImplementedError(
f"Unrecognised shape '{basis_fn['''shape''']}' specified in "
f"lfric.qr_basis_alloc_args(). Should be one of: "
f"{const.VALID_QUADRATURE_SHAPES}")
return alloc_args
# ---------- Classes -------------------------------------------------------- #
[docs]
class LFRicFuncDescriptor():
''' The LFRic API includes a function-space descriptor as
well as an argument descriptor which is not supported by the base
classes. This class captures the information specified in a
function-space descriptor. '''
def __init__(self, func_type):
self._func_type = func_type
if func_type.name != 'func_type':
raise ParseError(
f"In the lfric API each meta_func entry must be of type "
f"'func_type' but found '{func_type.name}'")
if len(func_type.args) < 2:
raise ParseError(
f"In the lfric API each meta_func entry must have at "
f"least 2 args, but found {len(func_type.args)}")
self._operator_names = []
const = LFRicConstants()
for idx, arg in enumerate(func_type.args):
if idx == 0: # first func_type arg
if arg.name not in const.VALID_FUNCTION_SPACE_NAMES:
raise ParseError(
f"In the LFRic API the 1st argument of a "
f"meta_func entry should be a valid function space "
f"name (one of {const.VALID_FUNCTION_SPACE_NAMES}), "
f"but found '{arg.name}' in '{func_type}'")
self._function_space_name = arg.name
else: # subsequent func_type args
if arg.name not in const.VALID_METAFUNC_NAMES:
raise ParseError(
f"In the lfric API, the 2nd argument and all "
f"subsequent arguments of a meta_func entry should "
f"be one of {const.VALID_METAFUNC_NAMES}, but found "
f"'{arg.name}' in '{func_type}'")
if arg.name in self._operator_names:
raise ParseError(
f"In the lfric API, it is an error to specify an "
f"operator name more than once in a meta_func entry, "
f"but '{arg.name}' is replicated in '{func_type}'")
self._operator_names.append(arg.name)
self._name = func_type.name
@property
def function_space_name(self):
''' Returns the name of the descriptors function space '''
return self._function_space_name
@property
def operator_names(self):
''' Returns a list of operators that are associated with this
descriptors function space '''
return self._operator_names
def __repr__(self):
return f"LFRicFuncDescriptor({self._func_type})"
def __str__(self):
res = "LFRicFuncDescriptor object" + os.linesep
res += f" name='{self._name}'" + os.linesep
res += f" nargs={len(self._operator_names)+1}" + os.linesep
res += f" function_space_name[{0}] = '{self._function_space_name}'" \
+ os.linesep
for idx, arg in enumerate(self._operator_names):
res += f" operator_name[{idx+1}] = '{arg}'" + \
os.linesep
return res
class RefElementMetaData():
'''
Class responsible for parsing reference-element metadata and storing
the properties that a kernel requires.
:param str kernel_name: name of the Kernel that the metadata is for.
:param type_declns: list of fparser1 parse tree nodes representing type \
declaration statements
:type type_declns: list of :py:class:`fparser.one.typedecl_statements.Type`
:raises ParseError: if an unrecognised reference-element property is found.
:raises ParseError: if a duplicate reference-element property is found.
'''
# pylint: disable=too-few-public-methods
class Property(Enum):
'''
Enumeration of the various properties of the Reference Element
(that a kernel can request). The names of each of these corresponds to
the names that must be used in kernel metadata.
'''
NORMALS_TO_HORIZONTAL_FACES = 1
NORMALS_TO_VERTICAL_FACES = 2
NORMALS_TO_FACES = 3
OUTWARD_NORMALS_TO_HORIZONTAL_FACES = 4
OUTWARD_NORMALS_TO_VERTICAL_FACES = 5
OUTWARD_NORMALS_TO_FACES = 6
def __init__(self, kernel_name, type_declns):
# The list of properties requested in the metadata (if any)
self.properties = []
re_properties = []
# Search the supplied list of type declarations for the one
# describing the reference-element properties required by the kernel.
for line in type_declns:
for entry in line.selector:
if entry == "reference_element_data_type":
# getkerneldescriptors raises a ParseError if the named
# element cannot be found.
re_properties = getkerneldescriptors(
kernel_name, line, var_name="meta_reference_element",
var_type="reference_element_data_type")
break
if re_properties:
# Optimisation - stop searching if we've found a type
# declaration for the reference-element data
break
try:
# The metadata entry is a declaration of a Fortran array of type
# reference_element_data_type. The initialisation of each member
# of this array is done as a Fortran structure constructor, the
# argument to which gives a property of the reference element.
for re_prop in re_properties:
for arg in re_prop.args:
self.properties.append(
self.Property[str(arg).upper()])
except KeyError as err:
# We found a reference-element property that we don't recognise.
# Sort for consistency when testing.
sorted_names = sorted([prop.name for prop in self.Property])
raise ParseError(
f"Unsupported reference-element property: '{arg}'. Supported "
f"values are: {sorted_names}") from err
# Check for duplicate properties
for prop in self.properties:
if self.properties.count(prop) > 1:
raise ParseError(f"Duplicate reference-element property "
f"found: '{prop}'.")
class MeshProperty(Enum):
'''
Enumeration of the various properties of the mesh that a kernel may
require (either named in metadata or implicitly, depending on the type
of kernel).
'''
# pylint: disable=too-few-public-methods
ADJACENT_FACE = 1
NCELL_2D = 2
NCELL_2D_NO_HALOS = 3
class MeshPropertiesMetaData():
'''
Parses any mesh-property kernel metadata and stores the properties that
a kernel requires.
:param str kernel_name: name of the kernel that the metadata is for.
:param type_declns: list of fparser1 parse tree nodes representing type \
declaration statements.
:type type_declns: list of :py:class:`fparser.one.typedecl_statements.Type`
:raises ParseError: if an unrecognised mesh property is found.
:raises ParseError: if a duplicate mesh property is found.
'''
# pylint: disable=too-few-public-methods
# The properties that may be specified in kernel metadata are a subset
# of the MeshProperty enumeration values.
supported_properties = [MeshProperty.ADJACENT_FACE]
def __init__(self, kernel_name, type_declns):
# The list of mesh properties requested in the metadata.
self.properties = []
mesh_props = []
# Search the supplied list of type declarations for the one
# describing the reference-element properties required by the kernel.
for line in type_declns:
for entry in line.selector:
if entry == "mesh_data_type":
# getkerneldescriptors raises a ParseError if the named
# element cannot be found.
mesh_props = getkerneldescriptors(
kernel_name, line, var_name="meta_mesh",
var_type="mesh_data_type")
break
if mesh_props:
# Optimisation - stop searching if we've found a type
# declaration for the mesh data
break
try:
# The metadata entry is a declaration of a Fortran array of type
# mesh_data_type. The initialisation of each member
# of this array is done as a Fortran structure constructor, the
# argument to which gives a mesh property.
for prop in mesh_props:
for arg in prop.args:
mesh_prop = MeshProperty[str(arg).upper()]
if mesh_prop not in self.supported_properties:
raise KeyError()
self.properties.append(mesh_prop)
except KeyError as err:
# We found a mesh property that we don't recognise or that
# is not supported.
supported_mesh_prop = [pr.name for pr in self.supported_properties]
raise ParseError(f"Unsupported mesh property in metadata: "
f"'{arg}'. Supported values are: "
f"{supported_mesh_prop}") from err
# Check for duplicate properties
for prop in self.properties:
if self.properties.count(prop) > 1:
raise ParseError(f"Duplicate mesh property "
f"found: '{prop}'.")
# --------------------------------------------------------------------------- #
# ========== Second section : PSy specialisations =========================== #
# --------------------------------------------------------------------------- #
# ---------- Classes -------------------------------------------------------- #
class LFRicMeshProperties(LFRicCollection):
'''
Holds all information on the the mesh properties required by either an
invoke or a kernel stub. Note that the creation of a suitable mesh
object is handled in the `LFRicMeshes` class. This class merely deals with
extracting the necessary properties from that object and providing them to
kernels.
:param node: kernel or invoke for which to manage mesh properties.
:type node: :py:class:`psyclone.domain.lfric.LFRicKern` or \
:py:class:`psyclone.domain.lfric.LFRicInvoke`
'''
def __init__(self, node):
super().__init__(node)
# The (ordered) list of mesh properties required by this invoke or
# kernel stub.
self._properties = []
for call in self.kernel_calls:
if call.mesh:
self._properties += [prop for prop in call.mesh.properties
if prop not in self._properties]
# Kernels that operate on the 'domain' need the number of columns,
# excluding those in the halo.
if call.iterates_over == "domain":
if MeshProperty.NCELL_2D_NO_HALOS not in self._properties:
self._properties.append(MeshProperty.NCELL_2D_NO_HALOS)
# Kernels performing CMA operations need the number of columns,
# including those in the halo.
if call.cma_operation:
if MeshProperty.NCELL_2D not in self._properties:
self._properties.append(MeshProperty.NCELL_2D)
# Store properties in symbol table
for prop in self._properties:
name_lower = prop.name.lower()
if prop == MeshProperty.ADJACENT_FACE:
# If it's adjacent face, make it a pointer array
self.symtab.find_or_create(
name_lower, symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
"integer(kind=i_def), pointer :: adjacent_face(:,:) "
"=> null()",
partial_datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
),
tag=name_lower)
else:
# Everything else is an integer
self.symtab.find_or_create(
name_lower, tag=name_lower,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
def kern_args(self, stub=False, var_accesses=None,
kern_call_arg_list=None):
# pylint: disable=too-many-locals, too-many-branches
'''
Provides the list of kernel arguments associated with the mesh
properties that the kernel requires. Optionally adds variable
access information if var_accesses is given.
:param bool stub: whether or not we are generating code for a \
kernel stub.
:param var_accesses: optional VariablesAccessMap instance to store \
the information about variable accesses.
:type var_accesses: \
:py:class:`psyclone.core.VariablesAccessMap`
:param kern_call_arg_list: an optional KernCallArgList instance \
used to store PSyIR representation of the arguments.
:type kern_call_arg_list: \
Optional[:py:class:`psyclone.domain.lfric.KernCallArgList`]
:returns: the kernel arguments associated with the mesh properties.
:rtype: list of str
:raises InternalError: if the class has been constructed for an \
invoke rather than a single kernel call.
:raises InternalError: if an unsupported mesh property is encountered.
'''
if not self._kernel:
raise InternalError(
"LFRicMeshProperties.kern_args() can only be called when "
"LFRicMeshProperties has been instantiated for a kernel "
"rather than an invoke.")
arg_list = []
for prop in self._properties:
if prop == MeshProperty.ADJACENT_FACE:
# Is this kernel already being passed the number of horizontal
# faces of the reference element?
has_nfaces = (
RefElementMetaData.Property.NORMALS_TO_HORIZONTAL_FACES
in self._kernel.reference_element.properties or
RefElementMetaData.Property.
OUTWARD_NORMALS_TO_HORIZONTAL_FACES
in self._kernel.reference_element.properties)
if not has_nfaces:
if kern_call_arg_list:
sym = kern_call_arg_list.\
append_integer_reference("nfaces_re_h")
name = sym.name
else:
lisdt = LFRicTypes("LFRicIntegerScalarDataType")()
name = self.symtab.\
find_or_create(
"nfaces_re_h", tag="nfaces_re_h",
symbol_type=DataSymbol,
datatype=lisdt
).name
arg_list.append(name)
if var_accesses is not None:
var_accesses.add_access(Signature(name),
AccessType.READ, self._kernel)
adj_face = "adjacent_face"
if not stub and kern_call_arg_list:
# Use the functionality in kern_call_arg_list to properly
# declare the symbol and to create a PSyIR reference for it
_, cell_ref = \
kern_call_arg_list.cell_ref_name(var_accesses)
adj_face_sym = kern_call_arg_list. \
append_array_reference(adj_face,
[":", cell_ref])
# Update the name in case there was a clash
adj_face = adj_face_sym.name
if var_accesses:
var_accesses.add_access(Signature(adj_face),
AccessType.READ, self._kernel,
[":", cell_ref])
if not stub:
adj_face = self.symtab.find_or_create_tag(
"adjacent_face").name
cell_name = "cell"
if self._kernel.is_coloured():
colour_name = "colour"
cmap_name = self.symtab.find_or_create_tag(
"cmap", root_name="cmap").name
adj_face += (f"(:,{cmap_name}({colour_name},"
f"{cell_name}))")
else:
adj_face += f"(:,{cell_name})"
arg_list.append(adj_face)
if var_accesses and not kern_call_arg_list:
# TODO #1320 Replace [1]
# The [1] just indicates that this variable is accessed
# as a rank 1 array. #1320 will improve this.
var_accesses.add_access(Signature(adj_face),
AccessType.READ, self._kernel,
[1])
else:
raise InternalError(
f"kern_args: found unsupported mesh property '{prop}' "
f"when generating arguments for kernel "
f"'{self._kernel.name}'. Only members of the MeshProperty "
f"Enum are permitted ({list(MeshProperty)}).")
return arg_list
def invoke_declarations(self):
'''
Creates the necessary declarations for variables needed in order to
provide mesh properties to a kernel call.
:raises InternalError: if an unsupported mesh property is found.
'''
super().invoke_declarations()
for prop in self._properties:
# The LFRicMeshes class will have created a mesh object so we
# don't need to do that here.
if prop == MeshProperty.ADJACENT_FACE:
self.symtab.lookup_with_tag("adjacent_face")
elif prop == MeshProperty.NCELL_2D_NO_HALOS:
self.symtab.find_or_create(
"ncell_2d_no_halos",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")(),
tag="ncell_2d_no_halos")
elif prop == MeshProperty.NCELL_2D:
self.symtab.find_or_create(
"ncell_2d", tag="ncell_2d",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
else:
raise InternalError(
f"Found unsupported mesh property '{prop}' when generating"
f" invoke declarations. Only members of the MeshProperty "
f"Enum are permitted ({list(MeshProperty)}).")
def stub_declarations(self):
'''
Creates the necessary declarations for the variables needed in order
to provide properties of the mesh in a kernel stub.
Note that argument order is redefined later by ArgOrdering.
:raises InternalError: if an unsupported mesh property is encountered.
'''
super().stub_declarations()
for prop in self._properties:
if prop == MeshProperty.ADJACENT_FACE:
adj_face = self.symtab.lookup("adjacent_face")
dimension = self.symtab.lookup("nfaces_re_h")
adj_face.datatype = ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[Reference(dimension)])
adj_face.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(adj_face)
elif prop == MeshProperty.NCELL_2D:
ncell_2d = self.symtab.lookup("ncell_2d")
ncell_2d.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(ncell_2d)
else:
raise InternalError(
f"Found unsupported mesh property '{prop}' when generating"
f" declarations for kernel stub. Only members of the "
f"MeshProperty Enum are permitted ({list(MeshProperty)})")
def initialise(self, cursor: int) -> int:
'''
Creates the PSyIR nodes for the initialisation of properties of
the mesh.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:raises InternalError: if an unsupported mesh property is encountered.
'''
const = LFRicConstants()
# Since colouring is applied via transformations, we have to check for
# it now, rather than when this class was first constructed.
need_colour_limits = False
need_colour_halo_limits = False
need_tilecolour_limits = False
need_tilecolour_halo_limits = False
for call in self.kernel_calls:
if call.is_coloured() and not call.is_intergrid:
loop = call.parent.parent
is_tiled = loop.loop_type == "cells_in_tile"
# Record which colour maps will be needed
if is_tiled:
has_halo = (loop.parent.parent.upper_bound_name in
const.HALO_ACCESS_LOOP_BOUNDS)
if has_halo:
need_tilecolour_halo_limits = True
else:
need_tilecolour_limits = True
else:
has_halo = (loop.upper_bound_name in
const.HALO_ACCESS_LOOP_BOUNDS)
if has_halo:
need_colour_halo_limits = True
else:
need_colour_limits = True
needs_colour_maps = (need_colour_limits or
need_colour_halo_limits or
need_tilecolour_limits or
need_tilecolour_halo_limits)
if not self._properties and not needs_colour_maps:
# If no mesh properties are required and there's no colouring
# (which requires a mesh object to lookup loop bounds) then we
# need do nothing.
return cursor
mesh = self.symtab.lookup_with_tag("mesh")
init_cursor = cursor
for prop in self._properties:
if prop == MeshProperty.ADJACENT_FACE:
adj_face = self.symtab.find_or_create_tag(
"adjacent_face")
assignment = Assignment.create(
lhs=Reference(adj_face),
rhs=Call.create(StructureReference.create(
mesh, ["get_adjacent_face"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
elif prop == MeshProperty.NCELL_2D_NO_HALOS:
symbol = self.symtab.find_or_create(
"ncell_2d_no_halos", tag="ncell_2d_no_halos",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
assignment = Assignment.create(
lhs=Reference(symbol),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_edge_cell"])),)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
elif prop == MeshProperty.NCELL_2D:
symbol = self.symtab.find_or_create(
"ncell_2d", tag="ncell_2d",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
assignment = Assignment.create(
lhs=Reference(symbol),
rhs=Call.create(StructureReference.create(
mesh, ["get_ncells_2d"])),)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
else:
raise InternalError(
f"Found unsupported mesh property '{str(prop)}' when "
f"generating initialisation code. Only members of the "
f"MeshProperty Enum are permitted ({list(MeshProperty)})")
self._invoke.schedule[init_cursor].append_preceding_comment(
"Initialise mesh properties")
if need_colour_halo_limits:
lhs = self.symtab.find_or_create_tag(
"last_halo_cell_all_colours")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_halo_cell_all_colours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if need_colour_limits:
lhs = self.symtab.find_or_create_tag(
"last_edge_cell_all_colours")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_edge_cell_all_colours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if need_tilecolour_halo_limits:
lhs = self.symtab.find_or_create_tag(
"last_halo_tile_per_colour")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_halo_tile_all_colours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
lhs = self.symtab.find_or_create_tag(
"last_halo_cell_per_colour_and_tile")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_halo_cell_all_colours_all_tiles"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if need_tilecolour_limits:
lhs = self.symtab.find_or_create_tag(
"last_edge_tile_per_colour")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_edge_tile_all_colours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
lhs = self.symtab.find_or_create_tag(
"last_edge_cell_per_colour_and_tile")
assignment = Assignment.create(
lhs=Reference(lhs),
rhs=Call.create(StructureReference.create(
mesh, ["get_last_edge_cell_all_colours_all_tiles"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
return cursor
class LFRicReferenceElement(LFRicCollection):
'''
Holds all information on the properties of the Reference Element
required by an Invoke or a Kernel stub.
:param node: Kernel or Invoke for which to manage Reference-Element \
properties.
:type node: :py:class:`psyclone.domain.lfric.LFRicKern` or \
:py:class:`psyclone.lfric.LFRicInvoke`
:raises InternalError: if an unsupported reference-element property \
is encountered.
'''
# pylint: disable=too-many-instance-attributes
def __init__(self, node):
# pylint: disable=too-many-branches, too-many-statements
super().__init__(node)
# Create a union of the reference-element properties required by all
# kernels in this invoke. Use a list to preserve the order in the
# kernel metadata (in the case of a kernel stub) and remove duplicate
# entries by using OrderedDict.
self._properties = []
self._nfaces_h_required = False
for call in self.kernel_calls:
if call.reference_element:
self._properties.extend(call.reference_element.properties)
if call.mesh and call.mesh.properties:
# If a kernel requires a property of the mesh then it will
# also require the number of horizontal faces of the
# reference element.
self._nfaces_h_required = True
if not (self._properties or self._nfaces_h_required):
return
if self._properties:
self._properties = list(OrderedDict.fromkeys(self._properties))
symtab = self.symtab
# Create and store symbol for the reference element object
self._ref_elem_symbol = None
# Initialise names for the properties of the reference element object:
# Number of horizontal/vertical/all faces,
self._nfaces_h_symbol = None
self._nfaces_v_symbol = None
self._nfaces_symbol = None
# Horizontal normals to faces,
self._horiz_face_normals_symbol = None
self._horiz_face_out_normals_symbol = None
# Vertical normals to faces,
self._vert_face_normals_symbol = None
self._vert_face_out_normals_symbol = None
# All normals to faces.
self._face_normals_symbol = None
self._face_out_normals_symbol = None
# Store argument properties for kernel calls and stub declarations
# and argument list
self._arg_properties = OrderedDict()
# Populate and check reference element properties
# Provide no. of horizontal faces if required
if (RefElementMetaData.Property.NORMALS_TO_HORIZONTAL_FACES
in self._properties or
RefElementMetaData.Property.OUTWARD_NORMALS_TO_HORIZONTAL_FACES
in self._properties or
self._nfaces_h_required):
self._nfaces_h_symbol = symtab.find_or_create(
"nfaces_re_h", tag="nfaces_re_h", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
# Provide no. of vertical faces if required
if (RefElementMetaData.Property.NORMALS_TO_VERTICAL_FACES
in self._properties or
RefElementMetaData.Property.OUTWARD_NORMALS_TO_VERTICAL_FACES
in self._properties):
self._nfaces_v_symbol = symtab.find_or_create(
"nfaces_re_v", tag="nfaces_re_v", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
# Provide no. of all faces if required
if (RefElementMetaData.Property.NORMALS_TO_FACES
in self._properties or
RefElementMetaData.Property.OUTWARD_NORMALS_TO_FACES
in self._properties):
self._nfaces_symbol = symtab.find_or_create(
"nfaces_re", tag="nfaces_re", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
# Now the arrays themselves, in the order specified in the
# kernel metadata (in the case of a kernel stub)
for prop in self._properties:
# Provide horizontal normals to faces
if prop == RefElementMetaData.Property.NORMALS_TO_HORIZONTAL_FACES:
name = "normals_to_horiz_faces"
self._horiz_face_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._horiz_face_normals_symbol not in self._arg_properties:
self._arg_properties[self._horiz_face_normals_symbol] = \
self._nfaces_h_symbol
# Provide horizontal normals to "outward" faces
elif prop == (RefElementMetaData.Property.
OUTWARD_NORMALS_TO_HORIZONTAL_FACES):
name = "out_normals_to_horiz_faces"
self._horiz_face_out_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._horiz_face_out_normals_symbol not in \
self._arg_properties:
self._arg_properties[self._horiz_face_out_normals_symbol] \
= self._nfaces_h_symbol
elif prop == (RefElementMetaData.Property.
NORMALS_TO_VERTICAL_FACES):
name = "normals_to_vert_faces"
self._vert_face_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._vert_face_normals_symbol not in self._arg_properties:
self._arg_properties[self._vert_face_normals_symbol] = \
self._nfaces_v_symbol
# Provide vertical normals to "outward" faces
elif prop == (RefElementMetaData.Property.
OUTWARD_NORMALS_TO_VERTICAL_FACES):
name = "out_normals_to_vert_faces"
self._vert_face_out_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._vert_face_out_normals_symbol not in \
self._arg_properties:
self._arg_properties[self._vert_face_out_normals_symbol] \
= self._nfaces_v_symbol
# Provide normals to all faces
elif prop == RefElementMetaData.Property.NORMALS_TO_FACES:
name = "normals_to_faces"
self._face_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._face_normals_symbol not in self._arg_properties:
self._arg_properties[self._face_normals_symbol] = \
self._nfaces_symbol
# Provide vertical normals to all "outward" faces
elif prop == RefElementMetaData.Property.OUTWARD_NORMALS_TO_FACES:
name = "out_normals_to_faces"
self._face_out_normals_symbol = \
symtab.find_or_create(
name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag=name)
if self._face_out_normals_symbol not in \
self._arg_properties:
self._arg_properties[self._face_out_normals_symbol] = \
self._nfaces_symbol
else:
all_props = [str(sprop)
for sprop in RefElementMetaData.Property]
raise InternalError(
f"Unsupported reference-element property ('{prop}') "
f"found when generating arguments for kernel "
f"'{self._kernel.name}'. Supported properties are: "
f"{all_props}")
def kern_args(self):
'''
:returns: the argument list for kernel call/stub arguments.
:rtype: List[str]
'''
argdict = self._arg_properties
# Remove duplicate "nfaces" by using OrderedDict
nfaces = list(OrderedDict.fromkeys(argdict.values()))
kern_args = nfaces + list(argdict.keys())
return [sym.name for sym in kern_args]
def kern_args_symbols(self):
'''
:returns: the argument symbol list for kernel call/stub arguments.
:rtype: List[:py:class:`psyclone.psyir.symbols.Symbol`]
'''
argdict = self._arg_properties
# Remove duplicate "nfaces" by using OrderedDict
nfaces = list(OrderedDict.fromkeys(argdict.values()))
return nfaces + list(argdict.keys())
def invoke_declarations(self):
'''
Create the necessary declarations for the variables needed in order
to provide properties of the reference element in a Kernel call.
'''
super().invoke_declarations()
if not self._properties and not self._nfaces_h_required:
# No reference-element properties required
return
const = LFRicConstants()
refelem_type = const.REFELEMENT_TYPE_MAP["refelement"]["type"]
refelem_mod = const.REFELEMENT_TYPE_MAP["refelement"]["module"]
mod = ContainerSymbol(refelem_mod)
self.symtab.add(mod)
self.symtab.add(
DataTypeSymbol(refelem_type, datatype=StructureType(),
interface=ImportInterface(mod)))
self._ref_elem_symbol = self.symtab.find_or_create_tag(
"reference_element")
self._ref_elem_symbol.specialise(
DataSymbol,
datatype=UnsupportedFortranType(
f"class({refelem_type}), pointer :: "
f"{self._ref_elem_symbol.name} => null()"))
def stub_declarations(self):
'''
Create the necessary declarations for the variables needed in order
to provide properties of the reference element in a Kernel stub.
Note that argument order is redefined later by ArgOrdering.
'''
super().stub_declarations()
if not (self._properties or self._nfaces_h_required):
return
# Declare the necessary scalars (duplicates are ignored)
scalars = list(self._arg_properties.values())
nfaces_h = self.symtab.find_or_create(
"nfaces_re_h", tag="nfaces_re_h",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
if self._nfaces_h_required and nfaces_h not in scalars:
scalars.append(nfaces_h)
for nface in scalars:
sym = self.symtab.find_or_create(
nface.name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
sym.interface = ArgumentInterface(ArgumentInterface.Access.READ)
self.symtab.append_argument(sym)
# Declare the necessary arrays
for arr, sym in self._arg_properties.items():
arrsym = self.symtab.lookup(arr.name)
arrsym.datatype = ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[Literal("3", INTEGER_TYPE), Reference(sym)])
arrsym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(arrsym)
def initialise(self, cursor: int) -> int:
'''
Creates the PSyIR nodes representing the necessary initialisation
code for properties of the reference element.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
if not (self._properties or self._nfaces_h_required):
return cursor
mesh_obj = self.symtab.find_or_create_tag("mesh")
ref_element = self._ref_elem_symbol
stmt = Assignment.create(
lhs=Reference(ref_element),
rhs=Call.create(
StructureReference.create(
mesh_obj, ["get_reference_element"])),
is_pointer=True)
stmt.preceding_comment = (
"Get the reference element and query its properties"
)
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._nfaces_h_symbol:
stmt = Assignment.create(
lhs=Reference(self._nfaces_h_symbol),
rhs=Call.create(
StructureReference.create(
ref_element, ["get_number_horizontal_faces"])))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._nfaces_v_symbol:
stmt = Assignment.create(
lhs=Reference(self._nfaces_v_symbol),
rhs=Call.create(
StructureReference.create(
ref_element, ["get_number_vertical_faces"])))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._nfaces_symbol:
stmt = Assignment.create(
lhs=Reference(self._nfaces_symbol),
rhs=Call.create(
StructureReference.create(
ref_element, ["get_number_faces"])))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._horiz_face_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element, ["get_normals_to_horizontal_faces"]))
stmt.addchild(Reference(self._horiz_face_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._horiz_face_out_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element,
["get_outward_normals_to_horizontal_faces"]))
stmt.addchild(Reference(self._horiz_face_out_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._vert_face_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element,
["get_normals_to_vertical_faces"]))
stmt.addchild(Reference(self._vert_face_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._vert_face_out_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element,
["get_outward_normals_to_vertical_faces"]))
stmt.addchild(Reference(self._vert_face_out_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._face_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element,
["get_normals_to_faces"]))
stmt.addchild(Reference(self._face_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
if self._face_out_normals_symbol:
stmt = Call.create(
StructureReference.create(
ref_element,
["get_outward_normals_to_faces"]))
stmt.addchild(Reference(self._face_out_normals_symbol))
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
return cursor
[docs]
class LFRicFunctionSpaces(LFRicCollection):
'''
Handles the declaration and initialisation of all function-space-related
quantities required by an Invoke.
:param invoke: the Invoke or Kernel object.
'''
def __init__(self, kern_or_invoke):
super().__init__(kern_or_invoke)
if self._invoke:
self._function_spaces = self._invoke.unique_fss()[:]
else:
self._function_spaces = self.kernel_calls[0].arguments.unique_fss
self._var_list = []
# Loop over all unique function spaces used by our kernel(s)
for function_space in self._function_spaces:
# We need ndf for a space if a kernel operates on cell-columns,
# has a field or operator on that space and is not a
# CMA kernel performing a matrix-matrix operation.
if self._invoke and not self._dofs_only or \
self._kernel and self._kernel.cma_operation != "matrix-matrix":
self._var_list.append(function_space.ndf_name)
# If there is a field on this space then add undf to list
# to declare later. However, if the invoke contains only
# kernels that operate on dofs and distributed memory is
# enabled then the number of dofs is obtained from the
# field proxy and undf is not required.
if self._invoke and self._invoke.field_on_space(function_space):
if not (self._dofs_only and Config.get().distributed_memory):
self._var_list.append(function_space.undf_name)
elif self._kernel and \
function_space.field_on_space(self._kernel.arguments):
self._var_list.append(function_space.undf_name)
[docs]
def stub_declarations(self):
'''
Add function-space-related declarations to a Kernel stub.
Note that argument order is redefined later by ArgOrdering.
'''
super().stub_declarations()
for var in self._var_list:
arg = self.symtab.find_or_create(
var, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
arg.interface = ArgumentInterface(ArgumentInterface.Access.READ)
self.symtab.append_argument(arg)
[docs]
def invoke_declarations(self):
'''
Add function-space-related declarations to a PSy-layer routine.
'''
super().invoke_declarations()
for var in self._var_list:
self.symtab.new_symbol(
var,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
[docs]
def initialise(self, cursor: int) -> int:
'''
Create the code that initialises function-space quantities.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
# Loop over all unique function spaces used by the kernels in
# the invoke
for function_space in self._function_spaces:
# Initialise information associated with this function space.
# If we have 1+ kernels that operate on cell-columns then we
# will need ndf and undf. If we don't then we only need undf
# (for the upper bound of the loop over dofs) if we're not
# doing DM.
# Find argument proxy name used to dereference the argument
arg = self._invoke.arg_for_funcspace(function_space)
# Initialise ndf for this function space.
if not self._dofs_only:
ndf_name = function_space.ndf_name
assignment = Assignment.create(
lhs=Reference(self.symtab.lookup(ndf_name)),
rhs=arg.generate_method_call(
"get_ndf", function_space=function_space))
assignment.preceding_comment = (
f"Initialise number of DoFs for "
f"{function_space.mangled_name}")
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# If there is a field on this space then initialise undf
# for this function space. However, if the invoke contains
# only kernels that operate on dofs and distributed
# memory is enabled then the number of dofs is obtained
# from the field proxy and undf is not required.
if not (self._dofs_only and Config.get().distributed_memory):
if self._invoke.field_on_space(function_space):
undf_name = function_space.undf_name
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(self.symtab.lookup(undf_name)),
rhs=arg.generate_method_call(
"get_undf", function_space=function_space)),
cursor)
cursor += 1
return cursor
[docs]
class LFRicProxies(LFRicCollection):
'''
Handles all proxy-related declarations and initialisation. Unlike other
sub-classes of LFRicCollection, we do not have to handle Kernel-stub
generation since Kernels know nothing about proxies.
An instance of this class is instantiated for each Invoke before the
PSy Layer is constructed. For each unique field or operator argument to
a kernel in the Invoke it:
* Creates a DataSymbol for the corresponding proxy;
* Creates a DataSymbol for the pointer to the data array accessed via
the proxy. If the argument is a field vector then a DataSymbol is
created for each component of the vector;
* Tags that DataSymbol so that the correct symbol can always be looked
up, irrespective of any name clashes;
Note that since the Fortran standard forbids (Note 12.34 in the
Fortran2008 standard) aliasing of effective arguments that are written to,
the set of unique kernel arguments must refer to unique memory locations
or to those that are read only.
'''
def __init__(self, node):
super().__init__(node)
const = LFRicConstants()
real_field_args = self._invoke.unique_declarations(
argument_types=const.VALID_FIELD_NAMES,
intrinsic_type=const.MAPPING_DATA_TYPES["gh_real"])
int_field_args = self._invoke.unique_declarations(
argument_types=const.VALID_FIELD_NAMES,
intrinsic_type=const.MAPPING_DATA_TYPES["gh_integer"])
op_args = self._invoke.unique_declarations(
argument_types=const.VALID_OPERATOR_NAMES)
# We put precision Symbols in the Container symbol table.
ctable = self._invoke.schedule.parent.symbol_table
for arg in real_field_args + int_field_args + op_args:
# Create symbols that we will associate with the internal
# data arrays of fields, field vectors and LMA operators.
if arg.argument_type == "gh_columnwise_operator":
# CMA operators are handled by the LFRicCMAOperators class.
continue
ctable.add_lfric_precision_symbol(arg.precision)
intrinsic_type = "integer" if arg in int_field_args else "real"
suffix = const.ARG_TYPE_SUFFIX_MAPPING[arg.argument_type]
if arg.vector_size > 1:
for idx in range(1, arg.vector_size+1):
# Make sure we're going to create a Symbol with a unique
# name.
new_name = self.symtab.next_available_name(
f"{arg.name}_{idx}_{suffix}")
tag = f"{arg.name}_{idx}:{suffix}"
# The data for a field lives in a rank-1 array.
self._add_symbol(new_name, tag, intrinsic_type, arg, 1)
else:
# Make sure we're going to create a Symbol with a unique
# name (since this is hardwired into the
# UnsupportedFortranType).
tag = f"{arg.name}:{suffix}"
new_name = self.symtab.next_available_name(
f"{arg.name}_{suffix}")
# The data for an operator lives in a rank-3 array.
rank = 1 if arg not in op_args else 3
self._add_symbol(new_name, tag, intrinsic_type, arg, rank)
def _add_symbol(self, name, tag, intrinsic_type, arg, rank):
'''
Creates a new DataSymbol representing either an LFRic field or
operator and adds it to the SymbolTable associated with this class.
The Symbol is of UnsupportedFortranType because it is a pointer
to the internal data array and the PSyIR does not support pointers. The
remainder of the type information is fully supplied in the
`partial_datatype` property of the UnsupportedFortranType.
The supplied Symbol name is assumed not to already exist in the
SymbolTable (e.g. it is obtained with the `next_available_name` method
of SymbolTable) because it is used in constructing the
UnsupportedFortranType which must be done before the Symbol is created.
:param str name: the name of the new Symbol.
:param str tag: the tag to associate with the new Symbol.
:param str intrinsic_type: whether the Symbol represents "real" or
"integer" data.
:param arg: the metadata description of the associated kernel argument.
:type arg: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param int rank: the rank of the array represented by the Symbol.
'''
if intrinsic_type == "real":
lfric_type = "LFRicRealScalarDataType"
else:
lfric_type = "LFRicIntegerScalarDataType"
precision = LFRicConstants().precision_for_type(arg.data_type)
array_type = ArrayType(
LFRicTypes(lfric_type)(precision),
[ArrayType.Extent.DEFERRED]*rank)
# Since the PSyIR doesn't have the pointer concept, we have
# to have an UnsupportedFortranType.
index_str = ",".join(rank*[":"])
dtype = UnsupportedFortranType(
f"{intrinsic_type}(kind={arg.precision}), pointer, "
f"dimension({index_str}) :: {name} => null()",
partial_datatype=array_type)
try:
self.symtab.new_symbol(name,
symbol_type=DataSymbol,
datatype=dtype,
tag=tag)
except KeyError:
# The tag already exists and therefore we don't need to do
# anything. This can happen if the Symbol Table has already
# been populated by a previous call to this constructor. Even if
# this is not the case, within a single Invoke we can have user-
# supplied kernels that accept a full field-vector as argument
# but also individual components of that vector might
# be passed to Builtins. Therefore a clash with an
# existing tag may occur which we can safely ignore.
pass
[docs]
def invoke_declarations(self):
'''
Insert declarations of all proxy-related quantities into the PSy layer.
'''
super().invoke_declarations()
const = LFRicConstants()
table = self.symtab
# Declarations of real and integer field proxies
# Filter field arguments by intrinsic type
real_field_args = self._invoke.unique_declarations(
argument_types=const.VALID_FIELD_NAMES,
intrinsic_type=const.MAPPING_DATA_TYPES["gh_real"])
int_field_args = self._invoke.unique_declarations(
argument_types=const.VALID_FIELD_NAMES,
intrinsic_type=const.MAPPING_DATA_TYPES["gh_integer"])
# Create a field argument map that splits the (real and
# integer) fields into their different datatypes for their
# proxy's
field_datatype_map = OrderedDict()
for arg in real_field_args + int_field_args:
try:
field_datatype_map[
(arg.proxy_data_type, arg.module_name)].append(arg)
except KeyError:
# This datatype has not been seen before so create a
# new entry
field_datatype_map[
(arg.proxy_data_type, arg.module_name)] = [arg]
# Add the Invoke subroutine declarations for the different
# field-type proxies
for (fld_type, fld_mod), args in field_datatype_map.items():
fld_mod_symbol = table.node.parent.symbol_table.lookup(fld_mod)
fld_type_sym = table.node.parent.symbol_table.new_symbol(
fld_type,
symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(fld_mod_symbol))
for arg in args:
if arg._vector_size > 1:
decl_type = ArrayType(fld_type_sym, [arg._vector_size])
else:
decl_type = fld_type_sym
table.new_symbol(arg.proxy_name,
symbol_type=DataSymbol,
datatype=decl_type)
# Declarations of LMA operator proxies
op_args = self._invoke.unique_declarations(
argument_types=["gh_operator"])
# Filter operators by their proxy datatype
operators_datatype_map = OrderedDict()
for op_arg in op_args:
try:
operators_datatype_map[op_arg.proxy_data_type].append(op_arg)
except KeyError:
# This proxy datatype has not been seen before so
# create new entry
operators_datatype_map[op_arg.proxy_data_type] = [op_arg]
# Declare the operator proxies
for operator_datatype, operators_list in \
operators_datatype_map.items():
mod_name = operators_list[0].module_name
mod_st = table.node.parent.symbol_table
fld_mod_symbol = mod_st.lookup(mod_name)
op_datatype_symbol = mod_st.find_or_create(
operator_datatype,
symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(fld_mod_symbol))
for op in operators_list:
table.new_symbol(op.proxy_declaration_name,
symbol_type=DataSymbol,
datatype=op_datatype_symbol)
# Declarations of CMA operator proxies
cma_op_args = self._invoke.unique_declarations(
argument_types=["gh_columnwise_operator"])
if cma_op_args:
op_type = cma_op_args[0].proxy_data_type
mod_name = cma_op_args[0].module_name
mod_st = table.node.parent.symbol_table
fld_mod_symbol = mod_st.lookup(mod_name)
op_datatype_symbol = mod_st.find_or_create(
op_type,
symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(fld_mod_symbol))
for arg in cma_op_args:
table.new_symbol(arg.proxy_declaration_name,
symbol_type=DataSymbol,
datatype=op_datatype_symbol)
[docs]
def initialise(self, cursor: int) -> int:
'''
Insert code into the PSy layer to initialise all necessary proxies.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:raises InternalError: if a kernel argument of an unrecognised type
is encountered.
'''
init_cursor = cursor
for arg in self._invoke.psy_unique_vars:
# We don't have proxies for scalars
if arg.is_scalar:
continue
const = LFRicConstants()
suffix = const.ARG_TYPE_SUFFIX_MAPPING[arg.argument_type]
if arg.vector_size > 1:
# the range function below returns values from
# 1 to the vector size which is what we
# require in our Fortran code
for idx in range(1, arg.vector_size+1):
self._invoke.schedule.addchild(
Assignment.create(
lhs=ArrayReference.create(
self.symtab.lookup(arg.proxy_name),
[Literal(str(idx), INTEGER_TYPE)]),
rhs=Call.create(ArrayOfStructuresReference.create(
self.symtab.lookup(arg.name),
[Literal(str(idx), INTEGER_TYPE)],
["get_proxy"]))),
cursor)
cursor += 1
symbol = self.symtab.lookup_with_tag(
f"{arg.name}_{idx}:{suffix}")
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(symbol),
rhs=ArrayOfStructuresReference.create(
self.symtab.lookup(arg.proxy_name),
[Literal(str(idx), INTEGER_TYPE)],
["data"]),
is_pointer=True),
cursor)
cursor += 1
else:
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(
self.symtab.find_or_create(arg.proxy_name)),
rhs=Call.create(StructureReference.create(
self.symtab.lookup(arg.name), ["get_proxy"]))),
cursor)
cursor += 1
if arg.is_field:
symbol = self.symtab.lookup_with_tag(
f"{arg.name}:{suffix}")
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(symbol),
rhs=StructureReference.create(
self.symtab.lookup(arg.proxy_name), ["data"]),
is_pointer=True),
cursor)
cursor += 1
elif arg.is_operator:
if arg.argument_type == "gh_columnwise_operator":
# CMA operator arguments are handled in
# LFRicCMAOperators
pass
elif arg.argument_type == "gh_operator":
symbol = self.symtab.lookup_with_tag(
f"{arg.name}:{suffix}")
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(symbol),
rhs=StructureReference.create(
self.symtab.lookup(arg.proxy_name),
["local_stencil"]),
is_pointer=True),
cursor)
cursor += 1
else:
raise InternalError(
f"Kernel argument '{arg.name}' is a recognised "
f"operator but its type ('{arg.argument_type}') is"
f" not supported by LFRicProxies.initialise()")
else:
raise InternalError(
f"Kernel argument '{arg.name}' of type "
f"'{arg.argument_type}' not "
f"handled in LFRicProxies.initialise()")
if cursor > init_cursor:
self._invoke.schedule[init_cursor].preceding_comment = (
"Initialise field and/or operator proxies")
return cursor
[docs]
class LFRicLMAOperators(LFRicCollection):
'''
Handles all entities associated with Local-Matrix-Assembly Operators.
'''
[docs]
def stub_declarations(self):
'''
Declare all LMA-related quantities in a Kernel stub. Note that argument
order will be defined later by ArgOrdering.
'''
super().stub_declarations()
lma_args = psyGen.args_filter(
self._kernel.arguments.args, arg_types=["gh_operator"])
if lma_args:
arg = self.symtab.find_or_create(
"cell", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
arg.interface = ArgumentInterface(ArgumentInterface.Access.READ)
self.symtab.append_argument(arg)
for arg in lma_args:
size = arg.name+"_ncell_3d"
op_dtype = arg.intrinsic_type
op_kind = arg.precision
size_sym = self.symtab.find_or_create(
size, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
size_sym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(size_sym)
ndf_name_to = self.symtab.lookup(
arg.function_space_to.ndf_name)
ndf_name_from = self.symtab.lookup(
arg.function_space_from.ndf_name)
# Create the PSyIR intrinsic DataType
kind_sym = self.symtab.find_or_create(
op_kind, symbol_type=DataSymbol, datatype=UnresolvedType(),
interface=ImportInterface(
self.symtab.lookup("constants_mod")))
if op_dtype == "real":
intr_type = ScalarType(ScalarType.Intrinsic.REAL,
Reference(kind_sym))
elif op_dtype == "integer":
intr_type = ScalarType(ScalarType.Intrinsic.INTEGER,
Reference(kind_sym))
else:
raise NotImplementedError(
f"Only REAL and INTEGER LMA Operator types are supported, "
f"but found '{op_dtype}'")
if arg.intent == "in":
intent = ArgumentInterface.Access.READ
elif arg.intent == "inout":
intent = ArgumentInterface.Access.READWRITE
# No need for else as arg.intent only returns in/inout or errors
arg_sym = self.symtab.find_or_create(
arg.name, symbol_type=DataSymbol,
datatype=ArrayType(intr_type, [
Reference(size_sym),
Reference(ndf_name_to),
Reference(ndf_name_from),
]))
arg_sym.interface = ArgumentInterface(intent)
self.symtab.append_argument(arg_sym)
[docs]
def invoke_declarations(self):
'''
Declare all LMA-related quantities in a PSy-layer routine.
Note: PSy layer in LFRic does not modify the LMA operator objects.
Hence, their Fortran intents are always "in" (the data updated in the
kernels is only pointed to from the LMA operator object and is thus
not a part of the object).
'''
super().invoke_declarations()
# Add the Invoke subroutine argument declarations for operators
op_args = self._invoke.unique_declarations(
argument_types=["gh_operator"])
# Update the operator intents
for arg in op_args:
symbol = self.symtab.lookup(arg.declaration_name)
symbol.interface = ArgumentInterface(ArgumentInterface.Access.READ)
[docs]
class LFRicCMAOperators(LFRicCollection):
'''
Holds all information on the Column-Matrix-Assembly operators
required by an Invoke or Kernel stub.
:param node: either an Invoke schedule or a single Kernel object.
:type node: :py:class:`psyclone.lfric.LFRicSchedule` or \
:py:class:`psyclone.domain.lfric.LFRicKern`
'''
# The scalar parameters that must be passed along with a CMA operator
# if its 'to' and 'from' spaces are the same
cma_same_fs_params = ["nrow", "bandwidth", "alpha",
"beta", "gamma_m", "gamma_p"]
# The scalar parameters that must be passed along with a CMA operator
# if its 'to' and 'from' spaces are different
cma_diff_fs_params = ["nrow", "ncol", "bandwidth", "alpha",
"beta", "gamma_m", "gamma_p"]
def __init__(self, node):
super().__init__(node)
# Look at every kernel call and generate a set of
# the unique CMA operators involved. For each one we create a
# dictionary entry. The key is the name of the CMA argument in the
# PSy layer and the entry is itself another dictionary containing
# two entries: the first 'arg' is the CMA argument object and the
# second 'params' is the list of integer variables associated with
# that CMA operator. The contents of this list depend on whether
# or not the to/from function spaces of the CMA operator are the
# same.
self._cma_ops = OrderedDict()
# You can't index into an OrderedDict so we keep a separate ref
# to the first CMA argument we find.
self._first_cma_arg = None
for call in self.kernel_calls:
if call.cma_operation:
# Get a list of all of the CMA arguments to this call
cma_args = psyGen.args_filter(
call.arguments.args,
arg_types=["gh_columnwise_operator"])
# Create a dictionary entry for each argument that we
# have not already seen
for arg in cma_args:
if arg.name not in self._cma_ops:
if arg.function_space_to.orig_name != \
arg.function_space_from.orig_name:
self._cma_ops[arg.name] = {
"arg": arg,
"params": self.cma_diff_fs_params}
else:
self._cma_ops[arg.name] = {
"arg": arg,
"params": self.cma_same_fs_params}
self._cma_ops[arg.name]["intent"] = arg.intent
self._cma_ops[arg.name]["datatype"] = \
arg.intrinsic_type
self._cma_ops[arg.name]["kind"] = arg.precision
# Keep a reference to the first CMA argument
if not self._first_cma_arg:
self._first_cma_arg = arg
[docs]
def initialise(self, cursor: int) -> int:
'''
Generates the calls to the LFRic infrastructure that look-up
the various components of each CMA operator. Adds these as
children of the supplied parent node.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
# If we have no CMA operators then we do nothing
if not self._cma_ops:
return cursor
const = LFRicConstants()
suffix = const.ARG_TYPE_SUFFIX_MAPPING["gh_columnwise_operator"]
first = True
for op_name in self._cma_ops:
# First, assign a pointer to the array containing the actual
# matrix.
cma_sym = self.symtab.find_or_create_tag(
f"{op_name}:{suffix}", op_name,
symbol_type=DataSymbol, datatype=UnresolvedType())
stmt = Assignment.create(
lhs=Reference(cma_sym),
rhs=StructureReference.create(
self.symtab.lookup(
self._cma_ops[op_name]["arg"].proxy_name),
["columnwise_matrix"]),
is_pointer=True)
if first:
stmt.preceding_comment = (
"Look-up information for each CMA operator"
)
first = False
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
# Then make copies of the related integer parameters
for param in self._cma_ops[op_name]["params"]:
param_name = self.symtab.find_or_create_tag(
f"{op_name}:{param}:{suffix}",
root_name=f"{op_name}_{param}",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
stmt = Assignment.create(
lhs=Reference(param_name),
rhs=StructureReference.create(
self.symtab.lookup(
self._cma_ops[op_name]["arg"].proxy_name),
[param]),
)
self._invoke.schedule.addchild(stmt, cursor)
cursor += 1
return cursor
[docs]
def invoke_declarations(self):
'''
Generate the necessary PSy-layer declarations for all column-wise
operators and their associated parameters.
Note: PSy layer in LFRic does not modify the CMA operator objects.
Hence, their Fortran intents are always "in" (the data updated in the
kernels is only pointed to from the column-wise operator object and is
thus not a part of the object).
'''
super().invoke_declarations()
# If we have no CMA operators then we do nothing
if not self._cma_ops:
return
const = LFRicConstants()
suffix = const.ARG_TYPE_SUFFIX_MAPPING["gh_columnwise_operator"]
for op_name in self._cma_ops:
new_name = self.symtab.next_available_name(
f"{op_name}_{suffix}")
tag = f"{op_name}:{suffix}"
arg = self._cma_ops[op_name]["arg"]
precision = LFRicConstants().precision_for_type(arg.data_type)
array_type = ArrayType(
LFRicTypes("LFRicRealScalarDataType")(precision),
[ArrayType.Extent.DEFERRED]*3)
index_str = ",".join(3*[":"])
dtype = UnsupportedFortranType(
f"real(kind={arg.precision}), pointer, "
f"dimension({index_str}) :: {new_name} => null()",
partial_datatype=array_type)
self.symtab.new_symbol(new_name,
symbol_type=DataSymbol,
datatype=dtype,
tag=tag)
# Declare the associated integer parameters
for param in self._cma_ops[op_name]["params"]:
name = f"{op_name}_{param}"
tag = f"{op_name}:{param}:{suffix}"
self.symtab.find_or_create(
name, tag=tag,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
[docs]
def stub_declarations(self):
'''
Generate all necessary declarations for CMA operators being passed to
a Kernel stub.
Note that argument order is redefined later by ArgOrdering.
'''
super().stub_declarations()
# If we have no CMA operators then we do nothing
if not self._cma_ops:
return
symtab = self.symtab
# CMA operators always need the current cell index and the number
# of columns in the mesh
symbol = symtab.find_or_create(
"cell", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
symbol.interface = ArgumentInterface(ArgumentInterface.Access.READ)
symtab.append_argument(symbol)
symbol = symtab.find_or_create(
"ncell_2d", symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
symbol.interface = ArgumentInterface(ArgumentInterface.Access.READ)
symtab.append_argument(symbol)
const = LFRicConstants()
suffix = const.ARG_TYPE_SUFFIX_MAPPING["gh_columnwise_operator"]
for op_name in self._cma_ops:
# Declare the associated scalar arguments before the array because
# some of them are used to dimension the latter (and some compilers
# get upset if this ordering is not followed)
for param in self._cma_ops[op_name]["params"]:
symbol = symtab.find_or_create_tag(
f"{op_name}:{param}:{suffix}",
root_name=f"{op_name}_{param}",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
symbol.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
symtab.append_argument(symbol)
# Declare the array that holds the CMA operator
bandwidth = symtab.find_or_create_tag(
f"{op_name}:bandwidth:{suffix}",
root_name=f"{op_name}_bandwidth",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
bandwidth.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
nrow = symtab.find_or_create_tag(
f"{op_name}:nrow:{suffix}",
root_name=f"{op_name}_nrow",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
nrow.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
op = symtab.find_or_create(
op_name, symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[Reference(bandwidth), Reference(nrow),
Reference(symtab.lookup("ncell_2d"))]))
if self._kernel.cma_operation == 'assembly':
op.interface = ArgumentInterface(
ArgumentInterface.Access.READWRITE)
else:
op.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
symtab.append_argument(op)
[docs]
class LFRicMeshes():
'''
Holds all mesh-related information (including colour maps if
required). If there are no inter-grid kernels then there is only
one mesh object required (when calling kernels with operates_on==domain,
colouring, doing distributed memory or querying the reference element).
However, kernels performing inter-grid operations require multiple mesh
objects as well as mesh maps and other quantities.
There are two types of inter-grid operation; the first is "prolongation"
where a field on a coarse mesh is mapped onto a fine mesh. The second
is "restriction" where a field on a fine mesh is mapped onto a coarse
mesh.
:param invoke: the Invoke for which to extract information on all \
required inter-grid operations.
:type invoke: :py:class:`psyclone.domain.lfric.LFRicInvoke`
:param unique_psy_vars: list of arguments to the PSy-layer routine.
:type unique_psy_vars: list of \
:py:class:`psyclone.lfric.LFRicKernelArgument` objects.
'''
def __init__(self, invoke, unique_psy_vars):
# List of names of unique mesh variables referenced in the Invoke
self._mesh_tag_names = []
# Whether or not the associated Invoke requires colourmap information
self._needs_colourmap = False
self._needs_colourmap_halo = False
self._needs_colourtilemap = False
self._needs_colourtilemap_halo = False
# Keep a reference to the Invoke so we can check its properties later
self._invoke = invoke
# Set used to generate a list of the unique mesh objects
_name_set = set()
# Find the first non-scalar argument to this PSy layer routine. We
# will use this to look-up the mesh if there are no inter-grid
# kernels in this invoke.
self._first_var = None
for var in unique_psy_vars:
if not var.is_scalar:
self._first_var = var
break
# Loop over all kernel calls in the schedule. Keep a list of
# any non-intergrid kernels so that we can generate a verbose error
# message if necessary.
non_intergrid_kernels = []
has_intergrid = False
for call in self._invoke.schedule.coded_kernels():
if (call.reference_element.properties or call.mesh.properties or
call.iterates_over == "domain" or call.cma_operation):
_name_set.add("mesh")
if not call.is_intergrid:
non_intergrid_kernels.append(call)
else:
has_intergrid = True
# Create and store the names of the associated mesh objects
_name_set.add(f"mesh_{call._intergrid_ref.fine.name}")
_name_set.add(f"mesh_{call._intergrid_ref.coarse.name}")
# If we found a mixture of both inter-grid and non-inter-grid kernels
# then we reject the invoke()
if non_intergrid_kernels and has_intergrid:
raise GenerationError(
f"An invoke containing inter-grid kernels must contain no "
f"other kernel types but kernels "
f"'{''', '''.join([c.name for c in non_intergrid_kernels])}' "
f"in invoke '{invoke.name}' are not inter-grid kernels.")
# If distributed memory is enabled then we will need at least
# one mesh object if we have one or more kernels that operate
# on cell-columns or are doing redundant computation for a
# kernel that operates on dofs. Since the latter condition
# comes about through the application of a transformation, we
# don't yet know whether or not a mesh is required. Therefore,
# the only solution is to assume that a mesh object is
# required if distributed memory is enabled. We also require a
# mesh object if any of the kernels require properties of
# either the reference element or the mesh. (Colourmaps also
# require a mesh object but that is handled in colourmap_init().)
if not _name_set and Config.get().distributed_memory:
# We didn't already have a requirement for a mesh so add one now.
_name_set.add("mesh")
self._add_mesh_symbols(list(_name_set))
@property
def symtab(self):
'''
:returns: associated symbol table.
:rtype: :py:class:`psyclone.psyir.symbols.SymbolTable`
'''
return self._invoke.schedule.symbol_table
def _add_mesh_symbols(self, mesh_tags):
'''
Add DataSymbols for the supplied list of mesh names and store the
corresponding list of tags.
A ContainerSymbol is created for the LFRic mesh module and a TypeSymbol
for the mesh type. If distributed memory is enabled then a DataSymbol
to hold the maximum halo depth is created for each mesh.
:param mesh_tags: tag names for every mesh object required.
:type mesh_tags: list of str
'''
if not mesh_tags:
return
self._mesh_tag_names = sorted(mesh_tags)
# Look up the names of the module and type for the mesh object
# from the LFRic constants class.
const = LFRicConstants()
mmod = const.MESH_TYPE_MAP["mesh"]["module"]
mtype = const.MESH_TYPE_MAP["mesh"]["type"]
# Create a Container symbol for the module
csym = self.symtab.find_or_create_tag(
mmod, symbol_type=ContainerSymbol)
# Create a TypeSymbol for the mesh type
mtype_sym = self.symtab.find_or_create_tag(
mtype, symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(csym))
name_list = []
for name in mesh_tags:
dt = UnsupportedFortranType(
f"type({mtype_sym.name}), pointer :: {name} => null()")
name_list.append(self.symtab.find_or_create_tag(
name, symbol_type=DataSymbol, datatype=dt).name)
if Config.get().distributed_memory:
# If distributed memory is enabled then we require a variable
# holding the maximum halo depth for each mesh.
for name in mesh_tags:
var_name = f"max_halo_depth_{name}"
self.symtab.find_or_create(
var_name, tag=var_name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
[docs]
def colourmap_init(self):
'''
Sets-up information on any required colourmaps. Since colouring is
applied by Transformations, this method is called as the final step
of LFRicColourTrans.apply().
'''
# pylint: disable=too-many-locals
const = LFRicConstants()
non_intergrid_kern = None
for call in [call for call in self._invoke.schedule.coded_kernels() if
call.is_coloured()]:
# Keep a record of whether or not any kernels (loops) in this
# invoke have been coloured and, if so, whether the associated loop
# is tiled or it goes into the halo.
loop = call.parent.parent
is_tiled = loop.loop_type == "cells_in_tile"
if is_tiled:
has_halo = (loop.parent.parent.upper_bound_name in
const.HALO_ACCESS_LOOP_BOUNDS)
if has_halo:
self._needs_colourtilemap_halo = True
else:
self._needs_colourtilemap = True
else:
has_halo = (loop.upper_bound_name in
const.HALO_ACCESS_LOOP_BOUNDS)
if has_halo:
self._needs_colourmap_halo = True
else:
self._needs_colourmap = True
if not call.is_intergrid:
non_intergrid_kern = call
continue
# This is an inter-grid kernel so look-up the names of
# the colourmap variables associated with the coarse
# mesh (since that determines the iteration space).
carg_name = call._intergrid_ref.coarse.name
# We use different variables if it is a tiled or a regular
# colour map
if not is_tiled:
base_name = "cmap_" + carg_name
array_type = ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
colour_map = self.symtab.find_or_create_tag(
base_name,
symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"integer(kind=i_def), pointer, dimension(:,:) :: "
f"{base_name} => null()",
partial_datatype=array_type))
# No. of colours
base_name = "ncolour_" + carg_name
ncolours = self.symtab.find_or_create_tag(
base_name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
# Array holding the last cell of a given colour.
if (Config.get().distributed_memory and
not call.all_updates_are_writes):
# This will require a loop into the halo and so the array
# is 2D (indexed by colour *and* halo depth).
base_name = "last_halo_cell_all_colours_" + carg_name
last_cell = self.symtab.find_or_create_tag(
base_name,
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2))
else:
# Array holding the last edge cell of a given colour. Just
# 1D as indexed by colour only.
base_name = "last_edge_cell_all_colours_" + carg_name
last_cell = self.symtab.find_or_create_tag(
base_name,
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*1))
# Add these symbols into the LFRicInterGrid entry for this
# kernel
call._intergrid_ref.set_colour_info(colour_map, ncolours,
last_cell)
else:
# Tiled colour map
base_name = "tmap_" + carg_name
tilecolour_map = self.symtab.find_or_create_tag(
base_name, symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"integer(kind=i_def), pointer :: {base_name}(:,:,:)"))
base_name = "ntilecolour_" + carg_name
ntilecolours = self.symtab.find_or_create_integer_symbol(
base_name, tag=base_name)
# Array holding the last cell of a given colour.
if (Config.get().distributed_memory and
not call.all_updates_are_writes):
# This will require a loop into the halo and so the array
# is 2D (indexed by colour *and* halo depth).
base_name = "last_halo_tile_per_colour_" + carg_name
last_tile = self.symtab.find_or_create_array(
base_name, 2, ScalarType.Intrinsic.INTEGER,
tag=base_name)
base_name = ("last_halo_cell_per_colour_and_tile_" +
carg_name)
last_cell_tile = self.symtab.find_or_create_array(
base_name, 3, ScalarType.Intrinsic.INTEGER,
tag=base_name)
else:
# Array holding the last edge cell of a given colour. Just
# 1D as indexed by colour only.
base_name = "last_edge_tile_per_colour_" + carg_name
last_tile = self.symtab.find_or_create_array(
base_name, 1, ScalarType.Intrinsic.INTEGER,
tag=base_name)
base_name = ("last_edge_cell_per_colour_and_tile_"
+ carg_name)
last_cell_tile = self.symtab.find_or_create_array(
base_name, 2, ScalarType.Intrinsic.INTEGER,
tag=base_name)
# Add these symbols into the dictionary entry for this
# inter-grid kernel
call._intergrid_ref.set_tilecolour_info(
tilecolour_map, ntilecolours, last_tile, last_cell_tile)
if non_intergrid_kern and (self._needs_colourmap or
self._needs_colourmap_halo):
# There aren't any inter-grid kernels but we do need colourmap
# information and that means we'll need a mesh object
self._add_mesh_symbols(["mesh"])
# This creates the colourmap information for this invoke if we
# don't already have one.
colour_map = non_intergrid_kern.colourmap
# No. of colours
ncolours = self.symtab.find_or_create(
"ncolour", tag="ncolour",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
).name
if self._needs_colourmap_halo:
self.symtab.find_or_create(
"last_halo_cell_all_colours",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag="last_halo_cell_all_colours")
if self._needs_colourmap:
self.symtab.find_or_create(
"last_edge_cell_all_colours",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*1),
tag="last_edge_cell_all_colours")
if non_intergrid_kern and (self._needs_colourtilemap or
self._needs_colourtilemap_halo):
# There aren't any inter-grid kernels but we do need colourtilemap
# information and that means we'll need a mesh object
self._add_mesh_symbols(["mesh"])
# Creates the colourtilemap information for this invoke if we
# don't already have one.
colour_map = non_intergrid_kern.tilecolourmap
# Create the No. of colours over tiles
_ = self.symtab.find_or_create_tag(
"ntilecolours",
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
if self._needs_colourtilemap_halo:
self.symtab.find_or_create(
"last_halo_tile_per_colour",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2),
tag="last_halo_tile_per_colour")
self.symtab.find_or_create(
"last_halo_cell_per_colour_and_tile",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*3),
tag="last_halo_cell_per_colour_and_tile")
if self._needs_colourtilemap:
self.symtab.find_or_create_tag(
"last_edge_tile_per_colour",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*1))
self.symtab.find_or_create_tag(
"last_edge_cell_per_colour_and_tile",
symbol_type=DataSymbol,
datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2))
[docs]
def invoke_declarations(self):
'''
Declare variables specific to mesh objects.
'''
# pylint: disable=too-many-locals, too-many-statements
const = LFRicConstants()
if self.intergrid_kernels:
mmap_type = const.MESH_TYPE_MAP["mesh_map"]["type"]
mmap_mod = const.MESH_TYPE_MAP["mesh_map"]["module"]
# Create a Container symbol for the module
csym = self.symtab.find_or_create_tag(
mmap_mod, symbol_type=ContainerSymbol)
# Create a TypeSymbol for the mesh type
self.symtab.find_or_create_tag(
mmap_type, symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(csym))
if not self.intergrid_kernels:
if self._needs_colourmap or self._needs_colourmap_halo:
# There aren't any inter-grid kernels but we do need
# colourmap information
csym = self.symtab.lookup_with_tag("cmap")
if self._needs_colourtilemap or self._needs_colourtilemap_halo:
# There aren't any inter-grid kernels but we do need
# colourmap information
csym = self.symtab.lookup_with_tag("tilecolourmap")
[docs]
def initialise(self, cursor: int) -> int:
'''
Initialise parameters specific to inter-grid kernels.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
# pylint: disable=too-many-branches
# If we haven't got any need for a mesh in this invoke then we
# don't do anything
if not self._mesh_tag_names:
return cursor
symtab = self._invoke.schedule.symbol_table
if len(self._mesh_tag_names) == 1:
# We only require one mesh object which means that this invoke
# contains no inter-grid kernels (which would require at least 2)
mesh_sym = symtab.lookup_with_tag(self._mesh_tag_names[0])
assignment = Assignment.create(
lhs=Reference(mesh_sym),
rhs=self._first_var.generate_method_call("get_mesh"),
is_pointer=True)
assignment.preceding_comment = "Create a mesh object"
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if Config.get().distributed_memory:
# If distributed memory is enabled then we need the maximum
# halo depth.
depth_sym = self.symtab.lookup_with_tag(
f"max_halo_depth_{self._mesh_tag_names[0]}")
self._invoke.schedule.addchild(Assignment.create(
lhs=Reference(depth_sym),
rhs=Call.create(StructureReference.create(
mesh_sym, ["get_halo_depth"]))),
cursor)
cursor += 1
if self._needs_colourmap or self._needs_colourmap_halo:
# Look-up variable names for colourmap and number of colours
cmap = self.symtab.find_or_create_tag("cmap")
ncolour = self.symtab.find_or_create_tag("ncolour")
# Get the number of colours
assignment = Assignment.create(
lhs=Reference(ncolour),
rhs=Call.create(StructureReference.create(
mesh_sym, ["get_ncolours"])))
assignment.preceding_comment = "Get the colourmap"
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Get the colour map
assignment = Assignment.create(
lhs=Reference(cmap),
rhs=Call.create(StructureReference.create(
mesh_sym, ["get_colour_map"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if self._needs_colourtilemap or self._needs_colourtilemap_halo:
# Look-up variable names for colourmap and number of colours
tmap = self.symtab.lookup_with_tag("tilecolourmap")
ntc = self.symtab.lookup_with_tag("ntilecolours")
# Get the number of colours
assignment = Assignment.create(
lhs=Reference(ntc),
rhs=Call.create(StructureReference.create(
mesh_sym, ["get_ntilecolours"])))
assignment.preceding_comment = "Get the tiled colourmap"
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Get the colour map
assignment = Assignment.create(
lhs=Reference(tmap),
rhs=Call.create(StructureReference.create(
mesh_sym, ["get_coloured_tiling_map"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Keep a list of quantities that we've already initialised so
# that we don't generate duplicate assignments
initialised = []
comment_cursor = cursor
# Loop over the LFRicInterGrid objects
for dig in self.intergrid_kernels:
# We need pointers to both the coarse and the fine mesh as well
# as the maximum halo depth for each.
fine_mesh = self.symtab.find_or_create_tag(f"mesh_{dig.fine.name}")
coarse_mesh = self.symtab.find_or_create_tag(
f"mesh_{dig.coarse.name}")
if fine_mesh not in initialised:
initialised.append(fine_mesh)
assignment = Assignment.create(
lhs=Reference(fine_mesh),
rhs=dig.fine.generate_method_call("get_mesh"),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if Config.get().distributed_memory:
max_halo_f_mesh = (
self.symtab.find_or_create_tag(
f"max_halo_depth_mesh_{dig.fine.name}"))
assignment = Assignment.create(
lhs=Reference(max_halo_f_mesh),
rhs=Call.create(StructureReference.create(
fine_mesh, ["get_halo_depth"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if coarse_mesh not in initialised:
initialised.append(coarse_mesh)
assignment = Assignment.create(
lhs=Reference(coarse_mesh),
rhs=dig.coarse.generate_method_call("get_mesh"),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if Config.get().distributed_memory:
max_halo_c_mesh = (
self.symtab.find_or_create_tag(
f"max_halo_depth_mesh_{dig.coarse.name}"))
assignment = Assignment.create(
lhs=Reference(max_halo_c_mesh),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_halo_depth"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# We also need a pointer to the mesh map which we get from
# the coarse mesh
if dig.mmap not in initialised:
initialised.append(dig.mmap)
digmmap = self.symtab.lookup(dig.mmap)
assignment = Assignment.create(
lhs=Reference(digmmap),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_mesh_map"]),
arguments=[Reference(fine_mesh)]),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Cell map. This is obtained from the mesh map.
if dig.cell_map not in initialised:
initialised.append(dig.cell_map)
digcellmap = self.symtab.lookup(dig.cell_map)
assignment = Assignment.create(
lhs=Reference(digcellmap),
rhs=Call.create(StructureReference.create(
digmmap, ["get_whole_cell_map"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Number of cells in the fine mesh
if dig.ncell_fine not in initialised:
initialised.append(dig.ncell_fine)
digncellfine = self.symtab.lookup(dig.ncell_fine)
if Config.get().distributed_memory:
# TODO this hardwired depth of 2 will need changing in
# order to support redundant computation
assignment = Assignment.create(
lhs=Reference(digncellfine),
rhs=Call.create(StructureReference.create(
fine_mesh, ["get_last_halo_cell"])))
assignment.rhs.append_named_arg("depth",
Literal("2", INTEGER_TYPE))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
else:
assignment = Assignment.create(
lhs=Reference(digncellfine),
rhs=dig.fine.generate_method_call("get_ncell"))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Number of fine cells per coarse cell in x.
if dig.ncellpercellx not in initialised:
initialised.append(dig.ncellpercellx)
digncellpercellx = self.symtab.lookup(dig.ncellpercellx)
assignment = Assignment.create(
lhs=Reference(digncellpercellx),
rhs=Call.create(StructureReference.create(
digmmap, ["get_ntarget_cells_per_source_x"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Number of fine cells per coarse cell in y.
if dig.ncellpercelly not in initialised:
initialised.append(dig.ncellpercelly)
digncellpercelly = self.symtab.lookup(dig.ncellpercelly)
assignment = Assignment.create(
lhs=Reference(digncellpercelly),
rhs=Call.create(StructureReference.create(
digmmap, ["get_ntarget_cells_per_source_y"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Colour map for the coarse mesh (if required)
if dig.colourmap_symbol:
# Number of colours
assignment = Assignment.create(
lhs=Reference(dig.ncolours_var_symbol),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_ncolours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Colour map itself
assignment = Assignment.create(
lhs=Reference(dig.colourmap_symbol),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_colour_map"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Last halo/edge cell per colour.
sym = dig.last_cell_var_symbol
if len(sym.datatype.shape) == 2:
# Array is 2D so is a halo access.
name = "get_last_halo_cell_all_colours"
else:
# Array is just 1D so go to the last edge cell.
name = "get_last_edge_cell_all_colours"
assignment = Assignment.create(
lhs=Reference(sym),
rhs=Call.create(StructureReference.create(
coarse_mesh, [name])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Colour map for the coarse mesh (if required)
if dig.tilecolourmap_symbol:
# Number of colours
assignment = Assignment.create(
lhs=Reference(dig.ntilecolours_var_symbol),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_ntilecolours"])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Colour map itself
assignment = Assignment.create(
lhs=Reference(dig.tilecolourmap_symbol),
rhs=Call.create(StructureReference.create(
coarse_mesh, ["get_coloured_tiling_map"])),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Last halo/edge tile per colour.
sym = dig.last_tile_var_symbol
if len(sym.datatype.shape) == 2:
# Array is 2D so is a halo access.
name = "get_last_halo_tile_all_colours"
else:
# Array is just 1D so go to the last edge cell.
name = "get_last_edge_tile_all_colours"
assignment = Assignment.create(
lhs=Reference(sym),
rhs=Call.create(StructureReference.create(
coarse_mesh, [name])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Last halo/edge cell per colour and tile.
sym = dig.last_cell_tile_var_symbol
if len(sym.datatype.shape) == 3:
# Array is 3D so is a halo access.
name = "get_last_halo_cell_all_colours_all_tiles"
else:
# Array is just 2D so go to the last edge cell.
name = "get_last_edge_cell_all_colours_all_tiles"
assignment = Assignment.create(
lhs=Reference(sym),
rhs=Call.create(StructureReference.create(
coarse_mesh, [name])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
if cursor != comment_cursor:
self._invoke.schedule[comment_cursor].preceding_comment = (
"Look-up mesh objects and loop limits for inter-grid kernels")
return cursor
@property
def intergrid_kernels(self):
'''
:returns: A list of objects describing the intergrid kernels used in
this invoke.
:rtype: list[:py:class:`psyclone.lfric.LFRicInterGrid`]
'''
intergrids = []
for call in self._invoke.schedule.coded_kernels():
if call.is_intergrid:
intergrids.append(call._intergrid_ref)
return intergrids
[docs]
class LFRicInterGrid():
'''
Holds information on quantities required by an inter-grid kernel.
:param fine_arg: Kernel argument on the fine mesh.
:type fine_arg: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param coarse_arg: Kernel argument on the coarse mesh.
:type coarse_arg: :py:class:`psyclone.lfric.LFRicKernelArgument`
'''
# pylint: disable=too-few-public-methods, too-many-instance-attributes
def __init__(self, fine_arg, coarse_arg):
# Arguments on the coarse and fine grids
self.coarse = coarse_arg
self.fine = fine_arg
# Get a reference to the InvokeSchedule SymbolTable
symtab = self.coarse.call.ancestor(InvokeSchedule).symbol_table
# Generate name for inter-mesh map
base_mmap_name = f"mmap_{fine_arg.name}_{coarse_arg.name}"
self.mmap = symtab.find_or_create(
base_mmap_name, tag=base_mmap_name,
symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"type(mesh_map_type), pointer :: {base_mmap_name}"
f" => null()")
).name
# Generate name for ncell variables
name = f"ncell_{fine_arg.name}"
self.ncell_fine = symtab.find_or_create(
name, tag=name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
).name
# No. of fine cells per coarse cell in x
name = f"ncpc_{fine_arg.name}_{coarse_arg.name}_x"
self.ncellpercellx = symtab.find_or_create(
name, tag=name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
).name
# No. of fine cells per coarse cell in y
name = f"ncpc_{fine_arg.name}_{coarse_arg.name}_y"
self.ncellpercelly = symtab.find_or_create(
name, tag=name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
).name
# Name for cell map
base_name = "cell_map_" + coarse_arg.name
ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
sym = symtab.find_or_create(
base_name,
symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"integer(kind=i_def), pointer :: {base_name}"
f"(:,:,:) => null()",
partial_datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*3)
))
self.cell_map = sym.name
# We have no colourmap information when first created
self._colourmap_symbol = None
# Symbol for the variable holding the number of colours
self._ncolours_var_symbol = None
# Symbol of the variable holding the last cell of a particular colour.
# Will be a 2D array if the kernel iteration space includes the halo
# and 1D otherwise.
self._last_cell_var_symbol = None
# We have no colourmap information when first created
self._tilecolourmap_symbol = None
# Symbol for the variable holding the number of colours
self._ntilecolours_var_symbol = None
# Symbol of the variable holding the last tile of a particular colour
self._last_tile_var_symbol = None
# Symbol of the variable holding the last cell of a particular tile
self._last_cell_tile_var_symbol = None
[docs]
def set_colour_info(self, colour_map: DataSymbol,
ncolours: DataSymbol, last_cell: DataSymbol):
'''Sets the colour_map, number of colours, and last cell of a
particular colour.
:param colour_map: the colour map symbol.
:param ncolours: the number of colours.
:param last_cell: the last halo cell of a particular colour.
'''
self._colourmap_symbol = colour_map
self._ncolours_var_symbol = ncolours
self._last_cell_var_symbol = last_cell
[docs]
def set_tilecolour_info(self, tilecolour_map: DataSymbol,
ntilecolours: DataSymbol,
last_tile: DataSymbol,
last_cell_tile: DataSymbol):
'''Sets the tilecolour_map, number of colours of tiles, last tile
of a particular colour and last cell of a particular tile and colour.
:param tilecolour_map: the tilecolourmap symbol.
:param ntilecolours: the number of tilecolours.
:param last_tile: the last tile of a particular colour.
:param last_cell_tile: the last cell of a particular tilecolour.
'''
self._tilecolourmap_symbol = tilecolour_map
self._ntilecolours_var_symbol = ntilecolours
self._last_tile_var_symbol = last_tile
self._last_cell_tile_var_symbol = last_cell_tile
@property
def colourmap_symbol(self) -> DataSymbol:
'''
:returns: the colour map symbol.
'''
return self._colourmap_symbol
@property
def ncolours_var_symbol(self) -> DataSymbol:
'''
:returns: the symbol storing the number of colours.
'''
return self._ncolours_var_symbol
@property
def last_cell_var_symbol(self) -> DataSymbol:
'''
:returns: the last halo/edge cell variable.
'''
return self._last_cell_var_symbol
@property
def tilecolourmap_symbol(self) -> DataSymbol:
'''
:returns: the tilecolour map symbol.
'''
return self._tilecolourmap_symbol
@property
def ntilecolours_var_symbol(self) -> DataSymbol:
'''
:returns: the symbol storing the number of tilecolours.
'''
return self._ntilecolours_var_symbol
@property
def last_tile_var_symbol(self) -> DataSymbol:
'''
:returns: the symbol with the last tile of a given colour.
'''
return self._last_tile_var_symbol
@property
def last_cell_tile_var_symbol(self) -> DataSymbol:
'''
:returns: the last cell in the tilecolour.
'''
return self._last_cell_tile_var_symbol
[docs]
class LFRicBasisFunctions(LFRicCollection):
''' Holds all information on the basis and differential basis
functions required by an invoke or kernel call. This covers both those
required for quadrature and for evaluators.
:param node: either the schedule of an Invoke or a single Kernel object
for which to extract information on all required
basis/diff-basis functions.
:type node: :py:class:`psyclone.domain.lfric.LFRicInvokeSchedule` or
:py:class:`psyclone.domain.lfric.LFRicKern`
:raises InternalError: if a call has an unrecognised evaluator shape.
'''
# Dimensioning vars for the basis function arrays required by each
# type of quadrature
qr_dim_vars = {"xyoz": ["np_xy", "np_z"],
"edge": ["np_xyz", "nedges"],
"face": ["np_xyz", "nfaces"]}
# The different weights arrays required by each type of quadrature
qr_weight_vars = {"xyoz": ["weights_xy", "weights_z"],
"edge": ["weights_xyz"],
"face": ["weights_xyz"]}
def __init__(self, node):
super().__init__(node)
# Construct a list of all the basis/diff-basis functions required
# by this invoke. Each entry in the list is a dictionary holding
# the shape, the function space and the 'target' function spaces
# (upon which the basis functions are evaluated).
self._basis_fns = []
# The dictionary of quadrature objects passed to this invoke. Keys
# are the various VALID_QUADRATURE_SHAPES, values are a list of
# associated quadrature variables. (i.e. we have a list of
# quadrature arguments for each shape.)
self._qr_vars = OrderedDict()
# The dict of target function spaces upon which we must provide
# evaluators. Keys are the FS names, values are (FunctionSpace,
# LFRicKernelArgument) tuples.
self._eval_targets = OrderedDict()
for call in self.kernel_calls:
if isinstance(call, LFRicBuiltIn) or not call.eval_shapes:
# Skip this kernel if it doesn't require basis/diff basis fns
continue
for shape, rule in call.qr_rules.items():
# This kernel requires quadrature
if shape not in self._qr_vars:
# We haven't seen a quadrature arg with this shape
# before so create a dictionary entry with an
# empty list
self._qr_vars[shape] = []
if rule.psy_name not in self._qr_vars[shape]:
# Add this qr argument to the list of those that
# have this shape
self._qr_vars[shape].append(rule.psy_name)
if "gh_evaluator" in call.eval_shapes:
# An evaluator consists of basis or diff basis functions
# for one FS evaluated on the nodes of another 'target' FS.
# Make a dict of 2-tuples, each containing the
# FunctionSpace and associated kernel argument for the
# target FSs.
# Loop over the target FS for evaluators required by this
# kernel
for fs_name in call.eval_targets:
if fs_name not in self._eval_targets:
# We don't already have this space in our list so
# add it to the list of target spaces
self._eval_targets[fs_name] = \
call.eval_targets[fs_name]
# Both quadrature and evaluators require basis and/or differential
# basis functions. This helper routine populates self._basis_fns
# with entries describing the basis functions required by
# this call.
self._setup_basis_fns_for_call(call)
[docs]
@staticmethod
def basis_first_dim_name(function_space):
'''
Get the name of the variable holding the first dimension of a
basis function
:param function_space: the function space the basis function is for
:type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:return: a Fortran variable name
:rtype: str
'''
return "dim_" + function_space.mangled_name
[docs]
@staticmethod
def basis_first_dim_value(function_space):
'''
Get the size of the first dimension of a basis function.
:param function_space: the function space the basis function is for
:type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:return: an integer length.
:rtype: string
:raises GenerationError: if an unsupported function space is supplied \
(e.g. ANY_SPACE_*, ANY_DISCONTINUOUS_SPACE_*)
'''
if function_space.has_scalar_basis:
first_dim = "1"
elif function_space.has_vector_basis:
first_dim = "3"
else:
# It is not possible to determine explicitly the first basis
# function array dimension from the metadata for any_space or
# any_discontinuous_space. This information needs to be passed
# from the PSy layer to the kernels (see issue #461).
const = LFRicConstants()
raise GenerationError(
f"Unsupported space for basis function, "
f"expecting one of {const.VALID_FUNCTION_SPACES} but found "
f"'{function_space.orig_name}'")
return first_dim
[docs]
@staticmethod
def diff_basis_first_dim_name(function_space):
'''
Get the name of the variable holding the first dimension of a
differential basis function.
:param function_space: the function space the diff-basis function \
is for.
:type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:return: a Fortran variable name.
:rtype: str
'''
return "diff_dim_" + function_space.mangled_name
[docs]
@staticmethod
def diff_basis_first_dim_value(function_space):
'''
Get the size of the first dimension of an array for a
differential basis function.
:param function_space: the function space the diff-basis function \
is for.
:type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:return: an integer length.
:rtype: str
:raises GenerationError: if an unsupported function space is \
supplied (e.g. ANY_SPACE_*, \
ANY_DISCONTINUOUS_SPACE_*)
'''
if function_space.has_scalar_diff_basis:
first_dim = "1"
elif function_space.has_vector_diff_basis:
first_dim = "3"
else:
# It is not possible to determine explicitly the first
# differential basis function array dimension from the metadata
# for any_space or any_discontinuous_space. This information
# needs to be passed from the PSy layer to the kernels
# (see issue #461).
const = LFRicConstants()
raise GenerationError(
f"Unsupported space for differential basis function, "
f"expecting one of {const.VALID_FUNCTION_SPACES} but found "
f"'{function_space.orig_name}'")
return first_dim
def _setup_basis_fns_for_call(self, call):
'''
Populates self._basis_fns with entries describing the basis
functions required by the supplied Call.
:param call: the kernel call for which basis functions are required.
:type call: :py:class:`psyclone.domain.lfric.LFRicKern`
:raises InternalError: if the supplied call is of incorrect type.
:raises InternalError: if the supplied call has an unrecognised \
evaluator shape.
'''
if not isinstance(call, LFRicKern):
raise InternalError(f"Expected an LFRicKern object but got: "
f"'{type(call)}'")
const = LFRicConstants()
# We need a full FunctionSpace object for each function space
# that has basis functions associated with it.
for fsd in call.fs_descriptors.descriptors:
# We need the full FS object, not just the name. Therefore
# we first have to get a kernel argument that is on this
# space...
arg, fspace = call.arguments.get_arg_on_space_name(fsd.fs_name)
for shape in call.eval_shapes:
# Populate a dict with the shape, function space and
# associated kernel argument for this basis/diff-basis f'n.
entry = {"shape": shape,
"fspace": fspace,
"arg": arg}
if shape in const.VALID_QUADRATURE_SHAPES:
# This is for quadrature - store the name of the
# qr variable
entry["qr_var"] = call.qr_rules[shape].psy_name
# Quadrature weights are evaluated at pre-determined
# points rather than at the nodes of another FS.
# We put one entry of None in the list of target
# spaces to facilitate cases where we loop over
# this list.
entry["nodal_fspaces"] = [None]
elif shape == "gh_evaluator":
# This is an evaluator
entry["qr_var"] = None
# Store a list of the FunctionSpace objects for which
# these basis functions are to be evaluated
entry["nodal_fspaces"] = [items[0] for items in
call.eval_targets.values()]
else:
raise InternalError(f"Unrecognised evaluator shape: "
f"'{shape}'. Should be one of "
f"{const.VALID_EVALUATOR_SHAPES}")
# Add our newly-constructed dict object to the list describing
# the required basis and/or differential basis functions for
# this Invoke.
if fsd.requires_basis:
entry["type"] = "basis"
self._basis_fns.append(entry)
if fsd.requires_diff_basis:
# Take a shallow copy of the dict and just modify the
# 'type' of the basis function it describes (this works
# because the 'type' entry is a primitive type [str]).
diff_entry = entry.copy()
diff_entry["type"] = "diff-basis"
self._basis_fns.append(diff_entry)
[docs]
def stub_declarations(self):
'''
Insert the variable declarations required by the basis functions into
the Kernel stub.
Note that argument order is redefined later by ArgOrdering.
:raises InternalError: if an unsupported quadrature shape is found.
'''
super().stub_declarations()
if not self._qr_vars and not self._eval_targets:
return
# The quadrature shapes that this method supports
supported_shapes = ["gh_quadrature_xyoz", "gh_quadrature_face",
"gh_quadrature_edge"]
# Get the lists of dimensioning variables and basis arrays
var_dims, basis_arrays = self._basis_fn_declns()
for var in var_dims:
arg = self.symtab.find_or_create(
var, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
if arg not in self.symtab.argument_list:
arg.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(arg)
for basis in basis_arrays:
dims = []
for value in basis_arrays[basis]:
try:
dims.append(Literal(value, INTEGER_TYPE))
except ValueError:
dims.append(Reference(self.symtab.find_or_create(value)))
arg = self.symtab.find_or_create(
basis, symbol_type=DataSymbol,
datatype=ArrayType(LFRicTypes("LFRicRealScalarDataType")(),
dims))
arg.interface = ArgumentInterface(ArgumentInterface.Access.READ)
self.symtab.append_argument(arg)
const = LFRicConstants()
for shape in self._qr_vars:
qr_name = "_qr_" + shape.split("_")[-1]
# Create the PSyIR intrinsic DataType
if shape not in const.QUADRATURE_TYPE_MAP:
raise InternalError(
f"Quadrature shapes other than {supported_shapes} are not "
f"yet supported - got: '{shape}'")
kind_sym = self.symtab.find_or_create(
const.QUADRATURE_TYPE_MAP[shape]["kind"],
symbol_type=DataSymbol, datatype=UnresolvedType(),
interface=ImportInterface(
self.symtab.lookup("constants_mod")))
# All quatratures are REAL
intr_type = ScalarType(ScalarType.Intrinsic.REAL,
Reference(kind_sym))
if shape == "gh_quadrature_xyoz":
dim = self.symtab.find_or_create(
"np_xy"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
sym = self.symtab.find_or_create(
"weights_xy"+qr_name, symbol_type=DataSymbol,
datatype=ArrayType(intr_type, [Reference(dim)]))
sym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(sym)
dim = self.symtab.find_or_create(
"np_z"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
sym = self.symtab.find_or_create(
"weights_z"+qr_name, symbol_type=DataSymbol,
datatype=ArrayType(intr_type, [Reference(dim)]))
sym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(sym)
elif shape == "gh_quadrature_face":
dim1 = self.symtab.find_or_create(
"np_xyz"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
dim2 = self.symtab.find_or_create(
"nfaces"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
sym = self.symtab.find_or_create(
"weights_xyz"+qr_name, symbol_type=DataSymbol,
datatype=ArrayType(intr_type, [Reference(dim1),
Reference(dim2)]))
sym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(sym)
elif shape == "gh_quadrature_edge":
dim1 = self.symtab.find_or_create(
"np_xyz"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
dim2 = self.symtab.find_or_create(
"nedges"+qr_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
sym = self.symtab.find_or_create(
"weights_xyz"+qr_name, symbol_type=DataSymbol,
datatype=ArrayType(intr_type, [Reference(dim1),
Reference(dim2)]))
sym.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(sym)
[docs]
def invoke_declarations(self):
'''
Add basis-function declarations to the PSy layer.
'''
super().invoke_declarations()
const = LFRicConstants()
# We need BASIS and/or DIFF_BASIS if any kernel requires quadrature
# or an evaluator
if self._qr_vars or self._eval_targets:
module = self.symtab.find_or_create(
const.FUNCTION_SPACE_TYPE_MAP["function_space"]["module"],
symbol_type=ContainerSymbol)
self.symtab.find_or_create(
"BASIS", symbol_type=DataSymbol, datatype=UnresolvedType(),
interface=ImportInterface(module))
self.symtab.find_or_create(
"DIFF_BASIS", symbol_type=DataSymbol,
datatype=UnresolvedType(), interface=ImportInterface(module))
if self._qr_vars:
# Look-up the module- and type-names from the QUADRATURE_TYPE_MAP
for shp in self._qr_vars:
quad_map = const.QUADRATURE_TYPE_MAP[shp]
module = self.symtab.find_or_create(
quad_map["module"],
symbol_type=ContainerSymbol)
self.symtab.find_or_create(
quad_map["type"], symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(module))
self.symtab.find_or_create(
quad_map["proxy_type"], symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(module))
for shape in self._qr_vars.keys():
# The PSy-layer routine is passed objects of
# quadrature_* type
dt_symbol = self.symtab.lookup(
const.QUADRATURE_TYPE_MAP[shape]["type"])
for name in self._qr_vars[shape]:
new_arg = self.symtab.find_or_create(
name, symbol_type=DataSymbol, datatype=dt_symbol,
)
new_arg.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(new_arg)
[docs]
def initialise(self, cursor: int) -> int:
'''
Create the declarations and assignments required for the
basis-functions required by an invoke. These are added as children
of the supplied parent node in the AST.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:raises InternalError: if an invalid entry is encountered in the \
self._basis_fns list.
'''
# pylint: disable=too-many-branches, too-many-locals
api_config = Config.get().api_conf("lfric")
const = LFRicConstants()
# We need BASIS and/or DIFF_BASIS if any kernel requires quadrature
# or an evaluator
if self._qr_vars or self._eval_targets:
module = self.symtab.find_or_create(
const.FUNCTION_SPACE_TYPE_MAP["function_space"]["module"],
symbol_type=ContainerSymbol)
self.symtab.find_or_create(
"BASIS", symbol_type=DataSymbol, datatype=UnresolvedType(),
interface=ImportInterface(module))
self.symtab.find_or_create(
"DIFF_BASIS", symbol_type=DataSymbol,
datatype=UnresolvedType(), interface=ImportInterface(module))
if self._qr_vars:
init_cursor = cursor
# Look-up the module- and type-names from the QUADRATURE_TYPE_MAP
for shp in self._qr_vars:
quad_map = const.QUADRATURE_TYPE_MAP[shp]
module = self.symtab.find_or_create(
quad_map["module"],
symbol_type=ContainerSymbol)
symbol = self.symtab.lookup(quad_map["type"])
symbol.interface = ImportInterface(module)
symbol = self.symtab.lookup(quad_map["proxy_type"])
symbol.interface = ImportInterface(module)
cursor = self._initialise_xyz_qr(cursor)
cursor = self._initialise_xyoz_qr(cursor)
cursor = self._initialise_xoyoz_qr(cursor)
cursor = self._initialise_face_or_edge_qr(cursor, "face")
cursor = self._initialise_face_or_edge_qr(cursor, "edge")
if init_cursor < cursor:
self._invoke.schedule[init_cursor].preceding_comment = (
"Look-up quadrature variables")
first = True
for (fspace, arg) in self._eval_targets.values():
# We need the list of nodes for each unique FS upon which we need
# to evaluate basis/diff-basis functions
nodes_name = "nodes_" + fspace.mangled_name
kind = api_config.default_kind["real"]
symbol = self.symtab.new_symbol(
nodes_name, symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"real(kind={kind}), pointer :: {nodes_name}"
f"(:,:) => null()",
partial_datatype=ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
))
assignment = Assignment.create(
lhs=Reference(symbol),
rhs=arg.generate_method_call(
"get_nodes", function_space=fspace),
is_pointer=True)
if first:
assignment.preceding_comment = (
"Initialise evaluator-related quantities for the target "
"function spaces")
first = False
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
init_cursor = cursor
var_dim_list = []
for basis_fn in self._basis_fns:
# Get the extent of the first dimension of the basis array.
if basis_fn['type'] == "basis":
first_dim = self.basis_first_dim_name(basis_fn["fspace"])
dim_space = "get_dim_space"
elif basis_fn['type'] == "diff-basis":
first_dim = self.diff_basis_first_dim_name(
basis_fn["fspace"])
dim_space = "get_dim_space_diff"
else:
raise InternalError(
f"Unrecognised type of basis function: "
f"'{basis_fn['''type''']}'. Should be either 'basis' or "
f"'diff-basis'.")
if first_dim not in var_dim_list:
var_dim_list.append(first_dim)
symbol = self.symtab.find_or_create(
first_dim, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
assignment = Assignment.create(
lhs=Reference(symbol),
rhs=basis_fn["arg"].generate_method_call(
dim_space, function_space=basis_fn["fspace"]))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
_, basis_arrays = self._basis_fn_declns()
# Allocate basis arrays
for basis in basis_arrays:
dims = "("+",".join([":"]*len(basis_arrays[basis]))+")"
symbol = self.symtab.find_or_create(
basis, symbol_type=DataSymbol, datatype=UnsupportedFortranType(
f"real(kind=r_def), allocatable :: {basis}{dims}"
))
alloc = IntrinsicCall.create(
IntrinsicCall.Intrinsic.ALLOCATE,
[ArrayReference.create(
symbol,
[Reference(self.symtab.find_or_create(
bn, symbol_type=DataSymbol,
datatype=UnresolvedType()))
for bn in basis_arrays[basis]])]
)
self._invoke.schedule.addchild(alloc, cursor)
cursor += 1
# Compute the values for any basis arrays
cursor = self._compute_basis_fns(cursor)
if init_cursor < cursor:
self._invoke.schedule[init_cursor].preceding_comment = (
"Allocate basis/diff-basis arrays")
return cursor
def _basis_fn_declns(self):
'''
Extracts all information relating to the necessary declarations
for basis-function arrays.
:returns: a 2-tuple containing a list of dimensioning variables & a \
dict of basis arrays.
:rtype: (list of str, dict)
:raises InternalError: if neither self._invoke or self._kernel are set.
:raises InternalError: if an unrecognised type of basis function is \
encountered.
:raises InternalError: if an unrecognised evaluator shape is \
encountered.
:raises InternalError: if there is no name for the quadrature object \
when generating PSy-layer code.
'''
# pylint: disable=too-many-branches
# Dictionary of basis arrays where key values are the array names and
# entries are a list of dimensions.
basis_arrays = OrderedDict()
# List of names of dimensioning (scalar) variables
var_dim_list = []
const = LFRicConstants()
# Loop over the list of dicts describing each basis function
# required by this Invoke.
for basis_fn in self._basis_fns:
# Get the extent of the first dimension of the basis array and
# store whether we have a basis or a differential basis function.
# Currently there are only those two possible types of basis
# function and we store the required diff basis name in basis_name.
if basis_fn['type'] == "basis":
if self._invoke:
first_dim = self.basis_first_dim_name(basis_fn["fspace"])
elif self._kernel:
first_dim = self.basis_first_dim_value(basis_fn["fspace"])
else:
raise InternalError("Require basis functions but do not "
"have either a Kernel or an "
"Invoke. Should be impossible.")
basis_name = "gh_basis"
elif basis_fn['type'] == "diff-basis":
if self._invoke:
first_dim = self.diff_basis_first_dim_name(
basis_fn["fspace"])
elif self._kernel:
first_dim = self.diff_basis_first_dim_value(
basis_fn["fspace"])
else:
raise InternalError("Require differential basis functions "
"but do not have either a Kernel or "
"an Invoke. Should be impossible.")
basis_name = "gh_diff_basis"
else:
raise InternalError(
f"Unrecognised type of basis function: "
f"'{basis_fn['''type''']}'. Should be either 'basis' or "
f"'diff-basis'.")
if self._invoke and first_dim not in var_dim_list:
var_dim_list.append(first_dim)
if basis_fn["shape"] in const.VALID_QUADRATURE_SHAPES:
qr_var = basis_fn["qr_var"]
if not qr_var:
raise InternalError(
f"Quadrature '{basis_fn['''shape''']}' is required but"
f" have no name for the associated Quadrature object.")
op_name = basis_fn["fspace"].get_operator_name(basis_name,
qr_var=qr_var)
if op_name in basis_arrays:
# We've already seen a basis with this name so skip
continue
# Dimensionality of the basis arrays depends on the
# type of quadrature...
alloc_args = qr_basis_alloc_args(first_dim, basis_fn)
for arg in alloc_args:
# In a kernel stub the first dimension of the array is
# a numerical value so make sure we don't try and declare
# it as a variable.
if not arg[0].isdigit() and arg not in var_dim_list:
var_dim_list.append(arg)
basis_arrays[op_name] = alloc_args
elif basis_fn["shape"].lower() == "gh_evaluator":
# This is an evaluator and thus may be required on more than
# one function space
for target_space in basis_fn["nodal_fspaces"]:
op_name = basis_fn["fspace"].\
get_operator_name(basis_name,
qr_var=basis_fn["qr_var"],
on_space=target_space)
if op_name in basis_arrays:
continue
# We haven't seen a basis with this name before so
# need to store its dimensions
basis_arrays[op_name] = [
first_dim,
basis_fn["fspace"].ndf_name,
target_space.ndf_name]
else:
raise InternalError(
f"Unrecognised evaluator shape: '{basis_fn['''shape''']}'."
f" Should be one of {const.VALID_EVALUATOR_SHAPES}")
return (var_dim_list, basis_arrays)
def _initialise_xyz_qr(self, cursor: int) -> int:
'''
Add in the initialisation of variables needed for XYZ
quadrature
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
# pylint: disable=unused-argument
# This shape is not yet supported so we do nothing
return cursor
def _initialise_xyoz_qr(self, cursor):
'''
Add in the initialisation of variables needed for XYoZ
quadrature
:param int cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:rtype: int
'''
const = LFRicConstants()
if "gh_quadrature_xyoz" not in self._qr_vars:
return cursor
for qr_arg_name in self._qr_vars["gh_quadrature_xyoz"]:
# We generate unique names for the integers holding the numbers
# of quadrature points by appending the name of the quadrature
# argument
for name in self.qr_dim_vars["xyoz"]:
self.symtab.new_symbol(
name+"_"+qr_arg_name, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
dtype = \
const.QUADRATURE_TYPE_MAP["gh_quadrature_xyoz"]["intrinsic"]
kind = const.QUADRATURE_TYPE_MAP["gh_quadrature_xyoz"]["kind"]
for name in self.qr_weight_vars["xyoz"]:
self.symtab.find_or_create(
name+"_"+qr_arg_name, symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"{dtype}(kind={kind}), pointer :: "
f"{name}_{qr_arg_name}(:) => null()"))
# Get the quadrature proxy
dtp_symbol = self.symtab.lookup(
const.QUADRATURE_TYPE_MAP["gh_quadrature_xyoz"]["proxy_type"])
proxy_symbol = self.symtab.find_or_create(
qr_arg_name+"_proxy", symbol_type=DataSymbol,
datatype=dtp_symbol)
symbol = self.symtab.lookup(qr_arg_name)
assignment = Assignment.create(
lhs=Reference(proxy_symbol),
rhs=Call.create(
StructureReference.create(
symbol, ['get_quadrature_proxy'])))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Number of points in each dimension
for qr_var in self.qr_dim_vars["xyoz"]:
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(self.symtab.lookup(
qr_var+"_"+qr_arg_name)),
rhs=StructureReference.create(
proxy_symbol, [qr_var])),
cursor)
cursor += 1
# Pointers to the weights arrays
for qr_var in self.qr_weight_vars["xyoz"]:
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(self.symtab.lookup(
qr_var+"_"+qr_arg_name)),
rhs=StructureReference.create(
proxy_symbol, [qr_var]),
is_pointer=True),
cursor)
cursor += 1
return cursor
def _initialise_xoyoz_qr(self, cursor: int) -> int:
'''
Add in the initialisation of variables needed for XoYoZ
quadrature.
:param cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
'''
# pylint: disable=unused-argument
# This shape is not yet supported so we do nothing
return cursor
def _initialise_face_or_edge_qr(self, cursor, qr_type):
'''
Add in the initialisation of variables needed for face or edge
quadrature.
:param int cursor: position where to add the next initialisation
statements.
:param str qr_type: whether to generate initialisation code for \
"face" or "edge" quadrature.
:returns: Updated cursor value.
:rtype: int
:raises InternalError: if `qr_type` is not "face" or "edge".
'''
if qr_type not in ["face", "edge"]:
raise InternalError(
f"_initialise_face_or_edge_qr: qr_type argument must be "
f"either 'face' or 'edge' but got: '{qr_type}'")
quadrature_name = f"gh_quadrature_{qr_type}"
if quadrature_name not in self._qr_vars:
return cursor
for qr_arg_name in self._qr_vars[quadrature_name]:
arg_symbol = self.symtab.lookup(qr_arg_name)
arg_symbol.interface = ArgumentInterface(
ArgumentInterface.Access.READ)
self.symtab.append_argument(arg_symbol)
# We generate unique names for the integers holding the numbers
# of quadrature points by appending the name of the quadrature
# argument
for name in self.qr_dim_vars[qr_type]:
self.symtab.find_or_create(
name+"_"+qr_arg_name, tag=name+"_"+qr_arg_name,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
array_type = ArrayType(
LFRicTypes("LFRicRealScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
for name in self.qr_weight_vars[qr_type]:
self.symtab.find_or_create(
f"{name}_{qr_arg_name}", symbol_type=DataSymbol,
datatype=UnsupportedFortranType(
f"real(kind=r_def), pointer, dimension(:,:) :: "
f"{name}_{qr_arg_name} => null()\n",
partial_datatype=array_type
),
tag=f"{name}_{qr_arg_name}")
const = LFRicConstants()
# Get the quadrature proxy
ptype = self.symtab.lookup(
const.QUADRATURE_TYPE_MAP[quadrature_name]["proxy_type"])
proxy_sym = self.symtab.find_or_create_tag(
qr_arg_name+"_proxy", symbol_type=DataSymbol, datatype=ptype)
call = Call.create(
StructureReference.create(
self.symtab.lookup(qr_arg_name),
["get_quadrature_proxy"]))
assignment = Assignment.create(
lhs=Reference(proxy_sym),
rhs=call)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# The dimensioning variables required for this quadrature
# (e.g. nedges/nfaces, np_xyz)
for qr_var in self.qr_dim_vars[qr_type]:
qr_sym = self.symtab.lookup(qr_var+'_'+qr_arg_name)
assignment = Assignment.create(
lhs=Reference(qr_sym),
rhs=StructureReference.create(proxy_sym, [qr_var]))
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
# Pointers to the weights arrays
for qr_var in self.qr_weight_vars[qr_type]:
qr_sym = self.symtab.lookup(qr_var+'_'+qr_arg_name)
assignment = Assignment.create(
lhs=Reference(qr_sym),
rhs=StructureReference.create(
proxy_sym, [qr_var]),
is_pointer=True)
self._invoke.schedule.addchild(assignment, cursor)
cursor += 1
return cursor
def _compute_basis_fns(self, cursor):
'''
Generates the necessary Fortran to compute the values of
any basis/diff-basis arrays required
:param int cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:rtype: int
'''
# pylint: disable=too-many-locals
const = LFRicConstants()
loop_var_list = set()
op_name_list = []
# add calls to compute the values of any basis arrays
first = True
for basis_fn in self._basis_fns:
# Currently there are only two possible types of basis function
# and we store the corresponding strings to use in basis_name,
# basis_type, and first_dim. If support for other basis function
# types is added in future then more tests need to be added here.
if basis_fn["type"] == "diff-basis":
basis_name = "gh_diff_basis"
basis_type = "DIFF_BASIS"
first_dim = self.diff_basis_first_dim_name(basis_fn["fspace"])
elif basis_fn["type"] == "basis":
basis_name = "gh_basis"
basis_type = "BASIS"
first_dim = self.basis_first_dim_name(basis_fn["fspace"])
else:
raise InternalError(
f"Unrecognised type of basis function: "
f"'{basis_fn['''type''']}'. Expected one of 'basis' or "
f"'diff-basis'.")
if basis_fn["shape"] in const.VALID_QUADRATURE_SHAPES:
op_name = basis_fn["fspace"].\
get_operator_name(basis_name, qr_var=basis_fn["qr_var"])
if op_name in op_name_list:
# Jump over any basis arrays we've seen before
continue
op_name_list.append(op_name)
# Create the argument list
args = [Reference(self.symtab.lookup(basis_type)),
basis_fn["arg"].generate_accessor(basis_fn["fspace"]),
Reference(self.symtab.lookup(first_dim)),
Reference(self.symtab.lookup(
basis_fn["fspace"].ndf_name)),
Reference(self.symtab.lookup(op_name))]
# insert the basis array call
call = Call.create(
StructureReference.create(
self.symtab.lookup(basis_fn["qr_var"]),
["compute_function"]),
args)
if first:
call.preceding_comment = "Compute basis/diff-basis arrays"
first = False
self._invoke.schedule.addchild(call, cursor)
cursor += 1
elif basis_fn["shape"].lower() == "gh_evaluator":
# We have an evaluator. We may need this on more than one
# function space.
for space in basis_fn["nodal_fspaces"]:
op_name = basis_fn["fspace"].\
get_operator_name(basis_name, on_space=space)
if op_name in op_name_list:
# Jump over any basis arrays we've seen before
continue
op_name_list.append(op_name)
nodal_loop_var = "df_nodal"
loop_var_list.add(nodal_loop_var)
# Loop over dofs of target function space
symbol = self.symtab.find_or_create_tag(
nodal_loop_var,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
loop = Loop.create(
symbol, Literal('1', INTEGER_TYPE),
Reference(self.symtab.lookup(space.ndf_name)),
Literal('1', INTEGER_TYPE), [])
if first:
loop.preceding_comment = (
"Compute basis/diff-basis arrays")
first = False
self._invoke.schedule.addchild(loop, cursor)
cursor += 1
dof_loop_var = "df_" + basis_fn["fspace"].mangled_name
loop_var_list.add(dof_loop_var)
symbol = self.symtab.find_or_create_tag(
dof_loop_var,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")())
inner_loop = Loop.create(
symbol, Literal('1', INTEGER_TYPE),
Reference(self.symtab.lookup(
basis_fn["fspace"].ndf_name)),
Literal('1', INTEGER_TYPE), [])
loop.loop_body.addchild(inner_loop)
symbol = self.symtab.lookup(op_name)
rhs = basis_fn['arg'].generate_method_call(
"call_function", function_space=basis_fn['fspace'])
rhs.addchild(Reference(self.symtab.lookup(basis_type)))
rhs.addchild(Reference(self.symtab.lookup(dof_loop_var)))
rhs.addchild(ArrayReference.create(
self.symtab.lookup(f"nodes_{space.mangled_name}"),
[":", Reference(self.symtab.lookup(
nodal_loop_var))]))
inner_loop.loop_body.addchild(
Assignment.create(
lhs=ArrayReference.create(symbol, [
":",
Reference(self.symtab.lookup(
f"df_{basis_fn['fspace'].mangled_name}")),
Reference(self.symtab.lookup("df_nodal"))
]),
rhs=rhs))
else:
raise InternalError(
f"Unrecognised shape '{basis_fn['''shape''']}' specified "
f"for basis function. Should be one of: "
f"{const.VALID_EVALUATOR_SHAPES}")
return cursor
[docs]
def deallocate(self):
'''
Add code (at the end of the Invoke Schedule) to deallocate all
basis/diff-basis function arrays.
:raises InternalError: if an unrecognised type of basis function
is encountered.
'''
func_space_var_names = set()
for basis_fn in self._basis_fns:
# add the basis array name to the list to use later
if basis_fn["type"] == "basis":
basis_name = "gh_basis"
elif basis_fn["type"] == "diff-basis":
basis_name = "gh_diff_basis"
else:
raise InternalError(
f"Unrecognised type of basis function: "
f"'{basis_fn['''type''']}'. Should be one of 'basis' or "
f"'diff-basis'.")
for fspace in basis_fn["nodal_fspaces"]:
op_name = basis_fn["fspace"].\
get_operator_name(basis_name,
qr_var=basis_fn["qr_var"],
on_space=fspace)
func_space_var_names.add(op_name)
first = True
if func_space_var_names:
# add the required deallocate call
dealloc = IntrinsicCall.create(
IntrinsicCall.Intrinsic.DEALLOCATE,
[Reference(self.symtab.lookup(name)) for name in
sorted(func_space_var_names)]
)
if first:
dealloc.preceding_comment = "Deallocate basis arrays"
self._invoke.schedule.children.append(dealloc)
[docs]
class LFRicBoundaryConditions(LFRicCollection):
'''
Manages declarations and initialisation of quantities required by
kernels that need boundary condition information.
:param node: the Invoke or Kernel stub for which we are to handle \
any boundary conditions.
:type node: :py:class:`psyclone.domain.lfric.LFRicInvoke` or \
:py:class:`psyclone.domain.lfric.LFRicKern`
:raises GenerationError: if a kernel named "enforce_bc_code" is found \
but does not have an argument on ANY_SPACE_1.
:raises GenerationError: if a kernel named "enforce_operator_bc_code" is \
found but does not have exactly one argument.
'''
# Define a BoundaryDofs namedtuple to help us manage the arrays that
# are required.
BoundaryDofs = namedtuple("BoundaryDofs", ["argument", "function_space"])
def __init__(self, node):
super().__init__(node)
self._boundary_dofs = []
# Check through all the kernel calls to see whether any of them
# require boundary conditions. Currently this is done by recognising
# the kernel name.
# pylint: disable=import-outside-toplevel
from psyclone.domain.lfric.metadata_to_arguments_rules import (
MetadataToArgumentsRules)
for call in self.kernel_calls:
if MetadataToArgumentsRules.bc_kern_regex.match(call.name):
bc_fs = None
for fspace in call.arguments.unique_fss:
if fspace.orig_name == "any_space_1":
bc_fs = fspace
break
if not bc_fs:
raise GenerationError(
"The enforce_bc_code kernel must have an argument on "
"ANY_SPACE_1 but failed to find such an argument.")
farg = call.arguments.get_arg_on_space(bc_fs)
self._boundary_dofs.append(self.BoundaryDofs(farg, bc_fs))
elif call.name.lower() == "enforce_operator_bc_code":
# Check that the kernel only has one argument
if len(call.arguments.args) != 1:
raise GenerationError(
f"The enforce_operator_bc_code kernel must have "
f"exactly one argument but found "
f"{len(call.arguments.args)}")
op_arg = call.arguments.args[0]
bc_fs = op_arg.function_space_to
self._boundary_dofs.append(self.BoundaryDofs(op_arg, bc_fs))
[docs]
def invoke_declarations(self):
'''
Add declarations for any boundary-dofs arrays required by an Invoke.
'''
super().invoke_declarations()
api_config = Config.get().api_conf("lfric")
for dofs in self._boundary_dofs:
name = "boundary_dofs_" + dofs.argument.name
kind = api_config.default_kind["integer"]
dtype = UnsupportedFortranType(
f"integer(kind={kind}), pointer "
f":: {name}(:,:) => null()",
partial_datatype=ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[ArrayType.Extent.DEFERRED]*2)
)
self.symtab.new_symbol(
name,
symbol_type=DataSymbol,
datatype=dtype)
[docs]
def stub_declarations(self):
'''
Add declarations for any boundary-dofs arrays required by a kernel.
Note that argument order is redefined later by ArgOrdering.
'''
super().stub_declarations()
for dofs in self._boundary_dofs:
name = "boundary_dofs_" + dofs.argument.name
ndf_name = self.symtab.lookup(dofs.function_space.ndf_name)
dtype = ArrayType(
LFRicTypes("LFRicIntegerScalarDataType")(),
[Reference(ndf_name), Literal("2", INTEGER_TYPE)])
new_symbol = self.symtab.new_symbol(
name,
symbol_type=DataSymbol,
datatype=dtype,
interface=ArgumentInterface(
ArgumentInterface.Access.READ)
)
self.symtab.append_argument(new_symbol)
[docs]
def initialise(self, cursor):
'''
Initialise any boundary-dofs arrays required by an Invoke.
:param int cursor: position where to add the next initialisation
statements.
:returns: Updated cursor value.
:rtype: int
'''
for dofs in self._boundary_dofs:
name = "boundary_dofs_" + dofs.argument.name
self._invoke.schedule.addchild(
Assignment.create(
lhs=Reference(self.symtab.lookup(name)),
rhs=dofs.argument.generate_method_call(
"get_boundary_dofs",
function_space=dofs.function_space),
is_pointer=True
),
cursor)
cursor += 1
return cursor
[docs]
class LFRicGlobalSum(GlobalSum):
'''
LFRic specific global sum class which can be added to and
manipulated in a schedule.
:param scalar: the kernel argument for which to perform a global sum.
:type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param parent: the parent node of this node in the PSyIR.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:raises GenerationError: if distributed memory is not enabled.
:raises InternalError: if the supplied argument is not a scalar.
:raises GenerationError: if the scalar is not of "real" intrinsic type.
'''
def __init__(self, scalar, parent=None):
# Check that distributed memory is enabled
if not Config.get().distributed_memory:
raise GenerationError(
"It makes no sense to create an LFRicGlobalSum object when "
"distributed memory is not enabled (dm=False).")
# Check that the global sum argument is indeed a scalar
if not scalar.is_scalar:
raise InternalError(
f"LFRicGlobalSum.init(): A global sum argument should be a "
f"scalar but found argument of type '{scalar.argument_type}'.")
# Check scalar intrinsic types that this class supports (only
# "real" for now)
if scalar.intrinsic_type != "real":
raise GenerationError(
f"LFRicGlobalSum currently only supports real scalars, but "
f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has "
f"'{scalar.intrinsic_type}' intrinsic type.")
# Initialise the parent class
super().__init__(scalar, parent=parent)
[docs]
def lower_to_language_level(self):
'''
:returns: this node lowered to language-level PSyIR.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
# Get the name strings to use
name = self._scalar.name
type_name = self._scalar.data_type
mod_name = self._scalar.module_name
# Get the symbols from the given names
symtab = self.ancestor(InvokeSchedule).symbol_table
sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol)
sum_type = symtab.find_or_create(type_name,
symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(sum_mod))
sum_name = symtab.find_or_create_tag("global_sum",
symbol_type=DataSymbol,
datatype=sum_type)
tmp_var = symtab.lookup(name)
# Create the assignments
assign1 = Assignment.create(
lhs=StructureReference.create(sum_name, ["value"]),
rhs=Reference(tmp_var)
)
assign1.preceding_comment = "Perform global sum"
self.parent.addchild(assign1, self.position)
assign2 = Assignment.create(
lhs=Reference(tmp_var),
rhs=Call.create(StructureReference.create(sum_name, ["get_sum"]))
)
return self.replace_with(assign2)
def _create_depth_list(halo_info_list, parent):
'''Halo exchanges may have more than one dependency. This method
simplifies multiple dependencies to remove duplicates and any
obvious redundancy. For example, if one dependency is for depth=1
and another for depth=2 then we do not need the former as it is
covered by the latter. Similarly, if we have a depth=extent+1 and
another for depth=extent+2 then we do not need the former as it is
covered by the latter. It also takes into account
needs_clean_outer, which indicates whether the outermost halo
needs to be clean (and therefore whether there is a dependence).
:param halo_info_list: a list containing halo access information
derived from all read fields dependent on this halo exchange.
:type: list[:py:class:`psyclone.lfric.HaloReadAccess`]
:param parent: the parent PSyIR node of the related halo exchange.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:returns: a list containing halo depth information derived from
the halo access information.
:rtype: list[:py:class:`psyclone.lfric.HaloDepth]`
'''
# pylint: disable=too-many-branches
depth_info_list = []
# First look to see if all field dependencies are
# annexed_only. If so we only care about annexed dofs.
annexed_only = True
for halo_info in halo_info_list:
if not (halo_info.annexed_only or
(halo_info.var_depth == Literal("1", INTEGER_TYPE)
and not halo_info.needs_clean_outer)):
# There are two cases when we only care about accesses to
# annexed dofs. 1) when annexed_only is set and 2) when
# the halo depth is 1 but we only depend on annexed dofs
# being up-to-date (needs_clean_outer is False).
annexed_only = False
break
if annexed_only:
depth_info = HaloDepth(parent)
depth_info.set_by_value(max_depth=False,
var_depth=Literal("1", INTEGER_TYPE),
annexed_only=True,
max_depth_m1=False)
return [depth_info]
# Next look to see if one of the field dependencies specifies
# a max_depth access. If so the whole halo region is accessed
# so we do not need to be concerned with other accesses.
max_depth_m1 = False
for halo_info in halo_info_list:
if halo_info.max_depth:
if halo_info.needs_clean_outer:
# Found a max_depth access so we only need one
# HaloDepth entry.
depth_info = HaloDepth(parent)
depth_info.set_by_value(max_depth=True, var_depth=None,
annexed_only=False, max_depth_m1=False)
return [depth_info]
# Remember that we found a max_depth-1 access.
max_depth_m1 = True
if max_depth_m1:
# We have at least one max_depth-1 access.
depth_info = HaloDepth(parent)
depth_info.set_by_value(max_depth=False, var_depth=None,
annexed_only=False, max_depth_m1=True)
depth_info_list.append(depth_info)
for halo_info in halo_info_list:
# Go through the halo information associated with each
# read dependency, skipping any max_depth-1 accesses.
if halo_info.max_depth and not halo_info.needs_clean_outer:
continue
var_depth = halo_info.var_depth
if var_depth and not halo_info.needs_clean_outer:
# Decrease depth by 1 if we don't care about the outermost
# access.
var_depth = BinaryOperation.create(
BinaryOperation.Operator.SUB,
var_depth.copy(), Literal("1", INTEGER_TYPE))
# check whether we match with existing depth information
for depth_info in depth_info_list:
if depth_info.var_depth == var_depth:
# This dependence has exactly the same depth as an existing one
# so no need to add a new HaloDepth.
break
else:
# No matches were found with existing entries so create a
# new one if 'var_depth' is set.
if var_depth:
depth_info = HaloDepth(parent)
depth_info.set_by_value(max_depth=False, var_depth=var_depth,
annexed_only=False, max_depth_m1=False)
depth_info_list.append(depth_info)
return depth_info_list
[docs]
class LFRicHaloExchange(HaloExchange):
'''LFRic-specific halo exchange class which can be added to and
manipulated in a schedule.
:param field: the field that this halo exchange will act on
:type field: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param bool check_dirty: optional argument default True indicating
whether this halo exchange should be subject to a run-time check
for clean/dirty halos.
:param vector_index: optional vector index (default None) to identify
identify which index of a vector field this halo exchange is
responsible for.
:type vector_index: int
:param parent: PSyIR parent node of this object.
:type parent: Optional[:py:class:`psyclone.psyir.nodes.Node`]
'''
def __init__(self, field, check_dirty=True,
vector_index=None, parent=None):
HaloExchange.__init__(self, field, check_dirty=check_dirty,
vector_index=vector_index, parent=parent)
# set up some defaults for this class
self._halo_exchange_name = "halo_exchange"
def _compute_stencil_type(self):
'''Dynamically work out the type of stencil required for this halo
exchange as it could change as transformations are applied to
the schedule. If all stencil accesses are of the same type then we
return that stencil, otherwise we return the "region" stencil
type (as it is safe for all stencils).
:return: the type of stencil required for this halo exchange
:rtype: str
'''
# get information about stencil accesses from all read fields
# dependent on this halo exchange
halo_info_list = self._compute_halo_read_info()
trial_stencil = halo_info_list[0].stencil_type
for halo_info in halo_info_list:
# assume that if stencil accesses are different that we
# simply revert to region. We could be more clever in the
# future e.g. x and y implies cross.
if halo_info.stencil_type != trial_stencil:
return "region"
return trial_stencil
def _compute_halo_depth(self):
'''Dynamically determine the depth of the halo for this halo exchange,
as the depth can change as transformations are applied to the
schedule.
:return: the PSyIR for the halo exchange depth.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
depth_info_list = self._compute_halo_read_depth_info()
if len(depth_info_list) == 1:
psyir = depth_info_list[0].psyir_expression()
else:
psyir = IntrinsicCall.create(
IntrinsicCall.Intrinsic.MAX,
[depth.psyir_expression() for depth in depth_info_list])
# Simplify the resulting expression. We need to create a fake
# Assignment to temporarily host the expression.
sym_maths = SymbolicMaths.get()
fake_assign = Assignment.create(
Reference(DataSymbol("tmp", INTEGER_TYPE)), psyir)
self.parent.addchild(fake_assign)
sym_maths.expand(fake_assign.rhs)
depth_expr = fake_assign.rhs.detach()
fake_assign.detach()
return depth_expr
def _compute_halo_read_depth_info(self, ignore_hex_dep=False):
'''Take a list of `psyclone.lfric.HaloReadAccess` objects and
create an equivalent list of `psyclone.lfric.HaloDepth`
objects. Whilst doing this we simplify the
`psyclone.lfric.HaloDepth` list to remove redundant depth
information e.g. depth=1 is not required if we have a depth=2.
If the optional ignore_hex_dep argument is set to True then
any read accesses contained in halo exchange nodes are
ignored. This option can therefore be used to filter out any
halo exchange dependencies and only return non-halo exchange
dependencies if and when required.
:param bool ignore_hex_dep: if True then ignore any read \
accesses contained in halo exchanges. This is an optional \
argument that defaults to False.
:return: a list containing halo depth information derived from \
all fields dependent on this halo exchange.
:rtype: :func:`list` of :py:class:`psyclone.lfric.HaloDepth`
'''
# get our halo information
halo_info_list = self._compute_halo_read_info(ignore_hex_dep)
# use the halo information to generate depth information
depth_info_list = _create_depth_list(halo_info_list, self)
return depth_info_list
def _compute_halo_read_info(self,
ignore_hex_dep: bool = False
) -> List[HaloReadAccess]:
'''Dynamically computes all halo read dependencies and returns the
required halo information (i.e. halo depth and stencil type)
in a list of HaloReadAccess objects. If the optional
ignore_hex_dep argument is set to True then any read accesses
contained in halo exchange nodes are ignored. This option can
therefore be used to filter out any halo exchange dependencies
and only return non-halo exchange dependencies if and when
required.
:param ignore_hex_dep: if True then ignore any read
accesses contained in halo exchanges. This is an optional
argument that defaults to False.
:return: halo information for each read dependency.
:raises InternalError: if there is more than one read
dependency associated with a halo exchange.
:raises InternalError: if there is a read dependency
associated with a halo exchange and it is not the last
entry in the read dependency list.
:raises GenerationError: if there is a read dependency
associated with an asynchronous halo exchange.
:raises InternalError: if no read dependencies are found.
'''
read_dependencies = self.field.forward_read_dependencies()
hex_deps = [dep for dep in read_dependencies
if isinstance(dep.call, LFRicHaloExchange)]
if hex_deps:
# There is a field accessed by a halo exchange that is
# a read dependence. As ignore_hex_dep is True this should
# be ignored so this is removed from the list.
if any(dep for dep in hex_deps
if isinstance(dep.call, (LFRicHaloExchangeStart,
LFRicHaloExchangeEnd))):
raise GenerationError(
"Please perform redundant computation transformations "
"before asynchronous halo exchange transformations.")
# There can only be one field accessed by a
# halo exchange that is a read dependence.
if len(hex_deps) != 1:
raise InternalError(
f"There should only ever be at most one read dependency "
f"associated with a halo exchange in the read dependency "
f"list, but found {len(hex_deps)} for field "
f"{self.field.name}.")
# For sanity, check that the field accessed by the halo
# exchange is the last dependence in the list.
if not isinstance(read_dependencies[-1].call, LFRicHaloExchange):
raise InternalError(
"If there is a read dependency associated with a halo "
"exchange in the list of read dependencies then it should "
"be the last one in the list.")
if ignore_hex_dep:
# Remove the last entry in the list (the field accessed by
# the halo exchange).
del read_dependencies[-1]
if not read_dependencies:
raise InternalError(
"Internal logic error. There should be at least one read "
"dependence for a halo exchange.")
return [HaloReadAccess(read_dependency, self._parent) for
read_dependency in read_dependencies]
def _compute_halo_write_info(self):
'''Determines how much of the halo has been cleaned from any previous
redundant computation.
:return: a HaloWriteAccess object containing the required
information, or None if no dependence information is found.
:rtype: :py:class:`psyclone.lfric.HaloWriteAccess` | None
:raises GenerationError: if more than one write dependence is
found for this halo exchange as this should not be possible.
'''
write_dependencies = self.field.backward_write_dependencies()
if not write_dependencies:
# no write dependence information
return None
if len(write_dependencies) > 1:
raise GenerationError(
f"Internal logic error. There should be at most one write "
f"dependence for a halo exchange. Found "
f"'{len(write_dependencies)}'")
return HaloWriteAccess(write_dependencies[0], parent=self)
[docs]
def required(self, ignore_hex_dep=False):
'''Determines whether this halo exchange is definitely required
``(True, True)``, might be required ``(True, False)`` or is definitely
not required ``(False, *)``.
If the optional ignore_hex_dep argument is set to True then
any read accesses contained in halo exchange nodes are
ignored. This option can therefore be used to filter out any
halo exchange dependencies and only consider non-halo exchange
dependencies if and when required.
Whilst a halo exchange is generally only ever added if it is
required, or if it may be required, this situation can change
if redundant computation transformations are applied. The
first argument can be used to remove such halo exchanges if
required.
When the first return value is True, the second return value
can be used to see if we need to rely on the runtime
(set_dirty and set_clean calls) and therefore add a
check_dirty() call around the halo exchange or whether we
definitely know that this halo exchange is required.
This routine assumes that a stencil size provided via a
variable may take the value 0. If a variables value is
constrained to be 1, or more, then the logic for deciding
whether a halo exchange is definitely required should be
updated. Note, the routine would still be correct as is, it
would just return more unknown results than it should).
:param bool ignore_hex_dep: if True then ignore any read \
accesses contained in halo exchanges. This is an optional \
argument that defaults to False.
:returns: (x, y) where x specifies whether this halo
exchange is (or might be) required - True, or is not
required - False. If the first tuple item is True then the
second argument specifies whether we definitely know that
we need the HaloExchange - True, or are not sure - False.
:rtype: (bool, bool)
'''
# pylint: disable=too-many-branches, too-many-return-statements
# get *aggregated* information about halo reads
required_clean_info = self._compute_halo_read_depth_info(
ignore_hex_dep)
# get information about the halo write
clean_info = self._compute_halo_write_info()
# no need to test whether we return at least one read
# dependency as _compute_halo_read_depth_info() raises an
# exception if none are found
if (Config.get().api_conf("lfric").compute_annexed_dofs and
len(required_clean_info) == 1 and
required_clean_info[0].annexed_only):
# We definitely don't need the halo exchange as we
# only read annexed dofs and these are always clean as
# they are computed by default when iterating over
# dofs and kept up-to-date by redundant computation
# when iterating over cells.
required = False
known = True # redundant information as it is always known
return required, known
if not clean_info:
# this halo exchange has no previous write dependencies so
# we do not know the initial state of the halo. This means
# that we do not know if we need a halo exchange or not
required = True
known = False
return required, known
if clean_info.max_depth:
if not clean_info.dirty_outer:
# all of the halo is cleaned by redundant computation
# so halo exchange is not required
required = False
known = True # redundant information as it is always known
else:
# the last level halo is dirty
if required_clean_info[0].max_depth:
# we know that we need to clean the outermost halo level
required = True
known = True
else:
# we don't know whether the halo exchange is
# required or not as the reader reads the halo to
# a specified depth but we don't know the depth
# of the halo
required = True
known = False
return required, known
# At this point we know that clean_info.max_depth is False
clean_depth = clean_info.clean_depth
if not clean_depth:
if clean_info.dirty_outer:
# the writer redundantly computes in the level 1 halo but
# leaves it dirty (although annexed dofs are now clean).
if len(required_clean_info) == 1 and \
required_clean_info[0].annexed_only:
# we definitely don't need the halo exchange as we
# only read annexed dofs and these have been made
# clean by the redundant computation
required = False
known = True # redundant information as it is always known
else:
# we definitely need the halo exchange as the reader(s)
# require the halo to be clean
required = True
known = True
return required, known
else:
# if clean_depth is 0 then the writer does not
# redundantly compute so we definitely need the halo
# exchange
required = True
known = True
return required, known
# We have a clean_depth so iterate over the various required depths
# to see whether or not they are all satisfied.
required = False
known = False
for required_clean in required_clean_info:
if required_clean.max_depth or required_clean.max_depth_m1:
required = True
known = False
return required, known
left_dirty = SymbolicMaths.greater_than(
required_clean.var_depth, clean_depth,
all_variables_positive=True)
if left_dirty == SymbolicMaths.Fuzzy.FALSE:
# Nothing is left dirty so no hexch required for this
# read access.
continue
if left_dirty == SymbolicMaths.Fuzzy.TRUE:
# Halo definitely left dirty to a certain depth.
required = True
known = True
return required, known
# Otherwise, we can't be sure. Continue looking at the other
# read accesses in case one of them is definitely not satisfied.
required = True
continue
# If we get to here then we may or may not require a halo exchange.
# If we do need one, then we aren't certain about it (as we'd already
# have returned if we were).
if not required:
# We know we don't require a halo exchange.
known = True
return required, known
[docs]
def node_str(self, colour=True):
''' Creates a text summary of this HaloExchange node.
:param bool colour: whether or not to include control codes for colour.
:returns: text summary of this node, optionally with control codes \
for colour highlighting.
:rtype: str
'''
_, known = self.required()
runtime_check = not known
field_id = self._field.name_indexed
return (f"{self.coloured_name(colour)}[field='{field_id}', "
f"type='{self._compute_stencil_type()}', "
f"depth={self._compute_halo_depth().debug_string()}, "
f"check_dirty={runtime_check}]")
[docs]
def lower_to_language_level(self):
'''
:returns: this node lowered to language-level PSyIR.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
symbol = DataSymbol(self._field.proxy_name, UnresolvedType())
method = self._halo_exchange_name
depth_expr = self._compute_halo_depth()
# Create infrastructure Calls
if self.vector_index:
idx = Literal(str(self.vector_index), INTEGER_TYPE)
if_condition = Call.create(
ArrayOfStructuresReference.create(symbol, [idx], ['is_dirty']),
[('depth', depth_expr.copy())])
if_body = Call.create(
ArrayOfStructuresReference.create(
symbol, [idx.copy()], [method]),
[('depth', depth_expr.copy())])
else:
if_condition = Call.create(
StructureReference.create(symbol, ['is_dirty']),
[('depth', depth_expr.copy())])
if_body = Call.create(
StructureReference.create(symbol, [method]),
[('depth', depth_expr.copy())])
# Add the "if_dirty" check when necessary
_, known = self.required()
if not known:
haloex = IfBlock.create(if_condition, [if_body])
else:
haloex = if_body
haloex.preceding_comment = self.preceding_comment
self.replace_with(haloex)
return haloex
[docs]
class LFRicHaloExchangeStart(LFRicHaloExchange):
'''The start of an asynchronous halo exchange. This is similar to a
regular halo exchange except that the Fortran name of the call is
different and the routine only reads the data being transferred
(the associated field is specified as having a read access). As a
result this class is not able to determine some important
properties (such as whether the halo exchange is known to be
required or not). This is solved by finding the corresponding
asynchronous halo exchange end (a halo exchange start always has a
corresponding halo exchange end and vice versa) and calling its
methods (a halo exchange end is specified as having readwrite
access to its associated field and therefore is able to determine
the required properties).
:param field: the field that this halo exchange will act on
:type field: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param check_dirty: optional argument (default True) indicating \
whether this halo exchange should be subject to a run-time check \
for clean/dirty halos.
:type check_dirty: bool
:param vector_index: optional vector index (default None) to \
identify which component of a vector field this halo exchange is \
responsible for
:type vector_index: int
:param parent: optional PSyIRe parent node (default None) of this \
object
:type parent: :py:class:`psyclone.psyir.nodes.Node`
'''
# Textual description of the node.
_text_name = "HaloExchangeStart"
_colour = "yellow"
def __init__(self, field, check_dirty=True,
vector_index=None, parent=None):
LFRicHaloExchange.__init__(self, field, check_dirty=check_dirty,
vector_index=vector_index, parent=parent)
# Update the field's access appropriately. Here "gh_read"
# specifies that the start of a halo exchange only reads
# the field's data.
self._field.access = AccessType.READ
# override appropriate parent class names
self._halo_exchange_name = "halo_exchange_start"
def _compute_stencil_type(self):
'''Call the required method in the corresponding halo exchange end
object. This is done as the field in halo exchange start is
only read and the dependence analysis beneath this call
requires the field to be modified.
:return: Return the type of stencil required for this pair of \
halo exchanges
:rtype: str
'''
# pylint: disable=protected-access
return self._get_hex_end()._compute_stencil_type()
def _compute_halo_depth(self):
'''Call the required method in the corresponding halo exchange end
object. This is done as the field in halo exchange start is
only read and the dependence analysis beneath this call
requires the field to be modified.
:return: Return the halo exchange depth as a Fortran string
:rtype: str
'''
# pylint: disable=protected-access
return self._get_hex_end()._compute_halo_depth()
[docs]
def required(self):
'''Call the required method in the corresponding halo exchange end
object. This is done as the field in halo exchange start is
only read and the dependence analysis beneath this call
requires the field to be modified.
:returns: (x, y) where x specifies whether this halo exchange \
is (or might be) required - True, or is not required \
- False. If the first tuple item is True then the second \
argument specifies whether we definitely know that we need \
the HaloExchange - True, or are not sure - False.
:rtype: (bool, bool)
'''
return self._get_hex_end().required()
def _get_hex_end(self):
'''An internal helper routine for this class which finds the halo
exchange end object corresponding to this halo exchange start
object or raises an exception if one is not found.
:return: The corresponding halo exchange end object
:rtype: :py:class:`psyclone.lfric.LFRicHaloExchangeEnd`
:raises GenerationError: If no matching HaloExchangeEnd is \
found, or if the first matching haloexchange that is found is \
not a HaloExchangeEnd
'''
# Look at all nodes following this one in schedule order
# (which is PSyIRe node order)
for node in self.following():
if self.sameParent(node) and isinstance(node, LFRicHaloExchange):
# Found a following `haloexchange`,
# `haloexchangestart` or `haloexchangeend` PSyIRe node
# that is at the same calling hierarchy level as this
# haloexchangestart
access = DataAccess(self.field)
if access.overlaps(node.field):
if isinstance(node, LFRicHaloExchangeEnd):
return node
raise GenerationError(
f"Halo exchange start for field '{self.field.name}' "
f"should match with a halo exchange end, but found "
f"{type(node)}")
# no match has been found which is an error as a halo exchange
# start should always have a matching halo exchange end that
# follows it in schedule (PSyIRe sibling) order
raise GenerationError(
f"Halo exchange start for field '{self.field.name}' has no "
f"matching halo exchange end")
[docs]
class LFRicHaloExchangeEnd(LFRicHaloExchange):
'''The end of an asynchronous halo exchange. This is similar to a
regular halo exchange except that the Fortran name of the call is
different and the routine only writes to the data being
transferred.
:param field: the field that this halo exchange will act on
:type field: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param check_dirty: optional argument (default True) indicating
whether this halo exchange should be subject to a run-time check
for clean/dirty halos.
:type check_dirty: bool
:param vector_index: optional vector index (default None) to
identify which index of a vector field this halo exchange is
responsible for.
:type vector_index: int
:param parent: PSyIR parent node (default None) of this object.
:type parent: Optional[:py:class:`psyclone.psyir.nodes.Node`]
'''
# Textual description of the node.
_text_name = "HaloExchangeEnd"
_colour = "yellow"
def __init__(self, field, check_dirty=True,
vector_index=None, parent=None):
LFRicHaloExchange.__init__(self, field, check_dirty=check_dirty,
vector_index=vector_index, parent=parent)
# Update field properties appropriately. The associated field is
# written to. However, a readwrite field access needs to be
# specified as this is required for the halo exchange logic to
# work correctly.
self._field.access = AccessType.READWRITE
# override appropriate parent class names
self._halo_exchange_name = "halo_exchange_finish"
[docs]
class HaloDepth():
'''Determines how much of the halo a read to a field accesses (the
halo depth).
:param parent: the parent PSyIR node of the region containing the field
access that is represented.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:raises TypeError: if the parent argument is not a Node.
'''
def __init__(self, parent):
# var_depth is used to store the PSyIR of the expression holding
# depth of halo that is accessed.
self._var_depth = None
# max_depth specifies whether the full depth of halo (whatever
# that might be) is accessed. If this is set then
# literal_depth, var_depth and max_depth_m1 have no
# meaning. max_depth being False does not necessarily mean the
# full halo depth is not accessed, rather it means that we do
# not know.
self._max_depth = False
# max_depth_m1 specifies whether the full depth of halo
# (whatever that might be) apart from the outermost level is
# accessed. If this is set then literal_depth, var_depth and
# max_depth have no meaning.
self._max_depth_m1 = False
# annexed only is True if the only access in the halo is for
# annexed dofs
self._annexed_only = False
# Keep a reference to the symbol table so that we can look-up
# variables holding the maximum halo depth.
# TODO #2503: This can become invalid if the HaloExchange
# containing this HaloDepth changes its ancestors.
if not isinstance(parent, Node):
raise TypeError(
f"The HaloDepth parent argument must be a Node, but found: "
f"{type(parent).__name__}"
)
self._parent = parent
@property
def annexed_only(self):
'''
:returns: True if only annexed dofs are accessed in the halo and
False otherwise.
:rtype: bool
'''
return self._annexed_only
@property
def max_depth(self):
'''
:returns: True if the read to the field is known to access all
of the halo and False otherwise.
:rtype: bool
'''
return self._max_depth
@property
def max_depth_m1(self):
'''Returns whether the read to the field is known to access all of the
halo except the outermost level or not.
:returns: True if the read to the field is known to access all
of the halo except the outermost and False otherwise.
:rtype: bool
'''
return self._max_depth_m1
@property
def var_depth(self):
'''Returns the PSyIR of the expression specifying the depth of halo
access if one is provided.
:returns: PSyIR specifying the halo access depth
if one exists, and None if not.
:rtype: :py:class:`psyclone.psyir.nodes.Statement`
'''
return self._var_depth
[docs]
def set_by_value(self,
max_depth: bool,
var_depth: Optional[DataNode],
annexed_only: bool,
max_depth_m1: bool):
# pylint: disable=too-many-arguments
'''Set halo depth information directly.
:param max_depth: True if the field accesses all of the
halo and False otherwise
:param var_depth: PSyIR expression specifying the halo
access depth, if one exists, and None if not
:param annexed_only: True if only the halo's annexed dofs
are accessed and False otherwise
:param max_depth_m1: True if the field accesses all of the halo but
does not require the outermost halo to be correct, False otherwise
'''
self._max_depth = max_depth
self._annexed_only = annexed_only
self._max_depth_m1 = max_depth_m1
self._var_depth = None
if not var_depth:
return
# Simplify any provided PSyIR expression.
sym_maths = SymbolicMaths.get()
# In order to be able to apply the SymbolicMaths.expand() method we
# have to create a fake Assignment and temporarily graft it into the
# tree.
fake_assign = Assignment.create(
Reference(DataSymbol("tmp", INTEGER_TYPE)), var_depth.copy())
sched = self._parent.ancestor(Schedule, include_self=True)
sched.addchild(fake_assign)
sym_maths.expand(fake_assign.rhs)
self._var_depth = fake_assign.rhs.detach()
fake_assign.detach()
[docs]
def psyir_expression(self):
'''
:returns: the PSyIR expression representing this HaloDepth.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
table = self._parent.scope.symbol_table
if self.max_depth:
max_depth = table.lookup_with_tag("max_halo_depth_mesh")
return Reference(max_depth)
if self.max_depth_m1:
max_depth = table.lookup_with_tag("max_halo_depth_mesh")
return BinaryOperation.create(
BinaryOperation.Operator.SUB,
Reference(max_depth),
Literal('1', INTEGER_TYPE))
if self.var_depth:
return self.var_depth.copy()
return None
def halo_check_arg(field: LFRicKernelArgument,
access_types: list[AccessType]) -> Kern:
'''
Support function which performs checks to ensure the first argument
is a field, that the field is contained within Kernel or Builtin
call and that the field is accessed in one of the ways specified
by the second argument. If no error is reported it returns the
call object containing this argument.
:param field: the argument object we are checking
:param access_types: List of allowed access types.
:return: the call containing the argument object
:raises GenerationError: if the first argument to this function is
the wrong type.
:raises GenerationError: if the first argument is not accessed in one of
the ways specified by the second argument to the function.
:raises GenerationError: if the first argument is not contained
within a call object.
'''
try:
# Get the kernel/built-in call associated with this field
call = field.call
except AttributeError as err:
raise GenerationError(
f"HaloInfo class expects an argument of type "
f"LFRicKernelArgument, or equivalent, on initialisation, but "
f"found, '{type(field)}'") from err
if field.access not in access_types:
api_strings = [access.api_specific_name() for access in access_types]
raise GenerationError(
f"In HaloInfo class, field '{field.name}' should be one of "
f"{api_strings}, but found '{field.access.api_specific_name()}'")
if not isinstance(call, (LFRicBuiltIn, LFRicKern)):
raise GenerationError(
f"In HaloInfo class, field '{field.name}' should be from a call "
f"but found {type(call)}")
return call
[docs]
class HaloWriteAccess(HaloDepth):
'''Determines how much of a field's halo is written to (the halo depth)
when a field is accessed in a particular kernel within a
particular loop nest.
:param field: the field that we are concerned with.
:param parent: the parent PSyIR node associated with the scoping region
that contains this halo access.
'''
def __init__(self, field: LFRicKernelArgument, parent: Node):
super().__init__(parent)
self._compute_from_field(field)
@property
def dirty_outer(self):
'''Returns True if the writer is continuous and accesses the halo and
False otherwise. It indicates that the outer level of halo that has
been written to is actually dirty (well to be precise it is a partial
sum).
:returns: True if the outer layer of halo that is written \
to remains dirty and False otherwise.
:rtype: bool
'''
return self._dirty_outer
@property
def clean_depth(self):
'''
Returns the depth to which this halo is clean following the write.
:returns: PSyIR for the expression for the clean halo depth or None.
:rtype: :py:class:`psyclone.psyir.nodes.Node` | NoneType
'''
if self.var_depth:
halo_depth = self.var_depth
elif self.max_depth:
# halo accesses(s) is/are to the full halo
# depth (-1 if continuous)
table = self._parent.scope.symbol_table
halo_depth = Reference(
table.lookup_with_tag("max_halo_depth_mesh"))
else:
return None
if self.dirty_outer:
halo_depth = BinaryOperation.create(
BinaryOperation.Operator.SUB,
halo_depth.copy(), Literal("1", INTEGER_TYPE))
sym_maths = SymbolicMaths.get()
fake_assign = Assignment.create(
Reference(DataSymbol("tmp", INTEGER_TYPE)),
halo_depth)
sched = self._parent.ancestor(Schedule)
sched.addchild(fake_assign)
sym_maths.expand(fake_assign.rhs)
depth_expr = fake_assign.rhs.detach()
fake_assign.detach()
if depth_expr == Literal("0", INTEGER_TYPE):
return None
return depth_expr
def _compute_from_field(self, field: LFRicKernelArgument):
'''Internal method to compute what parts of a field's halo are written
to in a certain kernel and loop. The information computed is the depth
of access and validity of the data after writing. The depth of access
can be the maximum halo depth or a specified (literal or variable)
depth and the outer halo layer that is written to may be dirty or
clean.
:param field: the field that we are concerned with.
'''
const = LFRicConstants()
call = halo_check_arg(field, AccessType.all_write_accesses())
# no test required here as all calls exist within a loop
loop = call.parent.parent
# The outermost halo level that is written to is dirty if it
# is a continuous field which writes into the halo in a loop
# over cells
self._dirty_outer = (
not field.discontinuous and
loop.iteration_space.endswith("cell_column") and
loop.upper_bound_name in const.HALO_ACCESS_LOOP_BOUNDS)
depth = None
max_depth = False
if loop.upper_bound_name in const.HALO_ACCESS_LOOP_BOUNDS:
# loop does redundant computation
if loop.upper_bound_halo_depth:
# loop redundant computation is to a certain depth.
depth = loop.upper_bound_halo_depth
else:
# loop redundant computation is to the maximum depth
max_depth = True
# If this is an inter-grid kernel and we're writing to the
# field on the fine mesh then the halo depth is effectively
# doubled
if call.is_intergrid and field.mesh == "gh_fine" and depth:
depth = BinaryOperation.create(
BinaryOperation.Operator.MUL,
Literal("2", INTEGER_TYPE), depth.copy())
# The third argument for set_by_value gives the PSyIR of the
# expression specifying the depth.
var_depth = depth
# The fourth argument for set_by_value indicates whether
# we only access annexed_dofs. At the moment this is not possible when
# modifying a field so we always supply False. The fifth
# argument indicates if the depth of access is the
# maximum-1. This is not possible here so we supply False.
HaloDepth.set_by_value(self, max_depth, var_depth,
False, False)
[docs]
class HaloReadAccess(HaloDepth):
'''Determines how much of a field's halo is read (the halo depth) and
additionally the access pattern (the stencil) when a field is
accessed in a particular kernel within a particular loop nest.
:param field: the field for which we want information.
:type field: :py:class:`psyclone.lfric.LFRicKernelArgument`
:param parent: the node where this HaloDepth belongs.
:type parent: :py:class:`psyclone.psyir.node.Node`
'''
def __init__(self, field, parent=None):
super().__init__(parent)
self._stencil_type = None
self._needs_clean_outer = None
self._compute_from_field(field)
@property
def needs_clean_outer(self):
'''Returns False if the reader has a gh_inc access and accesses the
halo. Otherwise returns True. Indicates that the outer level
of halo that has been read does not need to be clean (although
any annexed dofs do).
:return: Returns False if the outer layer of halo that is read \
does not need to be clean and True otherwise.
:rtype: bool
'''
return self._needs_clean_outer
@property
def stencil_type(self):
'''Returns the type of stencil access used by the field(s) in the halo
if one exists. If redundant computation (accessing the full
halo) is combined with a stencil access (potentially accessing
a subset of the halo) then the access is assumed to be full
access (region) for all depths.
:returns: the type of stencil access used or None if there is no \
stencil.
:rtype: str
'''
return self._stencil_type
def _compute_from_field(self, field: LFRicKernelArgument):
'''Internal method to compute which parts of a field's halo are read
in a certain kernel and loop. The information computed is the
depth of access and the access pattern. The depth of access
can be the maximum halo depth or a PSyIR expression specifying the
depth. The access pattern will only be specified if the kernel code
performs a stencil access on the field.
For a standard kernel that has been transformed to perform redundant
computation and has a field argument with a stencil access, the depth
of that field's halo that is read is computed as the *sum* of the
depth of the redundant computation and the stencil size. However, for
the special case of a Kernel which *must* iterate into the halo region
(has an ``OPERATES_ON`` of ``HALO_CELL_COLUMN`` or
``OWNED_AND_HALO_CELL_COLUMN``) to a depth ``ndepth``, say, then the
depth of the halo that is read is currently computed as the *maximum*
of ``ndepth`` and the stencil size. This is because, computing the sum
of ``ndepth`` and the stencil size results in a halo depth that is
greater than the maximum allowed. In this particular case, the stencil
accesses are 'parallel' to the halo and therefore this workaround is
valid. TODO #2781 - this case should be handled by adding support for
a new type of stencil rather than this ad-hoc fix.
:param field: the field that we are concerned with
:raises GenerationError: if an unsupported name for a loop
upper-bound is found.
:raises GenerationError: if a Kernel with a stencil access performs
redundant computation out to the maximum halo depth.
'''
# pylint: disable=too-many-branches
const = LFRicConstants()
self._annexed_only = False
call = halo_check_arg(field, AccessType.all_read_accesses())
loop = call.ancestor(LFRicLoop)
table = loop.ancestor(InvokeSchedule).symbol_table
# For GH_INC we accumulate contributions into the field being
# modified. In order to get correct results for owned and
# annexed dofs, this requires that the fields we are
# accumulating contributions from have up-to-date values in
# the halo cell(s). However, we do not need to be concerned
# with the values of the modified field in the last-level of
# the halo. This is because we only have enough information to
# partially compute the contributions in those cells
# anyway. (If the values of the field being modified are
# required, at some later point, in that level of the halo
# then we do a halo swap.)
self._needs_clean_outer = (
not (field.access == AccessType.INC
and loop.upper_bound_name in ["cell_halo",
"colour_halo"]))
# Records whether the kernel is a special "halo" kernel that must
# iterate into the halo for correctness.
is_halo_kernel = (call.iterates_over in
const.HALO_KERNEL_ITERATION_SPACES)
# now we have the parent loop we can work out what part of the
# halo this field accesses
if loop.upper_bound_name in const.HALO_ACCESS_LOOP_BOUNDS:
# This loop performs redundant computation or is a 'halo' kernel
# that iterates into the halo.
if loop.upper_bound_halo_depth:
self._var_depth = loop.upper_bound_halo_depth
else:
# loop redundant computation is to the maximum depth
self._max_depth = True
elif loop.upper_bound_name in ("ncolour",
"ntiles_per_colour",
"ncells_per_colour_and_tile"):
# Loop is coloured but does not access the halo.
pass
elif loop.upper_bound_name in ["ncells", "nannexed"]:
if field.descriptor.stencil:
# no need to worry about annexed dofs (if they exist)
# as the stencil will cover these (this is currently
# guaranteed as halo exchanges only exchange full
# halos)
pass
else: # there is no stencil
if (field.discontinuous or call.iterates_over == "dof" or
(not call.arguments.iteration_space_arg().discontinuous
and call.all_updates_are_writes)):
# There are only local accesses or the kernel is of the
# special form where any iteration is guaranteed to write
# the same value to a given shared entity.
pass
else:
# This is a continuous field which therefore
# accesses annexed dofs. We set access to the
# level 1 halo here as there is currently no
# mechanism to perform a halo exchange solely on
# annexed dofs.
self._var_depth = Literal("1", INTEGER_TYPE)
self._annexed_only = True
elif loop.upper_bound_name == "ndofs":
# we only access owned dofs so there is no access to the
# halo
pass
else:
raise GenerationError(
f"Internal error in HaloReadAccess._compute_from_field. Found "
f"unexpected loop upper bound name '{loop.upper_bound_name}'")
if self._max_depth or self._var_depth:
# Whilst stencil type has no real meaning when there is no
# stencil it is convenient to set it to "region" when
# there is redundant computation as the halo exchange
# logic is interested in the access pattern irrespective
# of whether there is a stencil access or not. We use
# "region" as it means access all of the halo data which
# is what is done when performing redundant computation
# with no stencil.
self._stencil_type = "region"
if field.descriptor.stencil:
# field has a stencil access
if self._max_depth:
raise GenerationError(
"redundant computation to max depth with a stencil is "
"invalid")
self._stencil_type = field.descriptor.stencil['type']
if self._var_depth:
# halo exchange does not support mixed accesses to the halo
# so we simply set the stencil type as 'region'.
self._stencil_type = "region"
stencil_depth = field.descriptor.stencil['extent']
if stencil_depth:
# stencil_depth is provided in the kernel metadata
st_depth = Literal(str(stencil_depth), INTEGER_TYPE)
else:
# Stencil_depth is provided by the algorithm layer.
# It is currently not possible to specify kind for an
# integer literal stencil depth in a kernel call. This
# will be enabled when addressing issue #753.
if field.stencil.extent_arg.is_literal():
# a literal is specified
value_str = field.stencil.extent_arg.text
st_depth = Literal(value_str, INTEGER_TYPE)
else:
# a variable is specified
st_depth = Reference(
table.lookup(field.stencil.extent_arg.varname))
if self._var_depth:
if is_halo_kernel:
# 'halo' kernels (those that have halos included in their
# iteration space in order to get correct results) are a
# special case - the necessary halo depth is computed as
# the MAX of the halo and stencil depths, rather than as
# their sum.
# TODO #2781 - instead of this ad-hoc fix, we should add
# support for a new type of stencil.
self._var_depth = IntrinsicCall.create(
IntrinsicCall.Intrinsic.MAX,
[st_depth, self._var_depth.copy()])
else:
# Not a special case so the necessary depth is the sum of
# the halo and stencil depths.
self._var_depth = BinaryOperation.create(
BinaryOperation.Operator.ADD,
st_depth, self._var_depth.copy())
else:
self._var_depth = st_depth
# If this is an intergrid kernel and the field in question is on
# the fine mesh then we must double the halo depth
if call.is_intergrid and field.mesh == "gh_fine":
if self._var_depth:
self._var_depth = BinaryOperation.create(
BinaryOperation.Operator.MUL,
Literal("2", INTEGER_TYPE), self._var_depth.copy())
[docs]
class FSDescriptor():
''' Provides information about a particular function space used by
a meta-funcs entry in the kernel metadata. '''
def __init__(self, descriptor):
self._descriptor = descriptor
@property
def requires_basis(self):
''' Returns True if a basis function is associated with this
function space, otherwise it returns False. '''
return "gh_basis" in self._descriptor.operator_names
@property
def requires_diff_basis(self):
''' Returns True if a differential basis function is
associated with this function space, otherwise it returns
False. '''
return "gh_diff_basis" in self._descriptor.operator_names
@property
def fs_name(self):
''' Returns the raw metadata value of this function space. '''
return self._descriptor.function_space_name
[docs]
class FSDescriptors():
''' Contains a collection of FSDescriptor objects and methods
that provide information across these objects. We have one
FSDescriptor for each meta-funcs entry in the kernel
metadata.
# TODO #274 this should actually be named something like
BasisFuncDescriptors as it holds information describing the
basis/diff-basis functions required by a kernel.
:param descriptors: list of objects describing the basis/diff-basis \
functions required by a kernel, as obtained from \
metadata.
:type descriptors: list of :py:class:`psyclone.LFRicFuncDescriptor`.
'''
def __init__(self, descriptors):
self._orig_descriptors = descriptors
self._descriptors = []
for descriptor in descriptors:
self._descriptors.append(FSDescriptor(descriptor))
[docs]
def exists(self, fspace):
''' Return True if a descriptor with the specified function
space exists, otherwise return False. '''
for descriptor in self._descriptors:
# FS descriptors hold information taken from the kernel
# metadata and therefore it is the original name of
# the supplied function space that we must look at
if descriptor.fs_name == fspace.orig_name:
return True
return False
[docs]
def get_descriptor(self, fspace):
''' Return the descriptor with the specified function space
name. If it does not exist raise an error.'''
for descriptor in self._descriptors:
if descriptor.fs_name == fspace.orig_name:
return descriptor
raise GenerationError(
f"FSDescriptors:get_descriptor: there is no descriptor for "
f"function space {fspace.orig_name}")
@property
def descriptors(self):
'''
:return: the list of Descriptors, one for each of the meta-funcs
entries in the kernel metadata.
:rtype: List of :py:class:`psyclone.lfric.FSDescriptor`
'''
return self._descriptors
def check_args(call, parent_call):
'''
Checks that the kernel arguments provided via the invoke call are
consistent with the information expected, as specified by the
kernel metadata.
:param call: the object produced by the parser that describes the
kernel call to be checked.
:type call: :py:class:`psyclone.parse.algorithm.KernelCall`
:param parent_call: the kernel-call object.
:type parent_call: :py:class:`psyclone.domain.lfric.LFRicKern`
:raises: GenerationError if the kernel arguments in the Algorithm layer
do not match up with the kernel metadata.
'''
# stencil arguments
stencil_arg_count = 0
for arg_descriptor in call.ktype.arg_descriptors:
if arg_descriptor.stencil:
if not arg_descriptor.stencil['extent']:
# an extent argument must be provided
stencil_arg_count += 1
if arg_descriptor.stencil['type'] == 'xory1d':
# a direction argument must be provided
stencil_arg_count += 1
const = LFRicConstants()
# Quadrature arguments - will have as many as there are distinct
# quadrature shapes specified in the metadata.
qr_arg_count = len(set(call.ktype.eval_shapes).intersection(
set(const.VALID_QUADRATURE_SHAPES)))
# If a kernel operates on halo columns then it takes an extra, halo-depth
# argument from the Algorithm layer.
halo_depth_count = 0
if "halo" in call.ktype.iterates_over:
halo_depth_count = 1
expected_arg_count = (len(call.ktype.arg_descriptors) +
stencil_arg_count + qr_arg_count + halo_depth_count)
if expected_arg_count != len(call.args):
msg = ""
if parent_call:
invoke = parent_call.ancestor(LFRicInvokeSchedule)
if invoke:
msg = f"from invoke '{invoke.name}' "
raise GenerationError(
f"error: expected '{expected_arg_count}' arguments for the call "
f"to kernel '{call.ktype.name}' {msg}in the algorithm layer but "
f"found '{len(call.args)}'. Expected "
f"'{len(call.ktype.arg_descriptors)}' standard arguments, "
f"'{stencil_arg_count}' stencil arguments, '{qr_arg_count}' "
f"qr_arguments and '{halo_depth_count}' halo-depth arguments.")
[docs]
@dataclass(frozen=True)
class LFRicArgStencil:
'''
Provides stencil information about an LFRic kernel argument.
LFRicArgStencil can provide the extent, algorithm argument for the extent,
and the direction argument of a stencil or set any of these properties.
:param name: the name of the stencil.
:param extent: the extent of the stencil if it is known. It will
be known if it is specified in the metadata.
:param extent_arg: the algorithm argument associated with the extent
value if extent was not found in the metadata.
:param direction_arg: the direction argument associated with the
direction of the stencil if the direction of the
stencil is not known.
'''
name: str
extent: str = None
extent_arg: Any = None
direction_arg: Any = None
[docs]
class LFRicKernelArguments(Arguments):
'''
Provides information about LFRic kernel call arguments
collectively, as specified by the kernel argument metadata.
:param call: the kernel metadata for which to extract argument info.
:type call: :py:class:`psyclone.parse.KernelCall`
:param parent_call: the kernel-call object.
:type parent_call: :py:class:`psyclone.domain.lfric.LFRicKern`
:param bool check: whether to check for consistency between the
kernel metadata and the algorithm layer. Defaults to True.
:raises GenerationError: if the kernel metadata specifies stencil extent.
'''
def __init__(self, call, parent_call, check=True):
# pylint: disable=too-many-branches
Arguments.__init__(self, parent_call)
# check that the arguments provided by the algorithm layer are
# consistent with those expected by the kernel(s)
check_args(call, parent_call)
# create our arguments and add in stencil information where
# appropriate.
self._args = []
idx = 0
for arg in call.ktype.arg_descriptors:
lfric_argument = LFRicKernelArgument(self, arg, call.args[idx],
parent_call, check)
idx += 1
if lfric_argument.descriptor.stencil:
if lfric_argument.descriptor.stencil['extent']:
raise GenerationError("extent metadata not yet supported")
# if supported we would add the following
# line: stencil.extent =
# lfric_argument.descriptor.stencil['extent']
# An extent argument has been added.
stencil_extent_arg = call.args[idx]
idx += 1
if lfric_argument.descriptor.stencil['type'] == 'xory1d':
# a direction argument has been added
stencil = LFRicArgStencil(
name=lfric_argument.descriptor.stencil['type'],
extent_arg=stencil_extent_arg,
direction_arg=call.args[idx]
)
idx += 1
else:
# Create a stencil object and store a reference to it in
# our new LFRicKernelArgument object.
stencil = LFRicArgStencil(
name=lfric_argument.descriptor.stencil['type'],
extent_arg=stencil_extent_arg
)
lfric_argument.stencil = stencil
self._args.append(lfric_argument)
# We have now completed the construction of the kernel arguments so
# we can go back and update the names of any stencil size and/or
# direction variable names to ensure there are no clashes.
if self._parent_call:
inv_sched = self._parent_call.ancestor(InvokeSchedule)
if hasattr(inv_sched, "symbol_table"):
symtab = inv_sched.symbol_table
else:
# This can happen in stub generation.
symtab = LFRicSymbolTable()
else:
# TODO 719 The symtab is not connected to other parts of the
# Stub generation.
symtab = LFRicSymbolTable()
const = LFRicConstants()
for arg in self._args:
if not arg.descriptor.stencil:
continue
if not arg.stencil.extent_arg.is_literal():
if arg.stencil.extent_arg.varname:
# Ensure extent argument name is registered in the
# symbol_table.
tag = "AlgArgs_" + arg.stencil.extent_arg.text
root = arg.stencil.extent_arg.varname
symbol = symtab.find_or_create_tag(
tag, root, symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
arg.stencil.extent_arg.varname = symbol.name
if arg.descriptor.stencil['type'] == 'xory1d':
# a direction argument has been added
if arg.stencil.direction_arg.varname and \
arg.stencil.direction_arg.varname not in \
const.VALID_STENCIL_DIRECTIONS:
# Register the name of the direction argument to ensure
# it is unique in the PSy layer
tag = "AlgArgs_" + arg.stencil.direction_arg.text
root = arg.stencil.direction_arg.varname
symbol = symtab.find_or_create_tag(
tag, root,
symbol_type=DataSymbol,
datatype=LFRicTypes("LFRicIntegerScalarDataType")()
)
arg.stencil.direction_arg.varname = symbol.name
self._dofs = []
# Generate a static list of unique function-space names used
# by the set of arguments: store the mangled names as these
# are what we use at the level of an Invoke
self._unique_fs_names = []
# List of corresponding unique function-space objects
self._unique_fss = []
for arg in self._args:
for function_space in arg.function_spaces:
# We check that function_space is not None because scalar
# args don't have one and fields only have one (only
# operators have two).
if function_space and \
function_space.mangled_name not in self._unique_fs_names:
self._unique_fs_names.append(function_space.mangled_name)
self._unique_fss.append(function_space)
[docs]
def get_arg_on_space_name(self, func_space_name):
'''
Returns the first argument (field or operator) found that is on
the named function space, as specified in the kernel metadata. Also
returns the associated FunctionSpace object.
:param str func_space_name: Name of the function space (as specified \
in kernel metadata) for which to \
find an argument.
:return: the first kernel argument that is on the named function \
space and the associated FunctionSpace object.
:rtype: (:py:class:`psyclone.lfric.LFRicKernelArgument`,
:py:class:`psyclone.domain.lfric.FunctionSpace`)
:raises: FieldNotFoundError if no field or operator argument is found \
for the named function space.
'''
for arg in self._args:
for function_space in arg.function_spaces:
if function_space:
if func_space_name == function_space.orig_name:
return arg, function_space
raise FieldNotFoundError(f"LFRicKernelArguments:get_arg_on_space_name:"
f" there is no field or operator with "
f"function space {func_space_name}")
[docs]
def get_arg_on_space(self, func_space):
'''
Returns the first argument (field or operator) found that is on
the specified function space. The mangled name of the supplied
function space is used for comparison.
:param func_space: The function space for which to find an argument.
:type func_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:return: the first kernel argument that is on the supplied function
space
:rtype: :py:class:`psyclone.lfric.LFRicKernelArgument`
:raises: FieldNotFoundError if no field or operator argument is found
for the specified function space.
'''
for arg in self._args:
for function_space in arg.function_spaces:
if function_space:
if func_space.mangled_name == function_space.mangled_name:
return arg
raise FieldNotFoundError(f"LFRicKernelArguments:get_arg_on_space: "
f"there is no field or operator with function"
f" space {func_space.orig_name} (mangled name"
f" = '{func_space.mangled_name}')")
[docs]
def has_operator(self, op_type=None):
''' Returns true if at least one of the arguments is an operator
of type op_type (either gh_operator [LMA] or gh_columnwise_operator
[CMA]). If op_type is None then searches for *any* valid operator
type. '''
const = LFRicConstants()
if op_type and op_type not in const.VALID_OPERATOR_NAMES:
raise GenerationError(
f"If supplied, 'op_type' must be a valid operator type (one "
f"of {const.VALID_OPERATOR_NAMES}) but got '{op_type}'")
if not op_type:
# If no operator type is specified then we match any type
op_list = const.VALID_OPERATOR_NAMES
else:
op_list = [op_type]
for arg in self._args:
if arg.argument_type in op_list:
return True
return False
@property
def unique_fss(self):
''' Returns a unique list of function space objects used by the
arguments of this kernel '''
return self._unique_fss
@property
def unique_fs_names(self):
''' Return the list of unique function space names used by the
arguments of this kernel. The names are unmangled (i.e. as
specified in the kernel metadata) '''
return self._unique_fs_names
@property
def first_field_or_operator(self):
'''
:returns: the first field or operator argument in the list.
:rtype: :py:class:`psyclone.lfric.LFRicKernelArgument`
:raises InternalError: if no field or operator argument is found.
'''
for arg in self._args:
arg: LFRicKernelArgument
if arg.is_field or arg.is_operator:
return arg
raise InternalError(
f"Invalid LFRic kernel: failed to find an LFRicKernelArgument that"
f" is a field or operator in '{self.names}'.")
[docs]
def iteration_space_arg(self):
'''
Returns an argument we can use to dereference the iteration
space. This can be a field or operator that is modified or
alternatively a field that is read if one or more scalars
are modified. If a kernel writes to more than one argument then
that requiring the largest iteration space is selected.
:return: Kernel argument from which to obtain iteration space
:rtype: :py:class:`psyclone.lfric.LFRicKernelArgument`
'''
# Since we always compute operators out to the L1 halo we first
# check whether this kernel writes to an operator
write_accesses = AccessType.all_write_accesses()
const = LFRicConstants()
op_args = psyGen.args_filter(
self._args,
arg_types=const.VALID_OPERATOR_NAMES,
arg_accesses=write_accesses)
if op_args:
return op_args[0]
# Is this an inter-grid kernel? If so, then the iteration space
# is determined by the coarse mesh, irrespective of whether
# we are prolonging (and thus writing to a field on the fine mesh)
# or restricting.
if self._parent_call.is_intergrid:
fld_args = psyGen.args_filter(
self._args,
arg_types=const.VALID_FIELD_NAMES,
arg_meshes=["gh_coarse"])
return fld_args[0]
# This is not an inter-grid kernel and it does not write to an
# operator. We now check for fields that are written to. We
# check first for any modified field on a continuous function
# space, failing that we try any_space function spaces
# (because we must assume such a space is continuous) and
# finally we try all discontinuous function spaces including
# any_discontinuous_space. We do this because if a quantity on
# a continuous FS is modified then our iteration space must be
# larger (include L1-halo cells)
const = LFRicConstants()
write_accesses = AccessType.all_write_accesses()
fld_args = psyGen.args_filter(
self._args,
arg_types=const.VALID_FIELD_NAMES,
arg_accesses=write_accesses)
if fld_args:
for spaces in [const.CONTINUOUS_FUNCTION_SPACES,
const.VALID_ANY_SPACE_NAMES,
const.VALID_DISCONTINUOUS_NAMES]:
for arg in fld_args:
if arg.function_space.orig_name in spaces:
return arg
# No modified fields or operators. Check for unmodified fields...
fld_args = psyGen.args_filter(
self._args,
arg_types=const.VALID_FIELD_NAMES)
if fld_args:
return fld_args[0]
# it is an error if we get to here
raise GenerationError(
"iteration_space_arg(). The lfric api must have a modified "
"field, a modified operator, or an unmodified field (in the case "
"of a modified scalar). None of these were found.")
@property
def dofs(self):
''' Currently required for Invoke base class although this
makes no sense for LFRic. Need to refactor the Invoke base class
and remove the need for this property (#279). '''
return self._dofs
[docs]
def psyir_expressions(self):
'''
:returns: the PSyIR expressions representing this Argument list.
:rtype: list of :py:class:`psyclone.psyir.nodes.Node`
'''
create_arg_list = KernCallArgList(self._parent_call)
create_arg_list.generate()
return create_arg_list.psyir_arglist
@property
def acc_args(self):
'''
:returns: the list of quantities that must be available on an \
OpenACC device before the associated kernel can be launched.
:rtype: list of str
'''
create_acc_arg_list = KernCallAccArgList(self._parent_call)
create_acc_arg_list.generate()
return create_acc_arg_list.arglist
@property
def scalars(self):
'''
Provides the list of names of scalar arguments required by the
kernel associated with this Arguments object. If there are none
then the returned list is empty.
:returns: A list of the names of scalar arguments in this object.
:rtype: list of str
'''
# Return nothing for the moment as it is unclear whether
# scalars need to be explicitly dealt with (for OpenACC) in
# the LFRic API.
return []
[docs]
class LFRicKernelArgument(KernelArgument):
'''
This class provides information about individual LFRic kernel call
arguments as specified by the kernel argument metadata and the
kernel invocation in the Algorithm layer.
:param kernel_args: object encapsulating all arguments to the \
kernel call.
:type kernel_args: :py:class:`psyclone.lfric.LFRicKernelArguments`
:param arg_meta_data: information obtained from the metadata for \
this kernel argument.
:type arg_meta_data: :py:class:`psyclone.domain.lfric.LFRicArgDescriptor`
:param arg_info: information on how this argument is specified in \
the Algorithm layer.
:type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
:param call: the kernel object with which this argument is associated.
:type call: :py:class:`psyclone.domain.lfric.LFRicKern`
:param bool check: whether to check for consistency between the \
kernel metadata and the algorithm layer. Defaults to True.
:raises InternalError: for an unsupported metadata in the argument \
descriptor data type.
'''
# pylint: disable=too-many-public-methods, too-many-instance-attributes
def __init__(self, kernel_args, arg_meta_data, arg_info, call, check=True):
# Keep a reference to LFRicKernelArguments object that contains
# this argument. This permits us to manage name-mangling for
# any-space function spaces.
self._kernel_args = kernel_args
self._vector_size = arg_meta_data.vector_size
self._argument_type = arg_meta_data.argument_type
self._stencil = None
if arg_meta_data.mesh:
self._mesh = arg_meta_data.mesh.lower()
else:
self._mesh = None
# The list of function-space objects for this argument. Each
# object can be queried for its original name and for the
# mangled name (used to make any-space arguments distinct
# within an invoke). The argument will only have more than
# one function-space associated with it if it is an operator.
fs1 = None
fs2 = None
if self.is_operator:
fs1 = FunctionSpace(arg_meta_data.function_space_to,
self._kernel_args)
fs2 = FunctionSpace(arg_meta_data.function_space_from,
self._kernel_args)
else:
if arg_meta_data.function_space:
fs1 = FunctionSpace(arg_meta_data.function_space,
self._kernel_args)
self._function_spaces = [fs1, fs2]
# Set the argument's intrinsic type from its descriptor's
# data type and check if an invalid data type is passed from
# the argument descriptor.
try:
const = LFRicConstants()
self._intrinsic_type = const.MAPPING_DATA_TYPES[
arg_meta_data.data_type]
except KeyError as err:
raise InternalError(
f"LFRicKernelArgument.__init__(): Found unsupported data "
f"type '{arg_meta_data.data_type}' in the kernel argument "
f"descriptor '{arg_meta_data}'.") from err
# Addressing issue #753 will allow us to perform static checks
# for consistency between the algorithm and the kernel
# metadata. This will include checking that a field on a read
# only function space is not passed to a kernel that modifies
# it. Note, issue #79 is also related to this.
KernelArgument.__init__(self, arg_meta_data, arg_info, call)
# Argument proxy data type (if/as defined in LFRic infrastructure)
self._proxy_data_type = None
# Set up kernel argument information for scalar, field and operator
# arguments: precision, module name, data type and proxy data type
self._init_data_type_properties(arg_info, check)
# Complete the initialisation of the argument (after
# _init_data_type_properties() so the precision info etc is
# already set up)
self._complete_init(arg_info)
@classmethod
def _ensure_unique_name(cls, name: str) -> str:
'''
Given the proposed argument name, returns a new name that will be
unique in the final PSy routine.
:param name: the proposed name of a kernel argument.
:returns: a new name for the kernel argument.
'''
# Symbol imports for STENCILS are not yet in the symbol table (until
# lowering time), so make sure the argument names do not overlap with
# them.
const = LFRicConstants()
new_name = name
if name.upper() in const.STENCIL_MAPPING.values():
new_name += "_arg"
return new_name
[docs]
def generate_method_call(self, method, function_space=None):
'''
Generate a PSyIR call to the given method of this object.
:param str method: name of the method to generate a call to.
:param Optional[str] function_space: name of the function space.
:returns: the generated call.
:rtype: :py:class:`psyclone.psyir.nodes.Call`
'''
# Go through invoke.schedule in case the link has bee updated
symtab = self._call.ancestor(InvokeSchedule).invoke.schedule\
.symbol_table
# Use the proxy variable as derived type base
symbol = symtab.lookup(self.proxy_name)
if self._vector_size > 1:
# For a field vector, just call the specified method on the first
# element
return Call.create(ArrayOfStructuresReference.create(
symbol, [Literal('1', INTEGER_TYPE)],
[self.ref_name(function_space), method]))
return Call.create(StructureReference.create(
symbol, [self.ref_name(function_space), method]))
[docs]
def generate_accessor(self, function_space=None):
'''
Generate a Reference accessing this object's data.
:param Optional[str] function_space: name of the function space.
:returns: the generated Reference.
:rtype: :py:class:`psyclone.psyir.nodes.Reference`
'''
# Go through invoke.schedule in case the link has bee updated
symtab = self._call.ancestor(InvokeSchedule).invoke\
.schedule.symbol_table
symbol = symtab.lookup(self.proxy_name)
if self._vector_size > 1:
# For a field vector, access the first element
return ArrayOfStructuresReference.create(
symbol, [Literal('1', INTEGER_TYPE)],
[self.ref_name(function_space)])
return StructureReference.create(
symbol, [self.ref_name(function_space)])
[docs]
def ref_name(self, function_space=None):
'''
Returns the name used to dereference this type of argument (depends
on whether it is a field or operator and, if the latter, whether it
is the to- or from-space that is specified).
:param function_space: the function space of this argument
:type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace`
:returns: the name used to dereference this argument.
:rtype: str
:raises GenerationError: if the supplied function space is not one \
of the function spaces associated with \
this argument.
:raises GenerationError: if the supplied function space is not being \
returned by either 'function_space_from' or \
'function_space_to'.
:raises GenerationError: if the argument type is not supported.
'''
# pylint: disable=too-many-branches
if not function_space:
if self.is_operator:
# For an operator we use the 'from' FS
function_space = self._function_spaces[1]
else:
function_space = self._function_spaces[0]
else:
# Check that the supplied function space is valid for this
# argument
found = False
for fspace in self.function_spaces:
if fspace and fspace.orig_name == function_space.orig_name:
found = True
break
if not found:
raise GenerationError(
f"LFRicKernelArgument.ref_name(fs): The supplied function "
f"space (fs='{function_space.orig_name}') is not one of "
f"the function spaces associated with this argument "
f"(fss={self.function_space_names}).")
if self.is_field:
return "vspace"
if self.is_operator:
if function_space.orig_name == self.descriptor.function_space_from:
return "fs_from"
if function_space.orig_name == self.descriptor.function_space_to:
return "fs_to"
raise GenerationError(
f"LFRicKernelArgument.ref_name(fs): Function space "
f"'{function_space.orig_name}' is one of the 'gh_operator' "
f"function spaces '{self.function_spaces}' but is not being "
f"returned by either function_space_from "
f"'{self.descriptor.function_space_from}' or "
f"function_space_to '{self.descriptor.function_space_to}'.")
raise GenerationError(
f"LFRicKernelArgument.ref_name(fs): Found unsupported argument "
f"type '{self._argument_type}'.")
def _init_data_type_properties(self, arg_info, check=True):
'''Set up kernel argument information from LFRicConstants: precision,
data type, proxy data type and module name. This is currently
supported for scalar, field and operator arguments.
:param arg_info: information on how this argument is specified \
in the Algorithm layer.
:type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
:param bool check: whether to use the algorithm \
information. Optional argument that defaults to True.
'''
alg_datatype_info = None
if arg_info:
alg_datatype_info = arg_info._datatype
alg_datatype = None
alg_precision = None
if alg_datatype_info:
alg_datatype, alg_precision = alg_datatype_info
const = LFRicConstants()
if arg_info and arg_info.form == "collection":
try:
alg_datatype = const.FIELD_VECTOR_TO_FIELD_MAP[alg_datatype]
except KeyError:
# The collection datatype is not recognised or supported.
alg_datatype = None
if self.is_scalar:
self._init_scalar_properties(alg_datatype, alg_precision,
check)
elif self.is_field:
self._init_field_properties(alg_datatype, check)
elif self.is_operator:
self._init_operator_properties(alg_datatype, check)
else:
raise InternalError(
f"Supported argument types are scalar, field and operator, "
f"but the argument '{self.name}' in kernel "
f"'{self._call.name}' is none of these.")
def _init_scalar_properties(
self, alg_datatype, alg_precision, check=True):
'''Set up the properties of this scalar using algorithm datatype
information if it is available.
:param alg_datatype: the datatype of this argument as \
specified in the algorithm layer or None if it is not \
known.
:type alg_datatype: str or NoneType
:param alg_precision: the precision of this argument as \
specified in the algorithm layer or None if it is not \
known.
:type alg_precision: str or NoneType
:param bool check: whether to use the algorithm \
information. Optional argument that defaults to True.
:raises InternalError: if the intrinsic type of the scalar is \
not supported.
:raises GenerationError: if the datatype specified in the \
algorithm layer is inconsistent with the kernel metadata.
:raises GenerationError: if the datatype for a gh_scalar \
could not be found in the algorithm layer.
:raises NotImplementedError: if the scalar is a reduction and \
its intrinsic type is not real.
:raises GenerationError: if the scalar is a reduction and is \
not declared with default precision.
'''
const = LFRicConstants()
# Check the type of scalar defined in the metadata is supported.
if self.intrinsic_type not in const.VALID_INTRINSIC_TYPES:
raise InternalError(
f"Expected one of {const.VALID_INTRINSIC_TYPES} intrinsic "
f"types for a scalar argument but found "
f"'{self.intrinsic_type}' in the metadata of kernel "
f"{self._call.name} for argument {self.name}.")
# Check the metadata and algorithm types are consistent if
# the algorithm information is available and is not being ignored.
if check and alg_datatype and \
alg_datatype != self.intrinsic_type:
raise GenerationError(
f"The kernel metadata for argument '{self.name}' in "
f"kernel '{self._call.name}' specifies this argument "
f"should be a scalar of type '{self.intrinsic_type}' but "
f"in the algorithm layer it is defined as a "
f"'{alg_datatype}'.")
# If the algorithm information is not being ignored and
# the datatype is known in the algorithm layer and it is
# not a literal then its precision should also be defined.
if check and alg_datatype and not alg_precision and \
not self.is_literal:
raise GenerationError(
f"LFRic coding standards require scalars to have "
f"their precision defined in the algorithm layer but "
f"'{self.name}' in '{self._call.name}' does not.")
if self.access in AccessType.get_valid_reduction_modes():
# Treat reductions separately to other scalars as it
# is expected that they should match the precision of
# the field they are reducing. At the moment there is
# an assumption that the precision will always be a
# particular value (the default), see issue #1570.
# Only real reductions are supported.
if not self.intrinsic_type == "real":
raise NotImplementedError(
"Reductions for datatypes other than real are not yet "
"supported in PSyclone.")
expected_precision = const.DATA_TYPE_MAP["reduction"]["kind"]
# If the algorithm information is not being ignored
# then check that the expected precision and the
# precision defined in the algorithm layer are
# the same.
if check and alg_precision and \
alg_precision != expected_precision:
raise GenerationError(
f"This scalar is a reduction which assumes precision "
f"of type '{expected_precision}' but the algorithm "
f"declares this scalar with precision "
f"'{alg_precision}'.")
# Use the default 'real' scalar reduction properties.
self._precision = expected_precision
self._data_type = const.DATA_TYPE_MAP["reduction"]["type"]
self._proxy_data_type = const.DATA_TYPE_MAP[
"reduction"]["proxy_type"]
self._module_name = const.DATA_TYPE_MAP["reduction"]["module"]
else:
# This is a scalar that is not part of a reduction.
if check and alg_precision:
# Use the algorithm precision if it is available
# and not being ignored.
self._precision = alg_precision
else:
# Use default precision for this datatype if the
# algorithm precision is either not available or is
# being ignored.
self._precision = const.SCALAR_PRECISION_MAP[
self.intrinsic_type]
def _init_field_properties(self, alg_datatype, check=True):
'''Set up the properties of this field using algorithm datatype
information if it is available.
:param alg_datatype: the datatype of this argument as \
specified in the algorithm layer or None if it is not \
known.
:type alg_datatype: str or NoneType
:param bool check: whether to use the algorithm \
information. Optional argument that defaults to True.
:raises GenerationError: if the datatype for a gh_field \
could not be found in the algorithm layer.
:raises GenerationError: if the datatype specified in the \
algorithm layer is inconsistent with the kernel metadata.
:raises InternalError: if the intrinsic type of the field is \
not supported (i.e. is not real or integer).
'''
const = LFRicConstants()
argtype = None
# If the algorithm information is not being ignored then
# it must be available.
if check and not alg_datatype:
raise GenerationError(
f"It was not possible to determine the field type from "
f"the algorithm layer for argument '{self.name}' in "
f"kernel '{self._call.name}'.")
# If the algorithm information is not being ignored then
# check the metadata and algorithm type are consistent and
# that the metadata specifies a supported intrinsic type.
if self.intrinsic_type == "real":
if not check:
# Use the default as we are ignoring any algorithm info
argtype = "field"
elif alg_datatype == "field_type":
argtype = "field"
elif alg_datatype == "r_bl_field_type":
argtype = "r_bl_field"
elif alg_datatype == "r_solver_field_type":
argtype = "r_solver_field"
elif alg_datatype == "r_tran_field_type":
argtype = "r_tran_field"
else:
raise GenerationError(
f"The metadata for argument '{self.name}' in kernel "
f"'{self._call.name}' specifies that this is a real "
f"field, however it is declared as a "
f"'{alg_datatype}' in the algorithm code.")
elif self.intrinsic_type == "integer":
if check and alg_datatype != "integer_field_type":
raise GenerationError(
f"The metadata for argument '{self.name}' in kernel "
f"'{self._call.name}' specifies that this is an "
f"integer field, however it is declared as a "
f"'{alg_datatype}' in the algorithm code.")
argtype = "integer_field"
else:
raise InternalError(
f"Expected one of {const.VALID_FIELD_INTRINSIC_TYPES} "
f"intrinsic types for a field argument but found "
f"'{self.intrinsic_type}'.")
self._data_type = const.DATA_TYPE_MAP[argtype]["type"]
self._precision = const.DATA_TYPE_MAP[argtype]["kind"]
self._proxy_data_type = const.DATA_TYPE_MAP[argtype]["proxy_type"]
self._module_name = const.DATA_TYPE_MAP[argtype]["module"]
def _init_operator_properties(self, alg_datatype, check=True):
'''Set up the properties of this operator using algorithm datatype
information if it is available.
:param alg_datatype: the datatype of this argument as \
specified in the algorithm layer or None if it is not \
known.
:type alg_datatype: str or NoneType
:param bool check: whether to use the algorithm \
information. Optional argument that defaults to True.
:raises GenerationError: if the datatype for a gh_operator \
could not be found in the algorithm layer (and check is \
True).
:raises GenerationError: if the datatype specified in the \
algorithm layer is inconsistent with the kernel metadata.
:raises InternalError: if this argument is not an operator.
'''
const = LFRicConstants()
argtype = None
if self.argument_type == "gh_operator":
if not check:
# Use the default as we are ignoring any algorithm info
argtype = "operator"
elif not alg_datatype:
# Raise an exception as we require algorithm
# information to determine the precision of the
# operator
raise GenerationError(
f"It was not possible to determine the operator type "
f"from the algorithm layer for argument '{self.name}' "
f"in kernel '{self._call.name}'.")
elif alg_datatype == "operator_type":
argtype = "operator"
elif alg_datatype == "r_solver_operator_type":
argtype = "r_solver_operator"
elif alg_datatype == "r_tran_operator_type":
argtype = "r_tran_operator"
else:
raise GenerationError(
f"The metadata for argument '{self.name}' in kernel "
f"'{self._call.name}' specifies that this is an "
f"operator, however it is declared as a "
f"'{alg_datatype}' in the algorithm code.")
elif self.argument_type == "gh_columnwise_operator":
if check and alg_datatype and \
alg_datatype != "columnwise_operator_type":
raise GenerationError(
f"The metadata for argument '{self.name}' in kernel "
f"'{self._call.name}' specifies that this is a "
f"columnwise operator, however it is declared as a "
f"'{alg_datatype}' in the algorithm code.")
argtype = "columnwise_operator"
else:
raise InternalError(
f"Expected 'gh_operator' or 'gh_columnwise_operator' "
f"argument type but found '{self.argument_type}'.")
self._data_type = const.DATA_TYPE_MAP[argtype]["type"]
self._precision = const.DATA_TYPE_MAP[argtype]["kind"]
self._proxy_data_type = const.DATA_TYPE_MAP[argtype]["proxy_type"]
self._module_name = const.DATA_TYPE_MAP[argtype]["module"]
@property
def is_scalar(self):
'''
:returns: True if this kernel argument represents a scalar, \
False otherwise.
:rtype: bool
'''
const = LFRicConstants()
return self._argument_type in const.VALID_SCALAR_NAMES
@property
def is_field(self):
'''
:returns: True if this kernel argument represents a field, \
False otherwise.
:rtype: bool
'''
const = LFRicConstants()
return self._argument_type in const.VALID_FIELD_NAMES
@property
def is_operator(self):
'''
:returns: True if this kernel argument represents an operator, \
False otherwise.
:rtype: bool
'''
const = LFRicConstants()
return self._argument_type in const.VALID_OPERATOR_NAMES
@property
def descriptor(self):
'''
:returns: a descriptor object which contains Kernel metadata \
about this argument.
:rtype: :py:class:`psyclone.domain.lfric.LFRicArgDescriptor`
'''
return self._arg
@property
def argument_type(self):
'''
:returns: the API type of this argument, as specified in \
the metadata.
:rtype: str
'''
return self._argument_type
@property
def intrinsic_type(self):
'''
:returns: the intrinsic Fortran type of this argument for scalars \
or of the argument's data for fields and operators.
:rtype: str
'''
return self._intrinsic_type
@property
def mesh(self):
'''
:returns: mesh associated with argument ('GH_FINE' or 'GH_COARSE').
:rtype: str
'''
return self._mesh
@property
def vector_size(self):
'''
:returns: the vector size of this argument as specified in \
the Kernel metadata.
:rtype: str
'''
return self._vector_size
@property
def name_indexed(self):
'''
:returns: the name for this argument with an additional index \
which accesses the first element for a vector argument.
:rtype: str
'''
if self._vector_size > 1:
return self._name+"(1)"
return self._name
[docs]
def psyir_expression(self):
'''
Looks up or creates a reference to a suitable Symbol for this kernel
argument. If the argument is a scalar that has been provided as a
literal (in the Algorithm layer) then the PSyIR of the expression
is returned.
:returns: the PSyIR for this kernel argument.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
:raises InternalError: if this argument is a literal but we fail to \
construct PSyIR that is consistent with this.
:raises NotImplementedError: if this argument is not a literal, scalar
or field.
'''
symbol_table = self._call.scope.symbol_table
if self.is_literal:
reader = FortranReader()
if self.precision:
# Ensure any associated precision symbol is in the table.
symbol_table.add_lfric_precision_symbol(self.precision)
lit = reader.psyir_from_expression(self.name, symbol_table)
# Sanity check that the resulting expression is a literal.
if lit.walk(Reference):
raise InternalError(
f"Expected argument '{self.name}' to kernel "
f"'{self.call.name}' to be a literal but the created "
f"PSyIR contains one or more References.")
return lit
if self.is_scalar:
try:
scalar_sym = symbol_table.lookup(self.name)
except KeyError:
# TODO once #1258 is done the symbols should already exist
# and therefore we should raise an exception if not.
scalar_sym = symbol_table.new_symbol(
self.name, symbol_type=DataSymbol,
datatype=self.infer_datatype())
return Reference(scalar_sym)
const = LFRicConstants()
try:
suffix = const.ARG_TYPE_SUFFIX_MAPPING[self.argument_type]
tag_name = f"{self.name}:{suffix}"
sym = symbol_table.lookup_with_tag(tag_name)
return Reference(sym)
except KeyError as err:
raise NotImplementedError(
f"Unsupported kernel argument type: '{self.name}' is of type "
f"'{self.argument_type}' which is not recognised as being a "
f"literal, scalar or field.") from err
@property
def declaration_name(self):
'''
:returns: the name for this argument with the array dimensions \
added if required.
:rtype: str
'''
if self._vector_size > 1:
return self._name+"("+str(self._vector_size)+")"
return self._name
@property
def proxy_name(self):
'''
:returns: the proxy name for this argument.
:rtype: str
'''
return self._name+"_proxy"
@property
def proxy_name_indexed(self):
'''
:returns: the proxy name for this argument with an additional \
index which accesses the first element for a vector \
argument.
:rtype: str
'''
if self._vector_size > 1:
return self._name+"_proxy(1)"
return self._name+"_proxy"
@property
def proxy_declaration_name(self):
'''
:returns: the proxy name for this argument with the array \
dimensions added if required.
:rtype: str
'''
if self._vector_size > 1:
return self.proxy_name+"("+str(self._vector_size)+")"
return self.proxy_name
@property
def proxy_data_type(self):
'''
:returns: the type of this argument's proxy (if it exists) as \
defined in LFRic infrastructure.
:rtype: str or NoneType
'''
return self._proxy_data_type
@property
def function_space(self):
'''
Returns the expected finite element function space for a kernel
argument as specified by the kernel argument metadata: a single
function space for a field and function_space_from for an operator.
:returns: function space for this argument.
:rtype: :py:class:`psyclone.domain.lfric.FunctionSpace`
'''
if self._argument_type == "gh_operator":
# We return the 'from' space for an operator argument
return self.function_space_from
return self._function_spaces[0]
@property
def function_space_to(self):
'''
:returns: the 'to' function space of an operator.
:rtype: str
'''
return self._function_spaces[0]
@property
def function_space_from(self):
'''
:returns: the 'from' function space of an operator.
:rtype: str
'''
return self._function_spaces[1]
@property
def function_spaces(self):
'''
Returns the expected finite element function space for a kernel
argument as specified by the kernel argument metadata: a single
function space for a field and a list containing
function_space_to and function_space_from for an operator.
:returns: function space(s) for this argument.
:rtype: list of :py:class:`psyclone.domain.lfric.FunctionSpace`
'''
return self._function_spaces
@property
def function_space_names(self):
'''
Returns a list of the names of the function spaces associated
with this argument. We have more than one function space when
dealing with operators.
:returns: list of function space names for this argument.
:rtype: list of str
'''
fs_names = []
for fspace in self._function_spaces:
if fspace:
fs_names.append(fspace.orig_name)
return fs_names
@property
def intent(self):
'''
Returns the Fortran intent of this argument as defined by the
valid access types for this API
:returns: the expected Fortran intent for this argument as \
specified by the kernel argument metadata
:rtype: str
'''
write_accesses = AccessType.all_write_accesses()
if self.access == AccessType.READ:
return "in"
if self.access in write_accesses:
return "inout"
# An argument access other than the pure "read" or one of
# the "write" accesses is invalid
valid_accesses = [AccessType.READ.api_specific_name()] + \
[access.api_specific_name() for access in write_accesses]
raise GenerationError(
f"In the LFRic API the argument access must be one of "
f"{valid_accesses}, but found '{self.access}'.")
@property
def discontinuous(self):
'''
Returns True if this argument is known to be on a discontinuous
function space including any_discontinuous_space, otherwise
returns False.
:returns: whether the argument is discontinuous.
:rtype: bool
'''
const = LFRicConstants()
if self.function_space.orig_name in \
const.VALID_DISCONTINUOUS_NAMES:
return True
if self.function_space.orig_name in \
const.VALID_ANY_SPACE_NAMES:
# We will eventually look this up based on our dependence
# analysis but for the moment we assume the worst
return False
return False
@property
def stencil(self):
'''
:returns: stencil information for this argument if it exists.
:rtype: :py:class:`psyclone.lfric.LFRicArgStencil`
'''
return self._stencil
@stencil.setter
def stencil(self, value):
'''
Sets stencil information for this kernel argument.
:param value: stencil information for this argument.
:type value: :py:class:`psyclone.lfric.LFRicArgStencil`
'''
self._stencil = value
[docs]
def infer_datatype(self, proxy: bool = False) -> DataType:
'''
Infer the datatype of this kernel argument in the PSy layer using
the LFRic API rules. If any LFRic infrastructure modules are required
but are not already present then suitable ContainerSymbols are added
to the outermost symbol table. Similarly, DataTypeSymbols are added for
any required LFRic derived types that are not already in the symbol
table.
TODO #1258 - ultimately this routine should not have to create any
DataTypeSymbols as that should already have been done.
:param proxy: whether or not we want the type of the proxy
object for this kernel argument. Defaults to False (i.e.
return the type rather than the proxy type).
:returns: the datatype of this argument.
:raises NotImplementedError: if an unsupported argument type is found.
'''
scope = self._call.ancestor(Container)
if scope is None:
# Prefer the module scope, but some tests that are disconnected can
# use the current scope
scope = self._call.scope
symtab = scope.symbol_table
def _find_or_create_type(mod_name: str,
type_name: str) -> DataTypeSymbol:
'''
Utility to find or create a DataTypeSymbol with the supplied name,
imported from the named module.
:param mod_name: the name of the module from which the
DataTypeSymbol should be imported.
:param type_name: the name of the derived type for which to
create a DataTypeSymbol.
:returns: the symbol for the requested type.
'''
return symtab.find_or_create(
type_name,
symbol_type=DataTypeSymbol,
datatype=UnresolvedType(),
interface=ImportInterface(symtab.find_or_create(
mod_name,
symbol_type=ContainerSymbol)
))
if self.is_scalar:
# Find or create the DataType for the appropriate scalar type.
if self.intrinsic_type == "real":
prim_type = ScalarType.Intrinsic.REAL
elif self.intrinsic_type == "integer":
prim_type = ScalarType.Intrinsic.INTEGER
elif self.intrinsic_type == "logical":
prim_type = ScalarType.Intrinsic.BOOLEAN
else:
raise NotImplementedError(
f"Unsupported scalar type '{self.intrinsic_type}'")
kind_name = self.precision
try:
kind_symbol = symtab.lookup(kind_name)
except KeyError:
mod_map = LFRicConstants().UTILITIES_MOD_MAP
const_mod = mod_map["constants"]["module"]
try:
constants_container = symtab.lookup(const_mod)
except KeyError:
# TODO Once #696 is done, we should *always* have a
# symbol for this container at this point so should
# raise an exception if we haven't.
constants_container = LFRicTypes(const_mod)
symtab.add(constants_container)
kind_symbol = DataSymbol(
kind_name, INTEGER_TYPE,
interface=ImportInterface(constants_container))
symtab.add(kind_symbol)
return ScalarType(prim_type, Reference(kind_symbol))
if self.is_field or self.is_operator:
# Find or create the DataTypeSymbol for the appropriate
# field or operator type.
mod_name = self._module_name
if proxy:
type_name = self._proxy_data_type
else:
type_name = self._data_type
dts = _find_or_create_type(mod_name, type_name)
if self.is_field and self.vector_size > 1:
# A field vector must be of ArrayType
return ArrayType(dts, [self.vector_size])
return dts
raise NotImplementedError(
f"'{str(self)}' is not a scalar, field or operator argument")
[docs]
class LFRicACCEnterDataDirective(ACCEnterDataDirective):
'''
Sub-classes ACCEnterDataDirective to provide an API-specific implementation
of data_on_device().
'''
[docs]
def data_on_device(self, _):
'''
Provide a hook to be able to add information about data being on a
device (or not). This is currently not used in LFRic.
'''
return None
# ---------- Documentation utils -------------------------------------------- #
# The list of module members that we wish AutoAPI to generate
# documentation for.
__all__ = [
'LFRicFuncDescriptor',
'LFRicFunctionSpaces',
'LFRicProxies',
'LFRicLMAOperators',
'LFRicCMAOperators',
'LFRicMeshes',
'LFRicInterGrid',
'LFRicBasisFunctions',
'LFRicBoundaryConditions',
'LFRicGlobalSum',
'LFRicHaloExchange',
'LFRicHaloExchangeStart',
'LFRicHaloExchangeEnd',
'HaloDepth',
'HaloWriteAccess',
'HaloReadAccess',
'FSDescriptor',
'FSDescriptors',
'LFRicArgStencil',
'LFRicKernelArguments',
'LFRicKernelArgument',
'LFRicACCEnterDataDirective']