Source code for psyclone.domain.lfric.lfric_types

# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2023-2024, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors: J. Henrichs, Bureau of Meteorology
#          A. R. Porter, STFC Daresbury Lab
#          O. Brunt, Met Office

'''This module contains a singleton class that manages LFRic types. '''


from collections import namedtuple
from dataclasses import dataclass

from psyclone.configuration import Config
from psyclone.domain.lfric.lfric_constants import LFRicConstants
from psyclone.errors import InternalError
from psyclone.psyir.nodes import Literal
from psyclone.psyir.symbols import (ArrayType, ContainerSymbol, DataSymbol,
                                    ImportInterface, INTEGER_TYPE, ScalarType)


[docs]class LFRicTypes: '''This class implements a singleton that manages LFRic types. Using the 'call' interface, you can query the data type for LFRic types, e.g.: >>> from psyclone.configuration import Config >>> from psyclone.domain.lfric import LFRicTypes >>> config = Config.get() >>> num_dofs_class = LFRicTypes("NumberOfUniqueDofsDataSymbol") >>> my_var = num_dofs_class("my_num_dofs") >>> print(my_var.name) my_num_dofs It uses the __new__ function to implement the access to the internal dictionary. This is done to minimise the required code for getting a value, e. g. compared with ``LFRicTypes.get()("something")``, or ``LFRicType.get("something")``. ''' # Class variable to store the singleton instance _instance = None # Class variable to store the mapping of names to the various objects # (classes and various instances) managed by this class. _name_to_class = {} # ------------------------------------------------------------------------ def __new__(cls, name): '''The class creation function __new__ is used to actually provide the object the user is querying. It is well documented that __new__ can return a different instance. This function will first make sure that the static internal dictionary is initialised (so it acts like a singleton in that regard). Then it will return the value the user asked for. :param str name: the name to query for. :returns: the corresponding object, which can be a class or an \ instance. :rtype: object (various types) :raises InternalError: if there specified name is not a name for \ an object managed here. ''' if not LFRicTypes._name_to_class: LFRicTypes.init() try: return LFRicTypes._name_to_class[name] except KeyError as err: raise InternalError(f"Unknown LFRic type '{name}'. Valid values " f"are {LFRicTypes._name_to_class.keys()}") \ from err # ------------------------------------------------------------------------ def __call__(self): '''This function is only here to trick pylint into thinking that the object returned from __new__ is callable, meaning that code like: ``LFRicTypes("LFRicIntegerScalarDataType")()`` does not trigger a pylint warning about not being callable. ''' # ------------------------------------------------------------------------ @staticmethod def init(): '''This method constructs the required classes and instances, and puts them into the dictionary. ''' # The global mapping of names to the corresponding classes or instances LFRicTypes._name_to_class = {} LFRicTypes._create_precision_from_const_module() LFRicTypes._create_generic_scalars() LFRicTypes._create_lfric_dimension() LFRicTypes._create_specific_scalars() LFRicTypes._create_fields() # Generate LFRic vector-field-data symbols as subclasses of # field-data symbols const = LFRicConstants() for intrinsic in const.VALID_FIELD_INTRINSIC_TYPES: name = f"{intrinsic.title()}VectorFieldDataSymbol" baseclass = LFRicTypes(f"{intrinsic.title()}FieldDataSymbol") LFRicTypes._name_to_class[name] = type(name, (baseclass, ), {}) # ------------------------------------------------------------------------ @staticmethod def _create_precision_from_const_module(): '''This function implements all precisions defined in the `dynamo0.3` (LFRic) domain. It adds "constants_mod" as ContainerSymbol. The names are added to the global mapping. ''' # The first Module namedtuple argument specifies the name of the # module and the second argument declares the name(s) of any symbols # declared by the module. lfric_const = LFRicConstants() api_config = Config.get().api_conf("dynamo0.3") lfric_kinds = list(api_config.precision_map.keys()) constants_mod = lfric_const.UTILITIES_MOD_MAP["constants"]["module"] Module = namedtuple('Module', ["name", "vars"]) modules = [Module(constants_mod, lfric_kinds)] # Generate LFRic module symbols from definitions for module_info in modules: module_name = module_info.name.lower() # Create the module (using a PSyIR ContainerSymbol) LFRicTypes._name_to_class[module_name] = \ ContainerSymbol(module_info.name) # Create the variables specified by the module (using # PSyIR DataSymbols) for module_var in module_info.vars: var_name = module_var.upper() interface = ImportInterface(LFRicTypes(module_name)) LFRicTypes._name_to_class[var_name] = \ DataSymbol(module_var, INTEGER_TYPE, interface=interface) # ------------------------------------------------------------------------ @staticmethod def _create_generic_scalars(): '''This function adds the generic data types and symbols for integer, real, and booleans to the global mapping. ''' GenericScalar = namedtuple('GenericScalar', ["name", "intrinsic", "precision"]) generic_scalar_datatypes = [ GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER, LFRicTypes("I_DEF")), GenericScalar("LFRicRealScalar", ScalarType.Intrinsic.REAL, LFRicTypes("R_DEF")), GenericScalar("LFRicLogicalScalar", ScalarType.Intrinsic.BOOLEAN, LFRicTypes("L_DEF"))] # Generate generic LFRic scalar datatypes and symbols from definitions for info in generic_scalar_datatypes: # Create the generic data type_name = f"{info.name}DataType" LFRicTypes._create_generic_scalar_data_type(type_name, info.intrinsic, info.precision) type_class = LFRicTypes(type_name) # Create the generic data symbol symbol_name = f"{info.name}DataSymbol" LFRicTypes._create_generic_scalar_data_symbol(symbol_name, type_class) # ------------------------------------------------------------------------ @staticmethod def _create_generic_scalar_data_type(name, intrinsic, default_precision): '''This function creates a generic scalar data type class and adds it to the global mapping. :param str name: name of the data type to create. :param intrinsic: the intrinsic type to use. :type intrinsic: \ :py:class:`pyclone.psyir.datatypes.ScalarType.Intrinsic` :param str default_precision: the default precision this class \ should have if the precision is not specified. ''' # This is the constructor for the dynamically created class: def __my_generic_scalar_type_init__(self, precision=None): if not precision: precision = self.default_precision ScalarType.__init__(self, self.intrinsic, precision) # --------------------------------------------------------------------- # Create the class, and set the above function as constructor. Note # that the values of 'intrinsic' and 'default_precision' must be stored # in the class: the `__my_generic_scalar_type_init__` function is # defined over and over again, each time with different values for # intrinsic/default_precision. So when these arguments would be # directly used in the constructor, they would remain at the values # used the last time this function was defined, but obviously each # class need the constructor using the right values. So these values # are stored in the class and then used in the constructor. LFRicTypes._name_to_class[name] = \ type(name, (ScalarType, ), {"__init__": __my_generic_scalar_type_init__, "intrinsic": intrinsic, "default_precision": default_precision}) # ------------------------------------------------------------------------ @staticmethod def _create_generic_scalar_data_symbol(name, type_class): '''This function creates a data symbol class with the specified name and data type, and adds it to the global mapping. :param str name: the name of the class to creates :param type_class: the data type for the symbol :type type_class: py:class:`pyclone.psyir.datatypes.ScalarType` ''' # This is the constructor for the dynamically created class: def __my_generic_scalar_symbol_init__(self, name, precision=None, **kwargs): DataSymbol.__init__(self, name, self.type_class(precision=precision), **kwargs) # --------------------------------------------------------------------- # Create the class, set the constructor and store the ScalarType as # an attribute, so it can be accessed in the constructor. LFRicTypes._name_to_class[name] = \ type(name, (DataSymbol, ), {"__init__": __my_generic_scalar_symbol_init__, "type_class": type_class}) # ------------------------------------------------------------------------ @staticmethod def _create_lfric_dimension(): '''This function adds the LFRicDimension class to the global mapping, and creates the two instances for scalar and vector dimension. ''' # The actual class: class LFRicDimension(Literal): '''An LFRic-specific scalar integer that captures a literal array dimension which can have a value between 1 and 3, inclusive. This is used for one of the dimensions in basis and differential basis functions and also for the vertical-boundary dofs mask. :param str value: the value of the scalar integer. :raises ValueError: if the supplied value is not '1', '2' or '3'. ''' # pylint: disable=undefined-variable def __init__(self, value): super().__init__(value, LFRicTypes("LFRicIntegerScalarDataType")()) if value not in ['1', '2', '3']: raise ValueError(f"An LFRic dimension object must be '1', " f"'2' or '3', but found '{value}'.") # -------------------------------------------------------------------- # Create the required entries in the dictionary LFRicTypes._name_to_class.update({ "LFRicDimension": LFRicDimension, "LFRIC_SCALAR_DIMENSION": LFRicDimension("1"), "LFRIC_VERTICAL_BOUNDARIES_DIMENSION": LFRicDimension("2"), "LFRIC_VECTOR_DIMENSION": LFRicDimension("3")}) # ------------------------------------------------------------------------ @staticmethod def _create_specific_scalars(): '''This function creates all required specific scalar, which are derived from the corresponding generic classes (e.g. LFRicIntegerScalarData) ''' # The Scalar namedtuple has 3 properties: the first # determines the names of the resultant datatype and datasymbol # classes, the second references the generic scalar type # classes declared above and the third specifies any # additional class properties that should be declared in the generated # datasymbol class. Scalar = namedtuple('Scalar', ["name", "generic_type_name", "properties"]) specific_scalar_datatypes = [ Scalar("CellPosition", "LFRicIntegerScalarData", []), Scalar("MeshHeight", "LFRicIntegerScalarData", []), Scalar("NumberOfCells", "LFRicIntegerScalarData", []), Scalar("NumberOfDofs", "LFRicIntegerScalarData", ["fs"]), Scalar("NumberOfUniqueDofs", "LFRicIntegerScalarData", ["fs"]), Scalar("NumberOfFaces", "LFRicIntegerScalarData", []), Scalar("NumberOfEdges", "LFRicIntegerScalarData", []), Scalar("NumberOfQrPointsInXy", "LFRicIntegerScalarData", []), Scalar("NumberOfQrPointsInZ", "LFRicIntegerScalarData", []), Scalar("NumberOfQrPointsInFaces", "LFRicIntegerScalarData", []), Scalar("NumberOfQrPointsInEdges", "LFRicIntegerScalarData", [])] for info in specific_scalar_datatypes: type_name = f"{info.name}DataType" LFRicTypes._name_to_class[type_name] = \ type(type_name, (LFRicTypes(f"{info.generic_type_name}Type"), ), {}) symbol_name = f"{info.name}DataSymbol" base_class = LFRicTypes(f"{info.generic_type_name}Symbol") LFRicTypes._create_scalar_data_type(symbol_name, base_class, info.properties) # ------------------------------------------------------------------------ @staticmethod def _create_scalar_data_type(class_name, base_class, parameters): '''This function creates a specific scalar data type with the given name, derived from the specified base class. :param str class_name: name of the class to create. :param base_class: the class on which to base the newly created class. :type base_class: :py:class:`psyclone.psyir.symbols.DataSymbol` :param parameters: additional required arguments of the constructor, \ which will be set as attributes in the base class. :type parameters: List[str] ''' # --------------------------------------------------------------------- # This is the __init__ function for the newly declared scalar data # types, which will be added as an attribute for the newly created # class. It parses the additional positional and keyword arguments # and sets them as attributes. def __my_scalar_init__(self, name, *args, **kwargs): # Set all the positional arguments as attributes: for i, arg in enumerate(args): setattr(self, self.parameters[i], arg) # Now handle the keyword arguments: any keyword arguments # that are declared as parameter will be set as attribute, # anything else will be passed to the constructor of the # base class. remaining_kwargs = {} for key, value in kwargs.items(): # It is one of the additional parameters, set it as # attribute: if key in self.parameters: setattr(self, key, value) else: # Otherwise add it as keyword parameter for the # base class constructor remaining_kwargs[key] = value self.base_class.__init__(self, name, **remaining_kwargs) # ---------------------------------------------------------------- # Now create the actual class. We need to keep a copy of the parameters # of this class as attributes, otherwise the __my_scalar_init__ # function (there is only one, which gets re-defined over and over) # will all be using the values for base_class and parameters that were # active the last time the function was defined. E.g. the # __my_scalar_init__ function set for CellPosition is the same as the # function set in NumberOfQrPointsInEdges (i.e. based on the same # values for base_class and parameters). LFRicTypes._name_to_class[class_name] = \ type(class_name, (base_class, ), {"__init__": __my_scalar_init__, "base_class": base_class, "parameters": parameters}) # ------------------------------------------------------------------------ @staticmethod def _create_fields(): '''This function creates the data symbol and types for LFRic fields. ''' # Note, field_datatypes are no different to array_datatypes and are # treated in the same way. They are only separated into a different # list because they are used to create vector field datatypes and # symbols. @dataclass(frozen=True) class Array: ''' Holds the properties of an LFRic array type, used when generating DataSymbol and DataSymbolType classes. :param name: the base name to use for the datatype and datasymbol. :param scalar_type: the name of the LFRic scalar type that this is an array of. :param dims: textual description of each of the dimensions. :param properties: names of additional class properties that should be declared in the generated datasymbol class. ''' name: str scalar_type: str dims: list # list[str] notation supported in Python 3.9+ properties: list # ditto field_datatypes = [ Array("RealField", "LFRicRealScalarDataType", ["number of unique dofs"], ["fs"]), Array("IntegerField", "LFRicIntegerScalarDataType", ["number of unique dofs"], ["fs"]), Array("LogicalField", "LFRicLogicalScalarDataType", ["number of unique dofs"], ["fs"])] # TBD: #918 the dimension datatypes and their ordering is captured in # field_datatypes and array_datatypes but is not stored in the # generated classes. # TBD: #926 attributes will be constrained to certain datatypes and # values. For example, a function space attribute should be a string # containing the name of a supported function space. These are not # currently checked. # TBD: #927 in some cases the values of attributes can be inferred, or # at least must be consistent. For example, a field datatype has an # associated function space attribute, its dimension symbol (if there # is one) must be a NumberOfUniqueDofsDataSymbol which also has a # function space attribute and the two function spaces must be # the same. This is not currently checked. array_datatypes = [ Array("Operator", "LFRicRealScalarDataType", ["number of dofs", "number of dofs", "number of cells"], ["fs_from", "fs_to"]), Array("DofMap", "LFRicIntegerScalarDataType", ["number of dofs"], ["fs"]), Array("BasisFunctionQrXyoz", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in xy", "number of qr points in z"], ["fs"]), Array("BasisFunctionQrFace", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in faces", "number of faces"], ["fs"]), Array("BasisFunctionQrEdge", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in edges", "number of edges"], ["fs"]), Array("DiffBasisFunctionQrXyoz", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in xy", "number of qr points in z"], ["fs"]), Array("DiffBasisFunctionQrFace", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in faces", "number of faces"], ["fs"]), Array("DiffBasisFunctionQrEdge", "LFRicRealScalarDataType", [LFRicTypes("LFRicDimension"), "number of dofs", "number of qr points in edges", "number of edges"], ["fs"]), Array("QrWeightsInXy", "LFRicRealScalarDataType", ["number of qr points in xy"], []), Array("QrWeightsInZ", "LFRicRealScalarDataType", ["number of qr points in z"], []), Array("QrWeightsInFaces", "LFRicRealScalarDataType", ["number of qr points in faces"], []), Array("QrWeightsInEdges", "LFRicRealScalarDataType", ["number of qr points in edges"], []), Array("VerticalBoundaryDofMask", "LFRicIntegerScalarDataType", ["number of dofs", LFRicTypes("LFRicDimension")], []) ] for array_type in array_datatypes + field_datatypes: name = f"{array_type.name}DataType" LFRicTypes._create_array_data_type_class( name, len(array_type.dims), LFRicTypes(array_type.scalar_type)) my_datatype_class = LFRicTypes(name) name = f"{array_type.name}DataSymbol" LFRicTypes._create_array_data_symbol_class(name, my_datatype_class, array_type.properties) # ------------------------------------------------------------------------ @staticmethod def _create_array_data_type_class(name, num_dims, scalar_type): '''This function create a data type class for the specified field. :param str name: name of the class to create. :param int num_dims: number of dimensions :param scalar_type: the scalar base type for this field. :type scalar_type: :py:class:`psyclone.psyir.datatypes.DataType` ''' # --------------------------------------------------------------------- # This is the constructor for the newly declared classes, which # verifies that the dimension argument has the expected number of # elements. def __my_type_init__(self, dims): ''' Constructor for the array data type class. :param list dims: the shape argument for the ArrayType constructor. ''' if len(dims) != self.num_dims: raise TypeError(f"'{type(self).__name__}' expected the number " f"of supplied dimensions to be {self.num_dims}" f" but found {len(dims)}.") ArrayType.__init__(self, self.scalar_class(), dims) # --------------------------------------------------------------------- # Create the class, and set the constructor. Store the scalar_type # and num_dims as attributes, so they are indeed different for all # the various classes created here. LFRicTypes._name_to_class[name] = \ type(name, (ArrayType, ), {"__init__": __my_type_init__, "scalar_class": scalar_type, "num_dims": num_dims}) # ------------------------------------------------------------------------ @staticmethod def _create_array_data_symbol_class(name, datatype_class, parameters): '''This function creates an array-data-symbol-class and adds it to the internal type dictionary. :param str name: the name of the class to be created. :param datatype_class: the corresponding data type class. :type datatype_class: :py:class:`psyclone.psyir.datatypes.DataType` :param parameters: the list of additional required properties \ to be passed to the constructor. :type parameters: List[str] ''' # --------------------------------------------------------------------- def __my_symbol_init__(self, name, dims, *args, **kwargs): '''This is the __init__ function for the newly declared array data type classes. It sets the required arguments automatically as attributes of the class. :param str name: the name of the data symbol to create. :param list dims: the shape argument for the ArrayType constructor. :param *args: other required positional parameters. :param **kwargs: other required keyword parameters. ''' # Set all the positional arguments as attributes: for i, arg in enumerate(args): setattr(self, self.parameters[i], arg) # Now handle the keyword arguments: any keyword arguments # that are declared as parameter will be set as attribute, # anything else will be passed to the constructor of the # base class. remaining_kwargs = {} for key, value in kwargs.items(): # It is one of the additional parameters, set it as # attribute: if key in self.parameters: setattr(self, key, value) else: # Otherwise add it as keyword parameter for the # base class constructor remaining_kwargs[key] = value DataSymbol.__init__(self, name, self.datatype_class(dims), **remaining_kwargs) # ---------------------------------------------------------------- # Now create the actual class. We need to keep a copy of the parameters # of this class as attributes, otherwise they would be shared among the # several instances of the __my_symbol_init__function: this affects the # required arguments (array_type.properties) and scalar class: LFRicTypes._name_to_class[name] = \ type(name, (DataSymbol, ), {"__init__": __my_symbol_init__, "datatype_class": datatype_class, "parameters": parameters})