Reference Guide  2.5.0
lfric_types.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2023-2024, Science and Technology Facilities Council.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice,
14 # this list of conditions and the following disclaimer in the documentation
15 # and/or other materials provided with the distribution.
16 #
17 # * Neither the name of the copyright holder nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 # POSSIBILITY OF SUCH DAMAGE.
33 # -----------------------------------------------------------------------------
34 # Authors: J. Henrichs, Bureau of Meteorology
35 # A. R. Porter, STFC Daresbury Lab
36 # O. Brunt, Met Office
37 
38 '''This module contains a singleton class that manages LFRic types. '''
39 
40 
41 from collections import namedtuple
42 from dataclasses import dataclass
43 
44 from psyclone.configuration import Config
45 from psyclone.domain.lfric.lfric_constants import LFRicConstants
46 from psyclone.errors import InternalError
47 from psyclone.psyir.nodes import Literal
48 from psyclone.psyir.symbols import (ArrayType, ContainerSymbol, DataSymbol,
49  ImportInterface, INTEGER_TYPE, ScalarType)
50 
51 
52 class LFRicTypes:
53  '''This class implements a singleton that manages LFRic types.
54  Using the 'call' interface, you can query the data type for
55  LFRic types, e.g.:
56 
57  >>> from psyclone.configuration import Config
58  >>> from psyclone.domain.lfric import LFRicTypes
59  >>> config = Config.get()
60  >>> num_dofs_class = LFRicTypes("NumberOfUniqueDofsDataSymbol")
61  >>> my_var = num_dofs_class("my_num_dofs")
62  >>> print(my_var.name)
63  my_num_dofs
64 
65  It uses the __new__ function to implement the access to the internal
66  dictionary. This is done to minimise the required code for getting a
67  value, e. g. compared with ``LFRicTypes.get()("something")``, or
68  ``LFRicType.get("something")``.
69 
70  '''
71 
72  # Class variable to store the singleton instance
73  _instance = None
74 
75  # Class variable to store the mapping of names to the various objects
76  # (classes and various instances) managed by this class.
77  _name_to_class = {}
78 
79  # ------------------------------------------------------------------------
80  def __new__(cls, name):
81  '''The class creation function __new__ is used to actually provide
82  the object the user is querying. It is well documented that __new__
83  can return a different instance. This function will first make sure
84  that the static internal dictionary is initialised (so it acts like
85  a singleton in that regard). Then it will return the value the user
86  asked for.
87 
88  :param str name: the name to query for.
89 
90  :returns: the corresponding object, which can be a class or an \
91  instance.
92  :rtype: object (various types)
93 
94  :raises InternalError: if there specified name is not a name for \
95  an object managed here.
96 
97  '''
98  if not LFRicTypes._name_to_class:
99  LFRicTypes.init()
100 
101  try:
102  return LFRicTypes._name_to_class[name]
103  except KeyError as err:
104  raise InternalError(f"Unknown LFRic type '{name}'. Valid values "
105  f"are {LFRicTypes._name_to_class.keys()}") \
106  from err
107 
108  # ------------------------------------------------------------------------
109  def __call__(self):
110  '''This function is only here to trick pylint into thinking that
111  the object returned from __new__ is callable, meaning that code like:
112  ``LFRicTypes("LFRicIntegerScalarDataType")()`` does not trigger
113  a pylint warning about not being callable.
114  '''
115 
116  # ------------------------------------------------------------------------
117  @staticmethod
118  def init():
119  '''This method constructs the required classes and instances, and
120  puts them into the dictionary.
121  '''
122 
123  # The global mapping of names to the corresponding classes or instances
124  LFRicTypes._name_to_class = {}
125 
126  LFRicTypes._create_precision_from_const_module()
127  LFRicTypes._create_generic_scalars()
128  LFRicTypes._create_lfric_dimension()
129  LFRicTypes._create_specific_scalars()
130  LFRicTypes._create_fields()
131 
132  # Generate LFRic vector-field-data symbols as subclasses of
133  # field-data symbols
134  const = LFRicConstants()
135  for intrinsic in const.VALID_FIELD_INTRINSIC_TYPES:
136  name = f"{intrinsic.title()}VectorFieldDataSymbol"
137  baseclass = LFRicTypes(f"{intrinsic.title()}FieldDataSymbol")
138  LFRicTypes._name_to_class[name] = type(name, (baseclass, ), {})
139 
140  # ------------------------------------------------------------------------
141  @staticmethod
142  def _create_precision_from_const_module():
143  '''This function implements all precisions defined in the
144  `dynamo0.3` (LFRic) domain. It adds "constants_mod" as
145  ContainerSymbol. The names are added to the global mapping.
146 
147  '''
148  # The first Module namedtuple argument specifies the name of the
149  # module and the second argument declares the name(s) of any symbols
150  # declared by the module.
151  lfric_const = LFRicConstants()
152  api_config = Config.get().api_conf("dynamo0.3")
153 
154  lfric_kinds = list(api_config.precision_map.keys())
155 
156  constants_mod = lfric_const.UTILITIES_MOD_MAP["constants"]["module"]
157  Module = namedtuple('Module', ["name", "vars"])
158  modules = [Module(constants_mod, lfric_kinds)]
159 
160  # Generate LFRic module symbols from definitions
161  for module_info in modules:
162  module_name = module_info.name.lower()
163  # Create the module (using a PSyIR ContainerSymbol)
164  LFRicTypes._name_to_class[module_name] = \
165  ContainerSymbol(module_info.name)
166  # Create the variables specified by the module (using
167  # PSyIR DataSymbols)
168  for module_var in module_info.vars:
169  var_name = module_var.upper()
170  interface = ImportInterface(LFRicTypes(module_name))
171  LFRicTypes._name_to_class[var_name] = \
172  DataSymbol(module_var, INTEGER_TYPE, interface=interface)
173 
174  # ------------------------------------------------------------------------
175  @staticmethod
176  def _create_generic_scalars():
177  '''This function adds the generic data types and symbols for
178  integer, real, and booleans to the global mapping.
179 
180  '''
181  GenericScalar = namedtuple('GenericScalar', ["name", "intrinsic",
182  "precision"])
183  generic_scalar_datatypes = [
184  GenericScalar("LFRicIntegerScalar", ScalarType.Intrinsic.INTEGER,
185  LFRicTypes("I_DEF")),
186  GenericScalar("LFRicRealScalar", ScalarType.Intrinsic.REAL,
187  LFRicTypes("R_DEF")),
188  GenericScalar("LFRicLogicalScalar", ScalarType.Intrinsic.BOOLEAN,
189  LFRicTypes("L_DEF"))]
190 
191  # Generate generic LFRic scalar datatypes and symbols from definitions
192  for info in generic_scalar_datatypes:
193 
194  # Create the generic data
195  type_name = f"{info.name}DataType"
196  LFRicTypes._create_generic_scalar_data_type(type_name,
197  info.intrinsic,
198  info.precision)
199  type_class = LFRicTypes(type_name)
200  # Create the generic data symbol
201  symbol_name = f"{info.name}DataSymbol"
202  LFRicTypes._create_generic_scalar_data_symbol(symbol_name,
203  type_class)
204 
205  # ------------------------------------------------------------------------
206  @staticmethod
207  def _create_generic_scalar_data_type(name, intrinsic, default_precision):
208  '''This function creates a generic scalar data type class and adds
209  it to the global mapping.
210 
211  :param str name: name of the data type to create.
212  :param intrinsic: the intrinsic type to use.
213  :type intrinsic: \
214  :py:class:`pyclone.psyir.datatypes.ScalarType.Intrinsic`
215  :param str default_precision: the default precision this class \
216  should have if the precision is not specified.
217 
218  '''
219  # This is the constructor for the dynamically created class:
220  def __my_generic_scalar_type_init__(self, precision=None):
221  if not precision:
222  precision = self.default_precision
223  ScalarType.__init__(self, self.intrinsic, precision)
224 
225  # ---------------------------------------------------------------------
226  # Create the class, and set the above function as constructor. Note
227  # that the values of 'intrinsic' and 'default_precision' must be stored
228  # in the class: the `__my_generic_scalar_type_init__` function is
229  # defined over and over again, each time with different values for
230  # intrinsic/default_precision. So when these arguments would be
231  # directly used in the constructor, they would remain at the values
232  # used the last time this function was defined, but obviously each
233  # class need the constructor using the right values. So these values
234  # are stored in the class and then used in the constructor.
235  LFRicTypes._name_to_class[name] = \
236  type(name, (ScalarType, ),
237  {"__init__": __my_generic_scalar_type_init__,
238  "intrinsic": intrinsic,
239  "default_precision": default_precision})
240 
241  # ------------------------------------------------------------------------
242  @staticmethod
243  def _create_generic_scalar_data_symbol(name, type_class):
244  '''This function creates a data symbol class with the specified name
245  and data type, and adds it to the global mapping.
246 
247  :param str name: the name of the class to creates
248  :param type_class: the data type for the symbol
249  :type type_class: py:class:`pyclone.psyir.datatypes.ScalarType`
250 
251  '''
252  # This is the constructor for the dynamically created class:
253  def __my_generic_scalar_symbol_init__(self, name, precision=None,
254  **kwargs):
255  DataSymbol.__init__(self, name,
256  self.type_class(precision=precision),
257  **kwargs)
258 
259  # ---------------------------------------------------------------------
260  # Create the class, set the constructor and store the ScalarType as
261  # an attribute, so it can be accessed in the constructor.
262  LFRicTypes._name_to_class[name] = \
263  type(name, (DataSymbol, ),
264  {"__init__": __my_generic_scalar_symbol_init__,
265  "type_class": type_class})
266 
267  # ------------------------------------------------------------------------
268  @staticmethod
269  def _create_lfric_dimension():
270  '''This function adds the LFRicDimension class to the global mapping,
271  and creates the two instances for scalar and vector dimension.
272 
273  '''
274  # The actual class:
275  class LFRicDimension(Literal):
276  '''An LFRic-specific scalar integer that captures a literal array
277  dimension which can have a value between 1 and 3, inclusive. This
278  is used for one of the dimensions in basis and differential basis
279  functions and also for the vertical-boundary dofs mask.
280 
281  :param str value: the value of the scalar integer.
282 
283  :raises ValueError: if the supplied value is not '1', '2' or '3'.
284 
285  '''
286  # pylint: disable=undefined-variable
287  def __init__(self, value):
288  super().__init__(value,
289  LFRicTypes("LFRicIntegerScalarDataType")())
290  if value not in ['1', '2', '3']:
291  raise ValueError(f"An LFRic dimension object must be '1', "
292  f"'2' or '3', but found '{value}'.")
293  # --------------------------------------------------------------------
294 
295  # Create the required entries in the dictionary
296  LFRicTypes._name_to_class.update({
297  "LFRicDimension": LFRicDimension,
298  "LFRIC_SCALAR_DIMENSION": LFRicDimension("1"),
299  "LFRIC_VERTICAL_BOUNDARIES_DIMENSION": LFRicDimension("2"),
300  "LFRIC_VECTOR_DIMENSION": LFRicDimension("3")})
301 
302  # ------------------------------------------------------------------------
303  @staticmethod
304  def _create_specific_scalars():
305  '''This function creates all required specific scalar, which are
306  derived from the corresponding generic classes (e.g.
307  LFRicIntegerScalarData)
308 
309  '''
310  # The Scalar namedtuple has 3 properties: the first
311  # determines the names of the resultant datatype and datasymbol
312  # classes, the second references the generic scalar type
313  # classes declared above and the third specifies any
314  # additional class properties that should be declared in the generated
315  # datasymbol class.
316  Scalar = namedtuple('Scalar', ["name", "generic_type_name",
317  "properties"])
318  specific_scalar_datatypes = [
319  Scalar("CellPosition", "LFRicIntegerScalarData", []),
320  Scalar("MeshHeight", "LFRicIntegerScalarData", []),
321  Scalar("NumberOfCells", "LFRicIntegerScalarData", []),
322  Scalar("NumberOfDofs", "LFRicIntegerScalarData", ["fs"]),
323  Scalar("NumberOfUniqueDofs", "LFRicIntegerScalarData", ["fs"]),
324  Scalar("NumberOfFaces", "LFRicIntegerScalarData", []),
325  Scalar("NumberOfEdges", "LFRicIntegerScalarData", []),
326  Scalar("NumberOfQrPointsInXy", "LFRicIntegerScalarData", []),
327  Scalar("NumberOfQrPointsInZ", "LFRicIntegerScalarData", []),
328  Scalar("NumberOfQrPointsInFaces", "LFRicIntegerScalarData", []),
329  Scalar("NumberOfQrPointsInEdges", "LFRicIntegerScalarData", [])]
330 
331  for info in specific_scalar_datatypes:
332  type_name = f"{info.name}DataType"
333  LFRicTypes._name_to_class[type_name] = \
334  type(type_name,
335  (LFRicTypes(f"{info.generic_type_name}Type"), ),
336  {})
337 
338  symbol_name = f"{info.name}DataSymbol"
339  base_class = LFRicTypes(f"{info.generic_type_name}Symbol")
340  LFRicTypes._create_scalar_data_type(symbol_name, base_class,
341  info.properties)
342 
343  # ------------------------------------------------------------------------
344  @staticmethod
345  def _create_scalar_data_type(class_name, base_class, parameters):
346  '''This function creates a specific scalar data type with the given
347  name, derived from the specified base class.
348 
349  :param str class_name: name of the class to create.
350  :param base_class: the class on which to base the newly created class.
351  :type base_class: :py:class:`psyclone.psyir.symbols.DataSymbol`
352  :param parameters: additional required arguments of the constructor, \
353  which will be set as attributes in the base class.
354  :type parameters: List[str]
355 
356  '''
357  # ---------------------------------------------------------------------
358  # This is the __init__ function for the newly declared scalar data
359  # types, which will be added as an attribute for the newly created
360  # class. It parses the additional positional and keyword arguments
361  # and sets them as attributes.
362 
363  def __my_scalar_init__(self, name, *args, **kwargs):
364  # Set all the positional arguments as attributes:
365  for i, arg in enumerate(args):
366  setattr(self, self.parameters[i], arg)
367  # Now handle the keyword arguments: any keyword arguments
368  # that are declared as parameter will be set as attribute,
369  # anything else will be passed to the constructor of the
370  # base class.
371  remaining_kwargs = {}
372  for key, value in kwargs.items():
373  # It is one of the additional parameters, set it as
374  # attribute:
375  if key in self.parameters:
376  setattr(self, key, value)
377  else:
378  # Otherwise add it as keyword parameter for the
379  # base class constructor
380  remaining_kwargs[key] = value
381  self.base_class.__init__(self, name, **remaining_kwargs)
382 
383  # ----------------------------------------------------------------
384 
385  # Now create the actual class. We need to keep a copy of the parameters
386  # of this class as attributes, otherwise the __my_scalar_init__
387  # function (there is only one, which gets re-defined over and over)
388  # will all be using the values for base_class and parameters that were
389  # active the last time the function was defined. E.g. the
390  # __my_scalar_init__ function set for CellPosition is the same as the
391  # function set in NumberOfQrPointsInEdges (i.e. based on the same
392  # values for base_class and parameters).
393  LFRicTypes._name_to_class[class_name] = \
394  type(class_name, (base_class, ),
395  {"__init__": __my_scalar_init__,
396  "base_class": base_class,
397  "parameters": parameters})
398 
399  # ------------------------------------------------------------------------
400  @staticmethod
401  def _create_fields():
402  '''This function creates the data symbol and types for LFRic fields.
403 
404  '''
405  # Note, field_datatypes are no different to array_datatypes and are
406  # treated in the same way. They are only separated into a different
407  # list because they are used to create vector field datatypes and
408  # symbols.
409 
410  @dataclass(frozen=True)
411  class Array:
412  '''
413  Holds the properties of an LFRic array type, used when generating
414  DataSymbol and DataSymbolType classes.
415 
416  :param name: the base name to use for the datatype and datasymbol.
417  :param scalar_type: the name of the LFRic scalar type that this is
418  an array of.
419  :param dims: textual description of each of the dimensions.
420  :param properties: names of additional class properties that should
421  be declared in the generated datasymbol class.
422  '''
423  name: str
424  scalar_type: str
425  dims: list # list[str] notation supported in Python 3.9+
426  properties: list # ditto
427 
428  field_datatypes = [
429  Array("RealField", "LFRicRealScalarDataType",
430  ["number of unique dofs"], ["fs"]),
431  Array("IntegerField", "LFRicIntegerScalarDataType",
432  ["number of unique dofs"], ["fs"]),
433  Array("LogicalField", "LFRicLogicalScalarDataType",
434  ["number of unique dofs"], ["fs"])]
435 
436  # TBD: #918 the dimension datatypes and their ordering is captured in
437  # field_datatypes and array_datatypes but is not stored in the
438  # generated classes.
439 
440  # TBD: #926 attributes will be constrained to certain datatypes and
441  # values. For example, a function space attribute should be a string
442  # containing the name of a supported function space. These are not
443  # currently checked.
444 
445  # TBD: #927 in some cases the values of attributes can be inferred, or
446  # at least must be consistent. For example, a field datatype has an
447  # associated function space attribute, its dimension symbol (if there
448  # is one) must be a NumberOfUniqueDofsDataSymbol which also has a
449  # function space attribute and the two function spaces must be
450  # the same. This is not currently checked.
451  array_datatypes = [
452  Array("Operator", "LFRicRealScalarDataType",
453  ["number of dofs", "number of dofs", "number of cells"],
454  ["fs_from", "fs_to"]),
455  Array("DofMap", "LFRicIntegerScalarDataType",
456  ["number of dofs"], ["fs"]),
457  Array("BasisFunctionQrXyoz", "LFRicRealScalarDataType",
458  [LFRicTypes("LFRicDimension"), "number of dofs",
459  "number of qr points in xy",
460  "number of qr points in z"], ["fs"]),
461  Array("BasisFunctionQrFace", "LFRicRealScalarDataType",
462  [LFRicTypes("LFRicDimension"), "number of dofs",
463  "number of qr points in faces",
464  "number of faces"], ["fs"]),
465  Array("BasisFunctionQrEdge", "LFRicRealScalarDataType",
466  [LFRicTypes("LFRicDimension"), "number of dofs",
467  "number of qr points in edges",
468  "number of edges"], ["fs"]),
469  Array("DiffBasisFunctionQrXyoz", "LFRicRealScalarDataType",
470  [LFRicTypes("LFRicDimension"), "number of dofs",
471  "number of qr points in xy",
472  "number of qr points in z"], ["fs"]),
473  Array("DiffBasisFunctionQrFace", "LFRicRealScalarDataType",
474  [LFRicTypes("LFRicDimension"), "number of dofs",
475  "number of qr points in faces",
476  "number of faces"], ["fs"]),
477  Array("DiffBasisFunctionQrEdge", "LFRicRealScalarDataType",
478  [LFRicTypes("LFRicDimension"), "number of dofs",
479  "number of qr points in edges", "number of edges"], ["fs"]),
480  Array("QrWeightsInXy", "LFRicRealScalarDataType",
481  ["number of qr points in xy"], []),
482  Array("QrWeightsInZ", "LFRicRealScalarDataType",
483  ["number of qr points in z"], []),
484  Array("QrWeightsInFaces", "LFRicRealScalarDataType",
485  ["number of qr points in faces"], []),
486  Array("QrWeightsInEdges", "LFRicRealScalarDataType",
487  ["number of qr points in edges"], []),
488  Array("VerticalBoundaryDofMask", "LFRicIntegerScalarDataType",
489  ["number of dofs", LFRicTypes("LFRicDimension")], [])
490  ]
491 
492  for array_type in array_datatypes + field_datatypes:
493  name = f"{array_type.name}DataType"
494  LFRicTypes._create_array_data_type_class(
495  name, len(array_type.dims), LFRicTypes(array_type.scalar_type))
496 
497  my_datatype_class = LFRicTypes(name)
498  name = f"{array_type.name}DataSymbol"
499  LFRicTypes._create_array_data_symbol_class(name, my_datatype_class,
500  array_type.properties)
501 
502  # ------------------------------------------------------------------------
503  @staticmethod
504  def _create_array_data_type_class(name, num_dims, scalar_type):
505  '''This function create a data type class for the specified field.
506 
507  :param str name: name of the class to create.
508  :param int num_dims: number of dimensions
509  :param scalar_type: the scalar base type for this field.
510  :type scalar_type: :py:class:`psyclone.psyir.datatypes.DataType`
511 
512  '''
513  # ---------------------------------------------------------------------
514  # This is the constructor for the newly declared classes, which
515  # verifies that the dimension argument has the expected number of
516  # elements.
517  def __my_type_init__(self, dims):
518  '''
519  Constructor for the array data type class.
520 
521  :param list dims: the shape argument for the ArrayType constructor.
522 
523  '''
524  if len(dims) != self.num_dims:
525  raise TypeError(f"'{type(self).__name__}' expected the number "
526  f"of supplied dimensions to be {self.num_dims}"
527  f" but found {len(dims)}.")
528  ArrayType.__init__(self, self.scalar_class(), dims)
529 
530  # ---------------------------------------------------------------------
531  # Create the class, and set the constructor. Store the scalar_type
532  # and num_dims as attributes, so they are indeed different for all
533  # the various classes created here.
534  LFRicTypes._name_to_class[name] = \
535  type(name, (ArrayType, ),
536  {"__init__": __my_type_init__,
537  "scalar_class": scalar_type,
538  "num_dims": num_dims})
539 
540  # ------------------------------------------------------------------------
541  @staticmethod
542  def _create_array_data_symbol_class(name, datatype_class, parameters):
543  '''This function creates an array-data-symbol-class and adds it to
544  the internal type dictionary.
545 
546  :param str name: the name of the class to be created.
547  :param datatype_class: the corresponding data type class.
548  :type datatype_class: :py:class:`psyclone.psyir.datatypes.DataType`
549 
550  :param parameters: the list of additional required properties \
551  to be passed to the constructor.
552  :type parameters: List[str]
553 
554  '''
555  # ---------------------------------------------------------------------
556  def __my_symbol_init__(self, name, dims, *args, **kwargs):
557  '''This is the __init__ function for the newly declared array data
558  type classes. It sets the required arguments automatically as
559  attributes of the class.
560 
561  :param str name: the name of the data symbol to create.
562  :param list dims: the shape argument for the ArrayType constructor.
563  :param *args: other required positional parameters.
564  :param **kwargs: other required keyword parameters.
565  '''
566  # Set all the positional arguments as attributes:
567  for i, arg in enumerate(args):
568  setattr(self, self.parameters[i], arg)
569  # Now handle the keyword arguments: any keyword arguments
570  # that are declared as parameter will be set as attribute,
571  # anything else will be passed to the constructor of the
572  # base class.
573  remaining_kwargs = {}
574  for key, value in kwargs.items():
575  # It is one of the additional parameters, set it as
576  # attribute:
577  if key in self.parameters:
578  setattr(self, key, value)
579  else:
580  # Otherwise add it as keyword parameter for the
581  # base class constructor
582  remaining_kwargs[key] = value
583  DataSymbol.__init__(self, name, self.datatype_class(dims),
584  **remaining_kwargs)
585  # ----------------------------------------------------------------
586 
587  # Now create the actual class. We need to keep a copy of the parameters
588  # of this class as attributes, otherwise they would be shared among the
589  # several instances of the __my_symbol_init__function: this affects the
590  # required arguments (array_type.properties) and scalar class:
591  LFRicTypes._name_to_class[name] = \
592  type(name, (DataSymbol, ),
593  {"__init__": __my_symbol_init__,
594  "datatype_class": datatype_class,
595  "parameters": parameters})