Reference Guide  2.5.0
configuration.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2018-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: R. W. Ford, A. R. Porter and S. Siso, STFC Daresbury Lab
35 # Modified: J. Henrichs, Bureau of Meteorology
36 # I. Kavcic, Met Office
37 # N. Nobre, STFC Daresbury Lab
38 
39 '''
40 PSyclone configuration management module.
41 
42 Deals with reading the config file and storing default settings.
43 '''
44 
45 import abc
46 from configparser import (ConfigParser, MissingSectionHeaderError,
47  ParsingError)
48 from collections import namedtuple
49 import os
50 import re
51 import sys
52 import psyclone
53 
54 from psyclone.errors import PSycloneError, InternalError
55 
56 
57 # Name of the config file we search for
58 _FILE_NAME = "psyclone.cfg"
59 
60 # The different naming schemes supported when transforming kernels:
61 # multiple = Every transformed kernel is given a unique name. This permits
62 # multiple versions of the same kernel to be created.
63 # single = If any given kernel (within a single Application) is transformed
64 # more than once then the same transformation must always be
65 # applied and only one version of the transformed kernel is created.
66 VALID_KERNEL_NAMING_SCHEMES = ["multiple", "single"]
67 
68 
69 # pylint: disable=too-many-lines
71  '''
72  Class for all configuration-related errors.
73 
74  :param str value: error message.
75  :param config: the Config object associated with the error.
76  :type config: :py:class:`psyclone.configuration.Config`.
77  '''
78  def __init__(self, value, config=None):
79  PSycloneError.__init__(self, value)
80  self.valuevaluevalue = "PSyclone configuration error"
81  if config:
82  self.valuevaluevalue = f"{self.value} (file={config.filename})"
83  self.valuevaluevalue += f": {value}"
84 
85 
86 # =============================================================================
87 class Config:
88  # pylint: disable=too-many-instance-attributes, too-many-public-methods
89  '''
90  Handles all configuration management. It is implemented as a singleton
91  using a class _instance variable and a get() function.
92  '''
93  # Class variable to store the singleton instance
94  _instance = None
95 
96  # A consistency flag that is set to true the moment the proper config
97  # file is loaded. If an instance of this class should be created before
98  # the loading of the config file, an exception will be raised.
99  _HAS_CONFIG_BEEN_INITIALISED = False
100 
101  # List of supported API by PSyclone
102  _supported_api_list = ["dynamo0.3", "gocean1.0", "nemo"]
103 
104  # List of supported stub API by PSyclone
105  _supported_stub_api_list = ["dynamo0.3"]
106 
107  # The default API, i.e. the one to be used if neither a command line
108  # option is specified nor is the API in the config file used.
109  _default_api = "dynamo0.3"
110 
111  # The default scheme to use when (re)naming transformed kernels.
112  # By default we support multiple, different versions of any given
113  # kernel by ensuring that each transformed kernel is given a
114  # unique name (within the specified kernel-output directory).
115  # N.B. the default location to which to write transformed kernels is
116  # the current working directory. Since this may change between the
117  # importing of this module and the actual generation of code (e.g. as
118  # happens in the test suite), we do not store it here. Instead it
119  # is set in the Config.kernel_output_dir getter.
120  _default_kernel_naming = "multiple"
121 
122  # The default name to use when creating new names in the
123  # PSyIR symbol table.
124  _default_psyir_root_name = "psyir_tmp"
125 
126  # The list of valid PSyData class prefixes
127  _valid_psy_data_prefixes = []
128 
129  @staticmethod
130  def get(do_not_load_file=False):
131  '''Static function that if necessary creates and returns the singleton
132  config instance.
133 
134  :param bool do_not_load_file: If set it will not load the default \
135  config file. This is used when handling the command line so \
136  that the user can specify the file to load.
137  '''
138  if not Config._instance:
139  Config._instance = Config()
140  if not do_not_load_file:
141  Config._instance.load()
142  return Config._instance
143 
144  # -------------------------------------------------------------------------
145  @staticmethod
147  ''':returns: if the config class has loaded a (potential custom) \
148  config file.
149 
150  '''
151  return Config._HAS_CONFIG_BEEN_INITIALISED
152 
153  # -------------------------------------------------------------------------
154  @staticmethod
156  '''This function returns the absolute path to the config file included
157  in the PSyclone repository. It is used by the testing framework to make
158  sure all tests get the same config file (see tests/config_tests for the
159  only exception).
160  :return str: Absolute path to the config file included in the \
161  PSyclone repository.
162  '''
163  this_dir = os.path.dirname(os.path.abspath(__file__))
164  # The psyclone root dir is "../.." from this directory,
165  # so to remain portable use dirname twice:
166  psyclone_root_dir = os.path.dirname(os.path.dirname(this_dir))
167  return os.path.join(psyclone_root_dir, "config", "psyclone.cfg")
168 
169  # -------------------------------------------------------------------------
170  def __init__(self):
171  '''This is the basic constructor that only sets the supported APIs
172  and stub APIs, it does not load a config file. The Config instance
173  is a singleton, and as such will test that no instance already exists
174  and raise an exception otherwise.
175  :raises GenerationError: If a singleton instance of Config already \
176  exists.
177  '''
178 
179  if Config._instance is not None:
180  raise ConfigurationError("Only one instance of "
181  "Config can be created")
182 
183  # This dictionary stores the API-specific config instances
184  # for each API specified in a config file.
185  self._api_conf_api_conf = {}
186 
187  # This will store the ConfigParser instance for the specified
188  # config file.
189  self._config_config = None
190 
191  # The name (including path) of the config file read.
192  self._config_file_config_file = None
193 
194  # The API selected by the user - either on the command line,
195  # or in the config file (or the default if neither).
196  self._api_api = None
197 
198  # The default stub API to use.
199  self._default_stub_api_default_stub_api = None
200 
201  # True if distributed memory code should be created.
202  self._distributed_mem_distributed_mem = None
203 
204  # True if reproducible reductions should be used.
205  self._reproducible_reductions_reproducible_reductions = None
206 
207  # Padding size (number of array elements) to be used when
208  # reproducible reductions are created.
209  self._reprod_pad_size_reprod_pad_size = None
210 
211  # Where to write transformed kernels - set at runtime.
212  self._kernel_output_dir_kernel_output_dir = None
213 
214  # The naming scheme to use for transformed kernels.
215  self._kernel_naming_kernel_naming = None
216 
217  # The list of directories to search for Fortran include files.
218  self._include_paths_include_paths = []
219 
220  # The root name to use when creating internal PSyIR names.
221  self._psyir_root_name_psyir_root_name = None
222 
223  # Number of OpenCL devices per node
224  self._ocl_devices_per_node_ocl_devices_per_node = 1
225 
226  # By default, a PSyIR backend performs validation checks as it
227  # traverses the tree. Setting this option to False disables those
228  # checks which can be useful in the case of unimplemented features.
229  self._backend_checks_enabled_backend_checks_enabled = True
230 
231  # -------------------------------------------------------------------------
232  def load(self, config_file=None):
233  '''Loads a configuration file.
234 
235  :param str config_file: Override default configuration file to read.
236  :raises ConfigurationError: if there are errors or inconsistencies in \
237  the specified config file.
238  '''
239  # pylint: disable=too-many-branches, too-many-statements
240  if config_file:
241  # Caller has explicitly provided the full path to the config
242  # file to read
243  if not os.path.isfile(config_file):
244  raise ConfigurationError(
245  f"File {config_file} does not exist")
246  self._config_file_config_file = config_file[:]
247  else:
248  # Search for the config file in various default locations
249  self._config_file_config_file = Config.find_file()
250  # Add a getlist method to the ConfigParser instance using the
251  # converters argument. The lambda functions also handles the
252  # case of an empty specification ('xx = ''), returning an
253  # empty list instead of a list containing the empty string:
254  self._config_config = ConfigParser(
255  converters={'list': lambda x: [] if not x else
256  [i.strip() for i in x.split(',')]})
257  try:
258  self._config_config.read(self._config_file_config_file)
259  # Check for missing section headers and general parsing errors
260  # (e.g. incomplete or incorrect key-value mapping)
261  except (MissingSectionHeaderError, ParsingError) as err:
262  raise ConfigurationError(
263  f"ConfigParser failed to read the configuration file. Is it "
264  f"formatted correctly? (Error was: {err})",
265  config=self) from err
266 
267  # Check that the configuration file has a [DEFAULT] section. Even
268  # if there isn't one in the file, ConfigParser creates an (empty)
269  # dictionary entry for it.
270  if 'DEFAULT' not in self._config_config or \
271  not self._config_config['DEFAULT'].keys():
272  raise ConfigurationError(
273  "Configuration file has no [DEFAULT] section", config=self)
274 
275  # The call to the 'read' method above populates a dictionary.
276  # All of the entries in that dict are unicode strings so here
277  # we pull out the values we want and deal with any type
278  # conversion. We protect each of these with a try block so as
279  # to catch any conversion errors due to incorrect entries in
280  # the config file.
281  try:
282  self._distributed_mem_distributed_mem = self._config_config['DEFAULT'].getboolean(
283  'DISTRIBUTED_MEMORY')
284  except ValueError as err:
285  raise ConfigurationError(
286  f"Error while parsing DISTRIBUTED_MEMORY: {err}",
287  config=self) from err
288 
289  # API for PSyclone
290  if "DEFAULTAPI" in self._config_config["DEFAULT"]:
291  self._api_api = self._config_config['DEFAULT']['DEFAULTAPI']
292  else:
293  self._api_api = Config._default_api
294  # Test if we have exactly one section (besides DEFAULT).
295  # If so, make this section the API (otherwise stick with
296  # the default API)
297  if len(self._config_config) == 2:
298  for section in self._config_config:
299  self._api_api = section.lower()
300  if self._api_api != "default":
301  break
302 
303  # Sanity check
304  if self._api_api not in Config._supported_api_list:
305  raise ConfigurationError(
306  f"The API ({self._api}) is not in the list of supported "
307  f"APIs ({Config._supported_api_list}).", config=self)
308 
309  # Default API for stub-generator
310  if 'defaultstubapi' not in self._config_config['DEFAULT']:
311  # Use the default API if no default is specified for stub API
312  self._default_stub_api_default_stub_api = Config._default_api
313  else:
314  self._default_stub_api_default_stub_api = self._config_config['DEFAULT']['DEFAULTSTUBAPI']
315 
316  # Sanity check for defaultstubapi:
317  if self._default_stub_api_default_stub_api not in Config._supported_stub_api_list:
318  raise ConfigurationError(
319  f"The default stub API ({self._default_stub_api}) is not in "
320  f"the list of supported stub APIs ("
321  f"{Config._supported_stub_api_list}).", config=self)
322 
323  try:
324  self._reproducible_reductions_reproducible_reductions = self._config_config['DEFAULT'].getboolean(
325  'REPRODUCIBLE_REDUCTIONS')
326  except ValueError as err:
327  raise ConfigurationError(
328  f"Error while parsing REPRODUCIBLE_REDUCTIONS: {err}",
329  config=self) from err
330 
331  try:
332  self._reprod_pad_size_reprod_pad_size = self._config_config['DEFAULT'].getint(
333  'REPROD_PAD_SIZE')
334  except ValueError as err:
335  raise ConfigurationError(
336  f"error while parsing REPROD_PAD_SIZE: {err}",
337  config=self) from err
338 
339  if 'PSYIR_ROOT_NAME' not in self._config_config['DEFAULT']:
340  # Use the default name if no default is specified for the
341  # root name.
342  self._psyir_root_name_psyir_root_name = Config._default_psyir_root_name
343  else:
344  self._psyir_root_name_psyir_root_name = self._config_config['DEFAULT']['PSYIR_ROOT_NAME']
345 
346  # Read the valid PSyData class prefixes. If the keyword does
347  # not exist then return an empty list.
348  self._valid_psy_data_prefixes_valid_psy_data_prefixes_valid_psy_data_prefixes = \
349  self._config_config["DEFAULT"].getlist("VALID_PSY_DATA_PREFIXES", [])
350  try:
351  self._ocl_devices_per_node_ocl_devices_per_node = self._config_config['DEFAULT'].getint(
352  'OCL_DEVICES_PER_NODE')
353  except ValueError as err:
354  raise ConfigurationError(
355  f"error while parsing OCL_DEVICES_PER_NODE: {err}",
356  config=self) from err
357 
358  # Verify that the prefixes will result in valid Fortran names:
359  valid_var = re.compile(r"[A-Z][A-Z0-9_]*$", re.I)
360  for prefix in self._valid_psy_data_prefixes_valid_psy_data_prefixes_valid_psy_data_prefixes:
361  if not valid_var.match(prefix):
362  raise ConfigurationError(
363  f"Invalid PsyData-prefix '{prefix}' in config file. The "
364  f"prefix must be valid for use as the start of a Fortran "
365  f"variable name.", config=self)
366 
367  # Whether validation is performed in the PSyIR backends.
368  if 'BACKEND_CHECKS_ENABLED' in self._config_config['DEFAULT']:
369  try:
370  self._backend_checks_enabled_backend_checks_enabled = (
371  self._config_config['DEFAULT'].getboolean(
372  'BACKEND_CHECKS_ENABLED'))
373  except ValueError as err:
374  raise ConfigurationError(
375  f"Error while parsing BACKEND_CHECKS_ENABLED: {err}",
376  config=self) from err
377 
378  # Now we deal with the API-specific sections of the config file. We
379  # create a dictionary to hold the API-specific Config objects.
380  self._api_conf_api_conf = {}
381  for api in Config._supported_api_list:
382  if api in self._config_config:
383  if api == "dynamo0.3":
384  self._api_conf_api_conf[api] = LFRicConfig(self, self._config_config[api])
385  elif api == "gocean1.0":
386  self._api_conf_api_conf[api] = GOceanConfig(self, self._config_config[api])
387  else:
388  raise NotImplementedError(
389  f"Configuration file '{self._config_file}' contains a "
390  f"'{api}' section but no Config sub-class has "
391  f"been implemented for this API")
392 
393  # The scheme to use when re-naming transformed kernels.
394  # By default we ensure that each transformed kernel is given a
395  # unique name (within the specified kernel-output directory).
396  self._kernel_naming_kernel_naming = Config._default_kernel_naming
397 
398  ignore_modules = self._config_config['DEFAULT'].getlist("IGNORE_MODULES", [])
399  # Avoid circular import
400  # pylint: disable=import-outside-toplevel
401  from psyclone.parse import ModuleManager
402  mod_manager = ModuleManager.get()
403  for module_name in ignore_modules:
404  mod_manager.add_ignore_module(module_name)
405 
406  # Set the flag that the config file has been loaded now.
407  Config._HAS_CONFIG_BEEN_INITIALISED = True
408 
409  def api_conf(self, api=None):
410  '''
411  Getter for the object holding API-specific configuration options.
412 
413  :param str api: Optional, the API for which configuration details are
414  required. If none is specified, returns the config for the
415  default API.
416  :returns: object containing API-specific configuration
417  :rtype: One of :py:class:`psyclone.configuration.LFRicConfig`,
418  :py:class:`psyclone.configuration.GOceanConfig` or None.
419 
420  :raises ConfigurationError: if api is not in the list of supported \
421  APIs.
422  :raises ConfigurationError: if the config file did not contain a \
423  section for the requested API.
424  '''
425 
426  if not api:
427  return self._api_conf_api_conf[self._api_api]
428 
429  if api not in self.supported_apissupported_apis:
430  raise ConfigurationError(
431  f"API '{api}' is not in the list '{self.supported_apis}'' of "
432  f"supported APIs.")
433  if api not in self._api_conf_api_conf:
434  raise ConfigurationError(
435  f"Configuration file did not contain a section for the "
436  f"'{api}' API", config=self)
437  return self._api_conf_api_conf[api]
438 
439  @staticmethod
440  def find_file():
441  '''
442  Static method that searches various locations for a configuration
443  file. If the full path to an existing file has been provided in
444  the PSYCLONE_CONFIG environment variable then that is returned.
445  Otherwise, we search the following locations, in order:
446 
447  - ${PWD}/.psyclone/
448  - if inside-a-virtual-environment:
449  <base-dir-of-virtual-env>/share/psyclone/
450  - ${HOME}/.local/share/psyclone/
451  - <system-install-prefix>/share/psyclone/
452  - <psyclone-installation-base>/share/psyclone/
453 
454  :returns: the fully-qualified path to the configuration file
455  :rtype: str
456 
457  :raises ConfigurationError: if no config file is found
458 
459  '''
460  # Moving this to the top causes test failures
461  # pylint: disable=import-outside-toplevel
462  from psyclone.utils import within_virtual_env
463 
464  # If $PSYCLONE_CONFIG is set then we use that unless the
465  # file it points to does not exist
466  _psy_config = os.environ.get('PSYCLONE_CONFIG')
467  if _psy_config and os.path.isfile(_psy_config):
468  return _psy_config
469 
470  # Set up list of locations to search
471  share_dir = os.path.join(sys.prefix, "share", "psyclone")
472  pkg_share_dir = [
473  os.path.join(os.path.dirname(psyclone_path), "share", "psyclone")
474  for psyclone_path in psyclone.__path__]
475 
476  # 1. .psyclone/ in the CWD
477  _file_paths = [os.path.join(os.getcwd(), ".psyclone")]
478  if within_virtual_env():
479  # 2. <virtual-env-base>/share/psyclone/
480  _file_paths.append(share_dir)
481  # 3. ~/.local/share/psyclone/
482  _file_paths.append(os.path.join(os.path.expanduser("~"),
483  ".local", "share", "psyclone"))
484  if not within_virtual_env():
485  # 4. <python-installation-base>/share/psyclone/
486  _file_paths.append(share_dir)
487  # 5. <psyclone-installation-base>/share/psyclone/
488  _file_paths.extend(pkg_share_dir)
489 
490  for cfile in [os.path.join(cdir, _FILE_NAME) for cdir in _file_paths]:
491  if os.path.isfile(cfile):
492  return cfile
493 
494  # If we get to here then we have failed to find a config file
495  raise ConfigurationError(f"{_FILE_NAME} not found in any of "
496  f"{_file_paths}")
497 
498  @property
500  '''
501  Getter for whether or not distributed memory is enabled
502 
503  :returns: True if DM is enabled, False otherwise
504  :rtype: bool
505  '''
506  return self._distributed_mem_distributed_mem
507 
508  @distributed_memory.setter
509  def distributed_memory(self, dist_mem):
510  '''
511  Setter for whether or not distributed memory support is enabled
512  in this configuration.
513 
514  :param bool dist_mem: Whether or not dm is enabled
515  '''
516  if not isinstance(dist_mem, bool):
517  raise ConfigurationError(
518  f"distributed_memory must be a boolean but got "
519  f"{type(dist_mem)}")
520  self._distributed_mem_distributed_mem = dist_mem
521 
522  @property
523  def default_api(self):
524  '''
525  Getter for the default API used by PSyclone.
526 
527  :returns: default PSyclone API
528  :rtype: str
529  '''
530  return self._default_api_default_api
531 
532  @property
533  def api(self):
534  '''Getter for the API selected by the user.
535 
536  :returns: The name of the selected API.
537  :rtype: str
538  '''
539  return self._api_api
540 
541  @api.setter
542  def api(self, api):
543  '''Setter for the API selected by the user.
544 
545  :param str api: The name of the API to use.
546 
547  :raises ValueError if api is not a supported API.
548  '''
549  if api not in self._supported_api_list_supported_api_list:
550  raise ValueError(f"'{api}' is not a valid API, it must be one "
551  f"of {Config._supported_api_list}'.")
552  self._api_api = api
553 
554  @property
555  def supported_apis(self):
556  '''
557  Getter for the list of APIs supported by PSyclone.
558 
559  :returns: list of supported APIs
560  :rtype: list of str
561  '''
562  return Config._supported_api_list
563 
564  @property
565  def default_stub_api(self):
566  '''
567  Getter for the default API used by the stub generator.
568 
569  :returns: default API for the stub generator
570  :rtype: str
571  '''
572  return self._default_stub_api_default_stub_api
573 
574  @property
576  '''
577  :returns: whether the validity checks in the PSyIR backend should be
578  disabled.
579  :rtype: bool
580  '''
581  return self._backend_checks_enabled_backend_checks_enabled
582 
583  @backend_checks_enabled.setter
584  def backend_checks_enabled(self, value):
585  '''
586  Setter for whether or not the PSyIR backend is to perform validation
587  checks.
588 
589  :param bool value: whether or not to perform validation.
590 
591  :raises TypeError: if `value` is not a boolean.
592 
593  '''
594  if not isinstance(value, bool):
595  raise TypeError(f"Config.backend_checks_enabled must be a boolean "
596  f"but got '{type(value).__name__}'")
597  self._backend_checks_enabled_backend_checks_enabled = value
598 
599  @property
601  '''
602  Getter for the list of APIs supported by the stub generator.
603 
604  :returns: list of supported APIs.
605  :rtype: list of str
606  '''
607  return Config._supported_stub_api_list
608 
609  @property
611  '''
612  Getter for whether reproducible reductions are enabled.
613 
614  :returns: True if reproducible reductions are enabled, False otherwise.
615  :rtype: bool
616  '''
617  return self._reproducible_reductions_reproducible_reductions
618 
619  @property
620  def reprod_pad_size(self):
621  '''
622  Getter for the amount of padding to use for the array required
623  for reproducible OpenMP reductions
624 
625  :returns: padding size (no. of array elements)
626  :rtype: int
627  '''
628  return self._reprod_pad_size_reprod_pad_size
629 
630  @property
631  def psyir_root_name(self):
632  '''
633  Getter for the root name to use when creating PSyIR names.
634 
635  :returns: the PSyIR root name.
636  :rtype: str
637  '''
638  return self._psyir_root_name_psyir_root_name
639 
640  @property
641  def filename(self):
642  '''
643  Getter for the full path and name of the configuration file used
644  to initialise this configuration object.
645 
646  :returns: full path and name of configuration file
647  :rtype: str
648  '''
649  return self._config_file_config_file
650 
651  @property
652  def kernel_output_dir(self):
653  '''
654  :returns: the directory to which to write transformed kernels.
655  :rtype: str
656  '''
657  if not self._kernel_output_dir_kernel_output_dir:
658  # We use the CWD if no directory has been specified
659  self._kernel_output_dir_kernel_output_dir = os.getcwd()
660  return self._kernel_output_dir_kernel_output_dir
661 
662  @kernel_output_dir.setter
663  def kernel_output_dir(self, value):
664  '''
665  Setter for kernel output directory.
666  :param str value: directory to which to write transformed kernels.
667  '''
668  self._kernel_output_dir_kernel_output_dir = value
669 
670  @property
671  def kernel_naming(self):
672  '''
673  :returns: what naming scheme to use when writing transformed kernels \
674  to file.
675  :rtype: str
676  '''
677  return self._kernel_naming_kernel_naming
678 
679  @kernel_naming.setter
680  def kernel_naming(self, value):
681  '''
682  Setter for how to re-name kernels when writing transformed kernels
683  to file.
684 
685  :param str value: one of VALID_KERNEL_NAMING_SCHEMES.
686  :raises ValueError: if the supplied value is not a recognised \
687  kernel-renaming scheme.
688  '''
689  if value not in VALID_KERNEL_NAMING_SCHEMES:
690  raise ValueError(
691  f"kernel_naming must be one of '{VALID_KERNEL_NAMING_SCHEMES}'"
692  f" but got '{value}'")
693  self._kernel_naming_kernel_naming = value
694 
695  @property
696  def include_paths(self):
697  '''
698  :returns: the list of paths to search for Fortran include files.
699  :rtype: list of str.
700  '''
701  return self._include_paths_include_paths
702 
703  @include_paths.setter
704  def include_paths(self, path_list):
705  '''
706  Sets the list of paths to search for Fortran include files.
707 
708  :param path_list: list of directories to search.
709  :type path_list: list of str.
710 
711  :raises ValueError: if `path_list` is not a list-like object.
712  :raises ConfigurationError: if any of the paths in the list do \
713  not exist.
714  '''
715  self._include_paths_include_paths = []
716  try:
717  for path in path_list:
718  if not os.path.exists(path):
719  raise ConfigurationError(
720  f"Include path '{path}' does not exist")
721  self._include_paths_include_paths.append(path)
722  except (TypeError, ValueError) as err:
723  raise ValueError(f"include_paths must be a list but got: "
724  f"{type(path_list)}") from err
725 
726  @property
728  ''':returns: The list of all valid class prefixes.
729  :rtype: list of str'''
730  return self._valid_psy_data_prefixes_valid_psy_data_prefixes_valid_psy_data_prefixes
731 
732  @property
734  ''':returns: The number of OpenCL devices per node.
735  :rtype: int'''
736  return self._ocl_devices_per_node_ocl_devices_per_node
737 
738  def get_default_keys(self):
739  '''Returns all keys from the default section.
740  :returns list: List of all keys of the default section as strings.
741  '''
742  return self._config_config.defaults()
743 
744  def get_constants(self):
745  ''':returns: the constants instance of the current API.
746  :rtype: :py:class:`psyclone.domain.lfric.LFRicConstants` |
747  :py:class:`psyclone.domain.gocean.GOceanConstants`
748  '''
749  return self.api_confapi_conf().get_constants()
750 
751 
752 # =============================================================================
754  '''A base class for functions that each API-specific class must provide.
755  At the moment this is just the function 'access_mapping' that maps between
756  API-specific access-descriptor strings and the PSyclone internal
757  AccessType.
758  :param section: :py:class:`configparser.SectionProxy`
759  :raises ConfigurationError: if an access-mapping is provided that \
760  assigns an invalid value (i.e. not one of 'read', 'write', \
761  'readwrite'), 'inc' or 'sum') to a string.
762  '''
763 
764  def __init__(self, section):
765  # Set a default mapping, this way the test cases all work without
766  # having to specify those mappings.
767  self._access_mapping_access_mapping = {"read": "read", "write": "write",
768  "readwrite": "readwrite", "inc": "inc",
769  "sum": "sum"}
770  # Get the mapping if one exists and convert it into a
771  # dictionary. The input is in the format: key1:value1,
772  # key2=value2, ...
773  mapping_list = section.getlist("ACCESS_MAPPING")
774  if mapping_list is not None:
775  self._access_mapping_access_mapping = \
776  APISpecificConfig.create_dict_from_list(mapping_list)
777  # Now convert the string type ("read" etc) to AccessType
778  # TODO (issue #710): Add checks for duplicate or missing access
779  # key-value pairs
780  # Avoid circular import
781  # pylint: disable=import-outside-toplevel
782  from psyclone.core.access_type import AccessType
783 
784  for api_access_name, access_type in self._access_mapping_access_mapping.items():
785  try:
786  self._access_mapping_access_mapping[api_access_name] = \
787  AccessType.from_string(access_type)
788  except ValueError as err:
789  # Raised by from_string()
790  raise ConfigurationError(
791  f"Unknown access type '{access_type}' found for key "
792  f"'{api_access_name}'") from err
793 
794  # Now create the reverse lookup (for better error messages):
795  self._reverse_access_mapping_reverse_access_mapping = {v: k for k, v in
796  self._access_mapping_access_mapping.items()}
797 
798  @staticmethod
799  def create_dict_from_list(input_list):
800  '''Takes a list of strings each with the format: key:value and creates
801  a dictionary with the key,value pairs. Any leading or trailing
802  white space is removed.
803 
804  :param input_list: the input list.
805  :type input_list: list of str
806 
807  :returns: a dictionary with the key,value pairs from the input list.
808  :rtype: dict[str, Any]
809 
810  :raises ConfigurationError: if any entry in the input list
811  does not contain a ":".
812 
813  '''
814  return_dict = {}
815  for entry in input_list:
816  try:
817  key, value = entry.split(":", 1)
818  except ValueError as err:
819  # Raised when split does not return two elements:
820  raise ConfigurationError(
821  f"Invalid format for mapping: {entry.strip()}") from err
822  # Remove spaces and convert unicode to normal strings in Python2
823  return_dict[str(key.strip())] = str(value.strip())
824  return return_dict
825 
826  @staticmethod
828  '''Extracts the precision map values from the psyclone.cfg file
829  and converts them to a dictionary with integer values.
830 
831  :returns: The precision maps to be used by this API.
832  :rtype: dict[str, int]
833  '''
834  precisions_list = section.getlist("precision_map")
835  return_dict = {}
836  return_dict = APISpecificConfig.create_dict_from_list(precisions_list)
837 
838  for key, value in return_dict.items():
839  # isdecimal returns True if all the characters are decimals (0-9).
840  # isdigit returns True if all characters are digits (this excludes
841  # special characters such as the decimal point).
842  if value.isdecimal() and value.isdigit():
843  return_dict[key] = int(value)
844  else:
845  # Raised when key contains special characters or letters:
846  raise ConfigurationError(
847  f"Wrong type supplied to mapping: '{value}'"
848  f" is not a positive integer or contains special"
849  f" characters.")
850  return return_dict
851 
853  '''Returns the mapping of API-specific access strings (e.g.
854  gh_write) to the AccessType (e.g. AccessType.WRITE).
855  :returns: The access mapping to be used by this API.
856  :rtype: Dictionary of strings
857  '''
858  return self._access_mapping_access_mapping
859 
861  '''Returns the reverse mapping of a PSyclone internal access type
862  to the API specific string, e.g.: AccessType.READ to 'gh_read'.
863  This is used to provide the user with API specific error messages.
864  :returns: The mapping of access types to API-specific strings.
865  :rtype: Dictionary of strings
866  '''
867  return self._reverse_access_mapping_reverse_access_mapping
868 
870  '''Returns the sorted, API-specific names of all valid access
871  names.
872  :returns: Sorted list of API-specific valid strings.
873  :rtype: List of strings
874  '''
875  valid_names = list(self._access_mapping_access_mapping.keys())
876  valid_names.sort()
877  return valid_names
878 
879  @abc.abstractmethod
880  def get_constants(self):
881  ''':returns: an object containing all constants for the API.
882  :rtype: :py:class:`psyclone.domain.lfric.LFRicConstants` |
883  :py:class:`psyclone.domain.gocean.GOceanConstants`
884  '''
885 
886 
887 # =============================================================================
888 class LFRicConfig(APISpecificConfig):
889  '''
890  LFRic-specific (Dynamo 0.3) Config sub-class. Holds configuration options
891  specific to the LFRic (Dynamo 0.3) API.
892 
893  :param config: the 'parent' Config object.
894  :type config: :py:class:`psyclone.configuration.Config`
895  :param section: the entry for the '[dynamo0.3]' section of \
896  the configuration file, as produced by ConfigParser.
897  :type section: :py:class:`configparser.SectionProxy`
898 
899  :raises ConfigurationError: for a missing mandatory configuration option.
900  :raises ConfigurationError: for an invalid option for the redundant \
901  computation over annexed dofs.
902  :raises ConfigurationError: for an invalid run_time_checks flag.
903  :raises ConfigurationError: if argument datatypes in the 'default_kind' \
904  mapping do not match the supported datatypes.
905  :raises ConfigurationError: for an invalid argument kind.
906  :raises ConfigurationError: for an invalid value type of NUM_ANY_SPACE.
907  :raises ConfigurationError: if the supplied number of ANY_SPACE \
908  function spaces is less than or equal to 0.
909  :raises ConfigurationError: for an invalid value type of \
910  NUM_ANY_DISCONTINUOUS_SPACE.
911  :raises ConfigurationError: if the supplied number of \
912  ANY_DISCONTINUOUS_SPACE function \
913  spaces is less than or equal to 0.
914 
915  '''
916  # pylint: disable=too-few-public-methods, too-many-instance-attributes
917  def __init__(self, config, section):
918  super().__init__(section)
919  # Ref. to parent Config object
920  self._config_config = config
921  # Initialise redundant computation setting
922  self._compute_annexed_dofs_compute_annexed_dofs = None
923  # Initialise run_time_checks setting
924  self._run_time_checks_run_time_checks = None
925  # Initialise LFRic datatypes' default kinds (precisions) settings
926  self._supported_fortran_datatypes_supported_fortran_datatypes = []
927  self._default_kind_default_kind = {}
928  self._precision_map_precision_map = {}
929  # Number of ANY_SPACE and ANY_DISCONTINUOUS_SPACE function spaces
930  self._num_any_space_num_any_space = None
931  self._num_any_discontinuous_space_num_any_discontinuous_space = None
932 
933  # Define and check mandatory keys
934  self._mandatory_keys_mandatory_keys = ["access_mapping",
935  "compute_annexed_dofs",
936  "supported_fortran_datatypes",
937  "default_kind",
938  "precision_map",
939  "run_time_checks",
940  "num_any_space",
941  "num_any_discontinuous_space"]
942  mdkeys = set(self._mandatory_keys_mandatory_keys)
943  if not mdkeys.issubset(set(section.keys())):
944  raise ConfigurationError(
945  f"Missing mandatory configuration option in the "
946  f"'[{section.name}]' section of the configuration file "
947  f"'{config.filename}'. Valid options are: "
948  f"{self._mandatory_keys}.")
949 
950  # Parse setting for redundant computation over annexed dofs
951  try:
952  self._compute_annexed_dofs_compute_annexed_dofs = section.getboolean(
953  "compute_annexed_dofs")
954  except ValueError as err:
955  raise ConfigurationError(
956  f"Error while parsing COMPUTE_ANNEXED_DOFS in the "
957  f"'[{section.name}]' section of the configuration file "
958  f"'{config.filename}': {str(err)}.",
959  config=self._config_config) from err
960 
961  # Parse setting for run_time_checks flag
962  try:
963  self._run_time_checks_run_time_checks = section.getboolean(
964  "run_time_checks")
965  except ValueError as err:
966  raise ConfigurationError(
967  f"Error while parsing RUN_TIME_CHECKS in the "
968  f"'[{section.name}]' section of the configuration file "
969  f"'{config.filename}': {str(err)}.",
970  config=self._config_config) from err
971 
972  # Parse setting for the supported Fortran datatypes. No
973  # need to check whether the keyword is found as it is
974  # mandatory (and therefore already checked).
975  self._supported_fortran_datatypes_supported_fortran_datatypes = section.getlist(
976  "supported_fortran_datatypes")
977 
978  # Parse setting for default kinds (precisions). No need to
979  # check whether the keyword is found as it is mandatory
980  # (and therefore already checked).
981  kind_list = section.getlist("default_kind")
982  all_kinds = self.create_dict_from_listcreate_dict_from_list(kind_list)
983  # Set default kinds (precisions) from config file
984  # Check for valid datatypes (filter to remove empty values)
985  datatypes = set(filter(None, all_kinds.keys()))
986  if datatypes != set(self._supported_fortran_datatypes_supported_fortran_datatypes):
987  raise ConfigurationError(
988  f"Fortran datatypes in the 'default_kind' mapping in the "
989  f"'[{section.name}]' section of the configuration file "
990  f"'{config.filename}' do not match the supported Fortran "
991  f"datatypes {self._supported_fortran_datatypes}.")
992  # Check for valid kinds (filter to remove any empty values)
993  datakinds = set(filter(None, all_kinds.values()))
994  if len(datakinds) != len(set(self._supported_fortran_datatypes_supported_fortran_datatypes)):
995  raise ConfigurationError(
996  f"Supplied kind parameters {sorted(datakinds)} in the "
997  f"'[{section.name}]' section of the configuration file "
998  f"'{config.filename}' do not define the default kind for "
999  f"one or more supported datatypes "
1000  f"{self._supported_fortran_datatypes}.")
1001  self._default_kind_default_kind = all_kinds
1002 
1003  # Parse setting for default precision map values.
1004  all_precisions = self.get_precision_map_dictget_precision_map_dict(section)
1005  self._precision_map_precision_map = all_precisions
1006 
1007  # Parse setting for the number of ANY_SPACE function spaces
1008  # (check for an invalid value and numbers <= 0)
1009  try:
1010  self._num_any_space_num_any_space = section.getint("NUM_ANY_SPACE")
1011  except ValueError as err:
1012  raise ConfigurationError(
1013  f"Error while parsing NUM_ANY_SPACE in the '[{section.name}]' "
1014  f"section of the configuration file '{config.filename}': "
1015  f"{str(err)}.", config=self._config_config) from err
1016 
1017  if self._num_any_space_num_any_space <= 0:
1018  raise ConfigurationError(
1019  f"The supplied number of ANY_SPACE function spaces "
1020  f"in the '[{section.name}]' section of the configuration "
1021  f"file '{config.filename}' must be greater than 0 but found "
1022  f"{self._num_any_space}.")
1023 
1024  # Parse setting for the number of ANY_DISCONTINUOUS_SPACE
1025  # function spaces (checks for an invalid value and numbers <= 0)
1026  try:
1027  self._num_any_discontinuous_space_num_any_discontinuous_space = section.getint(
1028  "NUM_ANY_DISCONTINUOUS_SPACE")
1029  except ValueError as err:
1030  raise ConfigurationError(
1031  f"Error while parsing NUM_ANY_DISCONTINUOUS_SPACE in the "
1032  f"'[{section.name}]' section of the configuration file "
1033  f"'{config.filename}': {str(err)}.",
1034  config=self._config_config) from err
1035 
1036  if self._num_any_discontinuous_space_num_any_discontinuous_space <= 0:
1037  raise ConfigurationError(
1038  f"The supplied number of ANY_DISCONTINUOUS_SPACE function "
1039  f"spaces in the '[{section.name}]' section of the "
1040  f"configuration file '{config.filename}' must be greater than "
1041  f"0 but found {self._num_any_discontinuous_space}.")
1042 
1043  @property
1045  '''
1046  Getter for whether or not we perform redundant computation over
1047  annexed dofs.
1048 
1049  :returns: true if we are to do redundant computation.
1050  :rtype: bool
1051 
1052  '''
1053  return self._compute_annexed_dofs_compute_annexed_dofs
1054 
1055  @property
1056  def run_time_checks(self):
1057  '''
1058  Getter for whether or not we generate run-time checks in the code.
1059 
1060  :returns: true if we are generating run-time checks
1061  :rtype: bool
1062 
1063  '''
1064  return self._run_time_checks_run_time_checks
1065 
1066  @property
1068  '''
1069  Getter for the supported Fortran argument datatypes in LFRic.
1070 
1071  :returns: supported Fortran datatypes for LFRic arguments.
1072  :rtype: list of str
1073 
1074  '''
1075  return self._supported_fortran_datatypes_supported_fortran_datatypes
1076 
1077  @property
1078  def default_kind(self):
1079  '''
1080  Getter for default kind (precision) for real, integer and logical
1081  datatypes in LFRic.
1082 
1083  :returns: the default kinds for main datatypes in LFRic.
1084  :rtype: dict of str
1085 
1086  '''
1087  return self._default_kind_default_kind
1088 
1089  @property
1090  def precision_map(self):
1091  '''
1092  Getter for precision map values for supported fortran datatypes
1093  in LFRic. (Precision in bytes indexed by the name of the LFRic
1094  kind parameter).
1095 
1096  :returns: the precision map values for main datatypes in LFRic.
1097  :rtype: dict[str, int]
1098 
1099  '''
1100  return self._precision_map_precision_map
1101 
1102  @property
1103  def num_any_space(self):
1104  '''
1105  :returns: the number of ANY_SPACE function spaces in LFRic.
1106  :rtype: int
1107 
1108  '''
1109  return self._num_any_space_num_any_space
1110 
1111  @property
1113  '''
1114  :returns: the number of ANY_DISCONTINUOUS_SPACE function \
1115  spaces in LFRic.
1116  :rtype: int
1117 
1118  '''
1119  return self._num_any_discontinuous_space_num_any_discontinuous_space
1120 
1121  def get_constants(self):
1122  ''':returns: an object containing all constants for the API.
1123  :rtype: :py:class:`psyclone.domain.lfric.LFRicConstants`
1124  '''
1125  # Avoid circular import
1126  # pylint: disable=import-outside-toplevel
1127  from psyclone.domain.lfric import LFRicConstants
1128 
1129  return LFRicConstants()
1130 
1131 
1132 # =============================================================================
1134  '''Gocean1.0-specific Config sub-class. Holds configuration options
1135  specific to the GOcean 1.0 API.
1136 
1137  :param config: The 'parent' Config object.
1138  :type config: :py:class:`psyclone.configuration.Config`
1139  :param section: The entry for the gocean1.0 section of \
1140  the configuration file, as produced by ConfigParser.
1141  :type section: :py:class:`configparser.SectionProxy`
1142 
1143  '''
1144  # pylint: disable=too-few-public-methods, too-many-branches
1145  def __init__(self, config, section):
1146  # pylint: disable=too-many-locals
1147  super().__init__(section)
1148  # Setup the mapping for the grid properties. This dictionary stores
1149  # the name of the grid property as key (e.g. ``go_grid_dx_t``),
1150  # with the value being a named tuple with an entry for 'property'
1151  # and 'type'. The 'property' is a format string to dereference
1152  # a property, and 'type' is a string.
1153  # These values are taken from the psyclone config file.
1154  self._grid_properties_grid_properties = {}
1155  # Initialise debug_mode settings (default to False if it doesn't exist)
1156  self._debug_mode_debug_mode = False
1157  for key in section.keys():
1158  # Do not handle any keys from the DEFAULT section
1159  # since they are handled by Config(), not this class.
1160  if key in config.get_default_keys():
1161  continue
1162  if key == "iteration-spaces":
1163  # The value associated with the iteration-spaces key is a
1164  # set of lines, each line defining one new iteration space.
1165  # Each individual iteration space added is checked
1166  # in add_bounds for correctness.
1167  value_as_str = str(section[key])
1168  new_iteration_spaces = value_as_str.split("\n")
1169  # Avoid circular import
1170  # pylint: disable=import-outside-toplevel
1171  from psyclone.gocean1p0 import GOLoop
1172  for it_space in new_iteration_spaces:
1173  GOLoop.add_bounds(it_space)
1174  elif key == "access_mapping":
1175  # Handled in the base class APISpecificConfig
1176  pass
1177  elif key == "debug_mode":
1178  # Boolean that specifies if debug mode is enabled
1179  try:
1180  self._debug_mode_debug_mode = section.getboolean("debug_mode")
1181  except ValueError as err:
1182  raise ConfigurationError(
1183  f"error while parsing DEBUG_MODE in the [gocean1p0] "
1184  f"section of the config file: {err}") from err
1185  elif key == "grid-properties":
1186  # Grid properties have the format:
1187  # go_grid_area_u: {0}%%grid%%area_u: array: real,
1188  # First the name, then the Fortran code to access the property,
1189  # followed by the type ("array" or "scalar") and then the
1190  # intrinsic type ("integer" or "real")
1191  all_props = self.create_dict_from_listcreate_dict_from_list(section.getlist(key))
1192  for grid_property, property_str in all_props.items():
1193  try:
1194  fortran, variable_type, intrinsic_type = \
1195  property_str.split(":")
1196  except ValueError as err:
1197  # Raised when the string does not contain exactly
1198  # three values separated by ":"
1199  error = (f"Invalid property '{grid_property}' found "
1200  f"with value '{property_str}' in "
1201  f"'{config.filename}'. It must have exactly "
1202  f"three ':'-delimited separated values: the "
1203  f"property, whether it is a scalar or an "
1204  f"array, and the intrinsic type (real or "
1205  f"integer).")
1206  raise ConfigurationError(error) from err
1207  # Make sure to remove the spaces which the config
1208  # file might contain
1209  self._grid_properties_grid_properties[grid_property] = \
1210  GOceanConfig.make_property(fortran.strip(),
1211  variable_type.strip(),
1212  intrinsic_type.strip())
1213  # Check that the required values for xstop and ystop
1214  # are defined:
1215  for required in ["go_grid_xstop", "go_grid_ystop",
1216  "go_grid_data",
1217  "go_grid_internal_inner_start",
1218  "go_grid_internal_inner_stop",
1219  "go_grid_internal_outer_start",
1220  "go_grid_internal_outer_stop",
1221  "go_grid_whole_inner_start",
1222  "go_grid_whole_inner_stop",
1223  "go_grid_whole_outer_start",
1224  "go_grid_whole_outer_stop"]:
1225  if required not in self._grid_properties_grid_properties:
1226  error = (f"The config file {config.filename} does not "
1227  f"contain values for the following, mandatory"
1228  f" grid property: '{required}'.")
1229  raise ConfigurationError(error)
1230  else:
1231  raise ConfigurationError(f"Invalid key '{key}' found in "
1232  f"'{config.filename}'.")
1233 
1234  # ---------------------------------------------------------------------
1235  @staticmethod
1236  def make_property(dereference_format, type_name, intrinsic_type):
1237  '''Creates a property (based on namedtuple) for a given Fortran
1238  code to access a grid property, and the type.
1239 
1240  :param str dereference_format: The Fortran code to access a property \
1241  given a field name (which will be used to replace a {0} in the \
1242  string. E.g. "{0}%whole%xstop").
1243  :param str type_name: The type of the grid property, must be \
1244  'scalar' or 'array'.
1245  :param str intrinsic_type: The intrinsic type of the grid property, \
1246  must be 'integer' or 'real'.
1247 
1248  :returns: a namedtuple for a grid property given the Fortran
1249  statement to access it and the type.
1250 
1251  :raises InternalError: if type_name is not 'scalar' or 'array'
1252  :raises InternalError: if intrinsic_type is not 'integer' or 'real'
1253  '''
1254  if type_name not in ['array', 'scalar']:
1255  raise InternalError(f"Type must be 'array' or 'scalar' but is "
1256  f"'{type_name}'.")
1257 
1258  if intrinsic_type not in ['integer', 'real']:
1259  raise InternalError(f"Intrinsic type must be 'integer' or 'real' "
1260  f"but is '{intrinsic_type}'.")
1261 
1262  Property = namedtuple("Property", "fortran type intrinsic_type")
1263  return Property(dereference_format, type_name, intrinsic_type)
1264 
1265  # ---------------------------------------------------------------------
1266  @property
1267  def grid_properties(self):
1268  ''':returns: the dict containing the grid properties.
1269  :rtype: a dict with values of \
1270  namedtuple("Property","fortran type intrinsic_type") instances.
1271  '''
1272  return self._grid_properties_grid_properties
1273 
1274  # ---------------------------------------------------------------------
1275  @property
1276  def debug_mode(self):
1277  '''
1278  :returns: whether we are generating additional debug code.
1279  :rtype: bool
1280 
1281  '''
1282  return self._debug_mode_debug_mode
1283 
1284  # ---------------------------------------------------------------------
1285  def get_constants(self):
1286  ''':returns: an object containing all constants for GOcean.
1287  :rtype: :py:class:`psyclone.domain.gocean.GOceanConstants`
1288  '''
1289  # Avoid circular import
1290  # pylint: disable=import-outside-toplevel
1291  from psyclone.domain.gocean import GOceanConstants
1292  return GOceanConstants()
1293 
1294 
1295 # ---------- Documentation utils -------------------------------------------- #
1296 # The list of module members that we wish AutoAPI to generate
1297 # documentation for. (See https://psyclone-ref.readthedocs.io)
1298 __all__ = ["APISpecificConfig",
1299  "Config",
1300  "ConfigurationError",
1301  "LFRicConfig",
1302  "GOceanConfig"]
def api_conf(self, api=None)
def load(self, config_file=None)
def get(do_not_load_file=False)
def make_property(dereference_format, type_name, intrinsic_type)
def __init__(self, config, section)