Reference Guide  2.5.0
gocean1p0.py
1 # pylint: disable=too-many-lines
2 # -----------------------------------------------------------------------------
3 # BSD 3-Clause License
4 #
5 # Copyright (c) 2017-2024, Science and Technology Facilities Council.
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are met:
10 #
11 # * Redistributions of source code must retain the above copyright notice, this
12 # list of conditions and the following disclaimer.
13 #
14 # * Redistributions in binary form must reproduce the above copyright notice,
15 # this list of conditions and the following disclaimer in the documentation
16 # and/or other materials provided with the distribution.
17 #
18 # * Neither the name of the copyright holder nor the names of its
19 # contributors may be used to endorse or promote products derived from
20 # this software without specific prior written permission.
21 #
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 # POSSIBILITY OF SUCH DAMAGE.
34 # -----------------------------------------------------------------------------
35 # Authors R. W. Ford, A. R. Porter and S. Siso, STFC Daresbury Lab
36 # Modified J. Henrichs, Bureau of Meteorology
37 # Modified I. Kavcic, Met Office
38 
39 
40 '''This module implements the PSyclone GOcean 1.0 API by specialising
41  the required base classes for both code generation (PSy, Invokes,
42  Invoke, InvokeSchedule, Loop, Kern, Arguments and KernelArgument)
43  and parsing (Descriptor and KernelType). It adds a
44  GOKernelGridArgument class to capture information on kernel arguments
45  that supply properties of the grid (and are generated in the PSy
46  layer).
47 
48 '''
49 
50 import re
51 
52 from fparser.two.Fortran2003 import NoMatchError, Nonlabel_Do_Stmt
53 from fparser.two.parser import ParserFactory
54 
55 from psyclone.configuration import Config, ConfigurationError
56 from psyclone.core import Signature
57 from psyclone.domain.common.psylayer import PSyLoop
58 from psyclone.domain.gocean import GOceanConstants, GOSymbolTable
59 from psyclone.errors import GenerationError, InternalError
60 import psyclone.expression as expr
61 from psyclone.f2pygen import (
62  DeclGen, UseGen, ModuleGen, SubroutineGen, TypeDeclGen, PSyIRGen)
63 from psyclone.parse.algorithm import Arg
64 from psyclone.parse.kernel import Descriptor, KernelType
65 from psyclone.parse.utils import ParseError
66 from psyclone.psyGen import (
67  PSy, Invokes, Invoke, InvokeSchedule, CodedKern, Arguments, Argument,
68  KernelArgument, args_filter, AccessType, HaloExchange)
69 from psyclone.psyir.frontend.fparser2 import Fparser2Reader
70 from psyclone.psyir.frontend.fortran import FortranReader
71 from psyclone.psyir.nodes import (
72  Literal, Schedule, KernelSchedule, StructureReference, IntrinsicCall,
73  Reference, Call, Assignment, ACCEnterDataDirective, ACCParallelDirective,
74  ACCKernelsDirective, Container, ACCUpdateDirective, Routine,
75  BinaryOperation)
76 from psyclone.psyir.symbols import (
77  ScalarType, INTEGER_TYPE, DataSymbol, RoutineSymbol, ContainerSymbol,
78  UnresolvedType, DataTypeSymbol, UnresolvedInterface, BOOLEAN_TYPE,
79  REAL_TYPE)
80 from psyclone.psyir.tools import DependencyTools
81 
82 
83 class GOPSy(PSy):
84  '''
85  The GOcean 1.0 specific PSy class. This creates a GOcean specific
86  invokes object (which controls all the required invocation calls).
87  Also overrides the PSy gen method so that we generate GOcean-
88  specific PSy module code.
89 
90  :param invoke_info: An object containing the required invocation \
91  information for code optimisation and generation.
92  :type invoke_info: :py:class:`psyclone.parse.FileInfo`
93 
94  '''
95  def __init__(self, invoke_info):
96  PSy.__init__(self, invoke_info)
97 
98  # Add GOcean infrastructure-specific libraries
99  field_sym = ContainerSymbol("field_mod")
100  field_sym.wildcard_import = True
101  self.containercontainer.symbol_table.add(field_sym)
102  kind_params_sym = ContainerSymbol("kind_params_mod")
103  kind_params_sym.wildcard_import = True
104  self.containercontainer.symbol_table.add(kind_params_sym)
105 
106  # Create invokes
107  self._invokes_invokes_invokes = GOInvokes(invoke_info.calls, self)
108 
109  @property
110  def gen(self):
111  '''
112  Generate PSy code for the GOcean api v.1.0.
113 
114  :rtype: ast
115 
116  '''
117  # create an empty PSy layer module
118  psy_module = ModuleGen(self.namename)
119  # include the kind_params module
120  psy_module.add(UseGen(psy_module, name="kind_params_mod"))
121  # include the field_mod module
122  psy_module.add(UseGen(psy_module, name="field_mod"))
123  self.invokesinvokes.gen_code(psy_module)
124  return psy_module.root
125 
126 
128  '''
129  The GOcean specific invokes class. This passes the GOcean specific
130  invoke class to the base class so it creates the one we require.
131 
132  :param alg_calls: The Invoke calls discovered in the Algorithm layer.
133  :type alg_calls: OrderedDict of :py:class:`psyclone.parse.InvokeCall` \
134  objects.
135  :param psy: the PSy object containing this GOInvokes object.
136  :type psy: :py:class:`psyclone.gocean1p0.GOPSy`
137 
138  '''
139  def __init__(self, alg_calls, psy):
140  self._0_to_n_0_to_n = GOInvoke(None, None, None) # for pyreverse
141  Invokes.__init__(self, alg_calls, GOInvoke, psy)
142 
143  index_offsets = []
144  # Loop over all of the kernels in all of the invoke() calls
145  # and check that they work on compatible grid-index offsets.
146  # Strictly speaking this check should be done in the parsing
147  # code since it is a check on the correctness of the meta-data.
148  # However, that would require a fundamental change to the parsing
149  # code since it requires information on all of the invokes and
150  # kernels in an application. Therefore it is much simpler to
151  # do it here where we have easy access to that information.
152  for invoke in self.invoke_listinvoke_list:
153  for kern_call in invoke.schedule.coded_kernels():
154  # We only care if the index offset is not offset_any (since
155  # that is compatible with any other offset)
156  if kern_call.index_offset != "go_offset_any":
157  # Loop over the offsets we've seen so far
158  for offset in index_offsets:
159  if offset != kern_call.index_offset:
160  raise GenerationError(
161  f"Meta-data error in kernel {kern_call.name}: "
162  f"INDEX_OFFSET of '{kern_call.index_offset}' "
163  f"does not match that ({offset}) of other "
164  f"kernels. This is not supported.")
165  # Append the index-offset of this kernel to the list of
166  # those seen so far
167  index_offsets.append(kern_call.index_offset)
168 
169  def gen_code(self, parent):
170  '''
171  GOcean redefines the Invokes.gen_code() to start using the PSyIR
172  backend when possible. In cases where the backend can not be used yet
173  (e.g. OpenCL and PSyDataNodes) the parent class will be called. This
174  is a temporary workaround to avoid modifying the generator file while
175  other APIs still use the f2pygen module for code generation.
176  Once the PSyIR backend has generated an output, this is added into a
177  f2pygen PSyIRGen block in the f2pygen AST for each Invoke in the
178  PSy layer.
179 
180  :param parent: the parent node in the f2pygen AST to which to add \
181  content.
182  :type parent: `psyclone.f2pygen.ModuleGen`
183  '''
184  if self.invoke_listinvoke_list:
185  # We just need one invoke as they all have a common root.
186  invoke = self.invoke_listinvoke_list[0]
187 
188  # Lower the GOcean PSyIR to language level so it can be visited
189  # by the backends
190  invoke.schedule.root.lower_to_language_level()
191  # Then insert it into a f2pygen AST as a PSyIRGen node.
192  # Note that other routines besides the Invoke could have been
193  # inserted during the lowering (e.g. module-inlined kernels),
194  # so have to iterate over all current children of root.
195  for child in invoke.schedule.root.children:
196  parent.add(PSyIRGen(parent, child))
197 
198 
200  '''
201  The GOcean specific invoke class. This passes the GOcean specific
202  schedule class to the base class so it creates the one we require.
203  A set of GOcean infrastructure reserved names are also passed to
204  ensure that there are no name clashes. Also overrides the gen_code
205  method so that we generate GOcean specific invocation code and
206  provides three methods which separate arguments that are arrays from
207  arguments that are {integer, real} scalars.
208 
209  :param alg_invocation: Node in the AST describing the invoke call.
210  :type alg_invocation: :py:class:`psyclone.parse.InvokeCall`
211  :param int idx: The position of the invoke in the list of invokes \
212  contained in the Algorithm.
213  :param invokes: the Invokes object containing this GOInvoke \
214  object.
215  :type invokes: :py:class:`psyclone.gocean1p0.GOInvokes`
216 
217  '''
218  def __init__(self, alg_invocation, idx, invokes):
219  self._schedule_schedule_schedule = GOInvokeSchedule('name', None) # for pyreverse
220  Invoke.__init__(self, alg_invocation, idx, GOInvokeSchedule, invokes)
221 
222  if Config.get().distributed_memory:
223  # Insert halo exchange calls
224  for loop in self.schedulescheduleschedule.loops():
225  loop.create_halo_exchanges()
226 
227  @property
229  ''' find unique arguments that are arrays (defined as those that are
230  field objects as opposed to scalars or properties of the grid). '''
231  result = []
232  for call in self._schedule_schedule_schedule.kernels():
233  for arg in call.arguments.args:
234  if arg.argument_type == 'field' and arg.name not in result:
235  result.append(arg.name)
236  return result
237 
238  @property
240  '''
241  :returns: the unique arguments that are scalars of type integer \
242  (defined as those that are i_scalar 'space').
243  :rtype: list of str.
244 
245  '''
246  result = []
247  for call in self._schedule_schedule_schedule.kernels():
248  for arg in args_filter(call.arguments.args, arg_types=["scalar"],
249  include_literals=False):
250  if arg.space.lower() == "go_i_scalar" and \
251  arg.name not in result:
252  result.append(arg.name)
253  return result
254 
255  def gen_code(self, parent):
256  # pylint: disable=too-many-locals
257  '''
258  Generates GOcean specific invocation code (the subroutine called
259  by the associated invoke call in the algorithm layer). This
260  consists of the PSy invocation subroutine and the declaration of
261  its arguments.
262 
263  :param parent: the node in the generated AST to which to add content.
264  :type parent: :py:class:`psyclone.f2pygen.ModuleGen`
265 
266  '''
267  # TODO 1010: GOcean doesn't use this method anymore and it can be
268  # deleted, but some tests still call it directly.
269 
270  # Create the subroutine
271  invoke_sub = SubroutineGen(parent, name=self.namename,
272  args=self.psy_unique_var_namespsy_unique_var_names)
273  parent.add(invoke_sub)
274 
275  # Generate the code body of this subroutine
276  self.schedulescheduleschedule.gen_code(invoke_sub)
277 
278  # Add the subroutine argument declarations for fields
279  if self.unique_args_arraysunique_args_arrays:
280  my_decl_arrays = TypeDeclGen(invoke_sub, datatype="r2d_field",
281  intent="inout",
282  entity_decls=self.unique_args_arraysunique_args_arrays)
283  invoke_sub.add(my_decl_arrays)
284 
285  # Add the subroutine argument declarations for integer and real scalars
286  i_args = []
287  for argument in self.schedulescheduleschedule.symbol_table.argument_datasymbols:
288  if argument.name in self.unique_args_iscalarsunique_args_iscalars:
289  i_args.append(argument.name)
290 
291  if i_args:
292  my_decl_iscalars = DeclGen(invoke_sub, datatype="INTEGER",
293  intent="inout",
294  entity_decls=i_args)
295  invoke_sub.add(my_decl_iscalars)
296 
297  # Add remaining local scalar symbols using the symbol table
298  for symbol in self.schedulescheduleschedule.symbol_table.automatic_datasymbols:
299  if isinstance(symbol.datatype, ScalarType):
300  invoke_sub.add(DeclGen(
301  invoke_sub,
302  datatype=symbol.datatype.intrinsic.name,
303  entity_decls=[symbol.name]))
304 
305 
307  ''' The GOcean specific InvokeSchedule sub-class. We call the base class
308  constructor and pass it factories to create GO-specific calls to both
309  user-supplied kernels and built-ins.
310 
311  :param str name: name of the Invoke.
312  :param alg_calls: list of KernelCalls parsed from the algorithm layer.
313  :type alg_calls: list of :py:class:`psyclone.parse.algorithm.KernelCall`
314  :param reserved_names: optional list of names that are not allowed in the \
315  new InvokeSchedule SymbolTable.
316  :type reserved_names: list of str
317  :param parent: the parent of this node in the PSyIR.
318  :type parent: :py:class:`psyclone.psyir.nodes.Node`
319 
320  '''
321  # Textual description of the node.
322  _text_name = "GOInvokeSchedule"
323 
324  def __init__(self, name, alg_calls, reserved_names=None, parent=None):
325  InvokeSchedule.__init__(self, name, GOKernCallFactory,
326  GOBuiltInCallFactory,
327  alg_calls, reserved_names, parent=parent)
328 
329 
330 # pylint: disable=too-many-instance-attributes
332  ''' The GOcean specific PSyLoop class. This passes the GOcean specific
333  single loop information to the base class so it creates the one we
334  require. Adds a GOcean specific setBounds method which tells the loop
335  what to iterate over. Need to harmonise with the topology_name method
336  in the Dynamo api.
337 
338  :param parent: optional parent node (default None).
339  :type parent: :py:class:`psyclone.psyir.nodes.Node`
340  :param str loop_type: loop type - must be 'inner' or 'outer'.
341  :param str field_name: name of the field this loop iterates on.
342  :param str field_space: space of the field this loop iterates on.
343  :param str iteration_space: iteration space of the loop.
344 
345  :raises GenerationError: if the loop is not inserted inside a \
346  GOInvokeSchedule region.
347 
348  '''
349  _bounds_lookup = {}
350 
351  def __init__(self, parent, loop_type="", field_name="", field_space="",
352  iteration_space="", index_offset=""):
353  # pylint: disable=too-many-arguments
354  const = GOceanConstants()
355 
356  super().__init__(parent=parent,
357  valid_loop_types=const.VALID_LOOP_TYPES)
358 
359  # The following attributes are validated in the respective setters
360  self.loop_typeloop_typeloop_typeloop_typeloop_type = loop_type
361  self.field_namefield_namefield_namefield_namefield_name = field_name
364  self.index_offsetindex_offset = index_offset
365 
366  # Check that the GOLoop is inside the GOcean PSy-layer
367  if not self.ancestor(GOInvokeSchedule):
368  raise GenerationError(
369  "GOLoops must always be constructed with a parent which is"
370  " inside (directly or indirectly) of a GOInvokeSchedule")
371 
372  # We set the loop variable name in the constructor so that it is
373  # available when we're determining which vars should be OpenMP
374  # PRIVATE (which is done *before* code generation is performed)
375  if self.loop_typeloop_typeloop_typeloop_typeloop_type == "inner":
376  tag = "contiguous_kidx"
377  suggested_name = "i"
378  elif self.loop_typeloop_typeloop_typeloop_typeloop_type == "outer":
379  tag = "noncontiguous_kidx"
380  suggested_name = "j"
381  else:
382  raise InternalError(f"While the loop type '{self._loop_type}' is "
383  f"valid, it is not yet supported.")
384 
385  # In the GOcean API the loop iteration variables are declared in the
386  # Invoke routine scope in order to share them between all GOLoops.
387  # This is important because some transformations/scripts work with
388  # this assumption when moving or fusing loops.
389  symtab = self.ancestor(InvokeSchedule).symbol_table
390  try:
391  self.variablevariable = symtab.lookup_with_tag(tag)
392  except KeyError:
393  self.variablevariable = symtab.new_symbol(
394  suggested_name, tag, symbol_type=DataSymbol,
395  datatype=INTEGER_TYPE)
396 
397  # Initialise bounds lookup map if it is not already
398  if not GOLoop._bounds_lookup:
399  GOLoop.setup_bounds()
400 
401  @staticmethod
402  def create(parent, loop_type, field_name="", field_space="",
403  iteration_space="", index_offset=""):
404  # pylint: disable=too-many-arguments,arguments-renamed
405  '''
406  Create a new instance of a GOLoop with the expected children to
407  represent the bounds given by the loop properties.
408 
409  :param parent: parent node of this GOLoop.
410  :type parent: :py:class:`psyclone.psyir.nodes.Node`
411  :param str loop_type: loop type - must be 'inner' or 'outer'.
412  :param str field_name: name of the field this loop iterates on.
413  :param str field_space: space of the field this loop iterates on.
414  :param str iteration_space: iteration space of the loop.
415  :param str index_offset: the grid index offset used by the kernel(s) \
416  within this loop.
417 
418  :returns: a new GOLoop node (with appropriate child nodes).
419  :rtype: :py:class:`psyclone.gocean1p0.GOLoop`
420  '''
421 
422  # Create loop node
423  node = GOLoop(parent, loop_type, field_name, field_space,
424  iteration_space, index_offset)
425 
426  # Add start, stop, step and body and Loop children
427  node.addchild(node.lower_bound())
428  node.addchild(node.upper_bound())
429  node.addchild(Literal("1", INTEGER_TYPE))
430  node.addchild(Schedule())
431 
432  return node
433 
434  @property
435  def field_space(self):
436  '''
437  :returns: the loop's field space (e.g. CU, CV...).
438  :rtype: str
439  '''
440  return self._field_space_field_space_field_space
441 
442  @field_space.setter
443  def field_space(self, my_field_space):
444  ''' Sets new value for the field_space and updates the Loop bounds,
445  if these exist, to match the given field_space.
446 
447  :param str my_field_space: new field_space value.
448 
449  :raises TypeError: if the provided field_space is not a string.
450  :raises ValueError: if the provided field_space is not a valid GOcean \
451  field_space.
452 
453  '''
454  # TODO 1393: This could call the super setter if the validations are
455  # generic
456  if not isinstance(my_field_space, str):
457  raise TypeError(
458  f"Field space must be a 'str' but found "
459  f"'{type(my_field_space).__name__}' instead.")
460  valid_fs = GOceanConstants().VALID_FIELD_GRID_TYPES + ['']
461  if my_field_space not in valid_fs:
462  raise ValueError(
463  f"Invalid string '{my_field_space}' provided for a GOcean "
464  f"field_space. The valid values are {valid_fs}")
465 
466  self._field_space_field_space_field_space = my_field_space
467  if len(self.children) > 1:
468  self.start_expr.replace_with(self.lower_boundlower_bound())
469  if len(self.children) > 2:
470  self.stop_expr.replace_with(self.upper_boundupper_bound())
471 
472  @property
473  def iteration_space(self):
474  '''
475  :returns: the loop's iteration space (e.g. 'go_internal_pts', \
476  'go_all_pts', ...).
477  :rtype: str
478  '''
479  return self._iteration_space_iteration_space_iteration_space
480 
481  @iteration_space.setter
482  def iteration_space(self, it_space):
483  ''' Sets new value for the iteration_space and updates the Loop bounds,
484  if these exist, to match the given iteration_space.
485 
486  :param str it_space: new iteration_space value.
487 
488  :raises TypeError: if the provided it_space is not a string.
489 
490  '''
491  if not isinstance(it_space, str):
492  raise TypeError(
493  f"Iteration space must be a 'str' but found "
494  f"'{type(it_space).__name__}' instead.")
495 
496  # TODO 1393: We could validate also the value, but at the moment there
497  # are some ambiguities to resolve.
498 
499  self._iteration_space_iteration_space_iteration_space = it_space
500  if len(self.children) > 1:
501  self.start_expr.replace_with(self.lower_boundlower_bound())
502  if len(self.children) > 2:
503  self.stop_expr.replace_with(self.upper_boundupper_bound())
504 
505  @property
506  def bounds_lookup(self):
507  '''
508  :returns: the GOcean loop bounds lookup table. This is a \
509  5-dimensional dictionary with index-offset, field-space, \
510  iteration-space, loop-type, and boundary-side lookup keys \
511  which provides information about how to construct the \
512  loop boundaries for a kernel with such parameters.
513  :rtype: dict
514  '''
515  return self._bounds_lookup_bounds_lookup
516 
518  test_all_variables=False,
519  signatures_to_ignore=None,
520  dep_tools=None):
521  '''
522  This function is a GOcean-specific override of the default method
523  in the Loop class. It allows domain-specific rules to be applied when
524  determining whether or not loop iterations are independent.
525 
526  :param bool test_all_variables: if True, it will test if all variable
527  accesses are independent, otherwise it will stop after the first
528  variable access is found that isn't.
529  :param signatures_to_ignore: list of signatures for which to skip
530  the access checks.
531  :type signatures_to_ignore: Optional[
532  List[:py:class:`psyclone.core.Signature`]]
533  :param dep_tools: an optional instance of DependencyTools so that the
534  caller can access any diagnostic messages detailing why the loop
535  iterations are not independent.
536  :type dep_tools: Optional[
537  :py:class:`psyclone.psyir.tools.DependencyTools`]
538 
539  :returns: True if the loop iterations are independent, False otherwise.
540  :rtype: bool
541 
542  '''
543  if not dep_tools:
544  dtools = DependencyTools()
545  else:
546  dtools = dep_tools
547 
548  try:
549  stat = dtools.can_loop_be_parallelised(
550  self, test_all_variables=test_all_variables,
551  signatures_to_ignore=signatures_to_ignore)
552  return stat
553  except InternalError:
554  # The dependence analysis in GOcean doesn't yet use PSyIR
555  # consistently and that causes failures - TODO #845.
556  return True
557 
558  # -------------------------------------------------------------------------
559  def _halo_read_access(self, arg):
560  '''Determines whether the supplied argument has (or might have) its
561  halo data read within this loop. Returns True if it does, or if
562  it might and False if it definitely does not.
563 
564  :param arg: an argument contained within this loop.
565  :type arg: :py:class:`psyclone.gocean1p0.GOKernelArgument`
566 
567  :return: True if the argument reads, or might read from the \
568  halo and False otherwise.
569  :rtype: bool
570 
571  '''
572  return arg.argument_type == 'field' and arg.stencil.has_stencil and \
573  arg.access in [AccessType.READ, AccessType.READWRITE,
574  AccessType.INC]
575 
577  '''Add halo exchanges before this loop as required by fields within
578  this loop. The PSyIR insertion logic is coded in the _add_halo_exchange
579  helper method. '''
580 
581  for halo_field in self.unique_fields_with_halo_readsunique_fields_with_halo_reads():
582  # for each unique field in this loop that has its halo
583  # read, find the previous update of this field.
584  prev_arg_list = halo_field.backward_write_dependencies()
585  if not prev_arg_list:
586  # field has no previous dependence so create new halo
587  # exchange(s) as we don't know the state of the fields
588  # halo on entry to the invoke
589  # TODO 856: If dl_es_inf supported an is_dirty flag, we could
590  # be more selective on which HaloEx are needed. This
591  # function then will be the same as in LFRic and therefore
592  # the code can maybe be generalised.
593  self._add_halo_exchange_add_halo_exchange(halo_field)
594  else:
595  prev_node = prev_arg_list[0].call
596  if not isinstance(prev_node, HaloExchange):
597  # Previous dependence is not a halo exchange so one needs
598  # to be added to satisfy the dependency in distributed
599  # memory.
600  self._add_halo_exchange_add_halo_exchange(halo_field)
601 
602  def _add_halo_exchange(self, halo_field):
603  '''An internal helper method to add the halo exchange call immediately
604  before this loop using the halo_field argument for the associated
605  field information.
606 
607  :param halo_field: the argument requiring a halo exchange
608  :type halo_field: :py:class:`psyclone.gocean1p0.GOKernelArgument`
609 
610  '''
611  exchange = GOHaloExchange(halo_field, parent=self.parent)
612  self.parent.children.insert(self.position, exchange)
613 
614  # -------------------------------------------------------------------------
615  @staticmethod
617  '''Populates the GOLoop._bounds_lookup dictionary. This is
618  used by PSyclone to look up the loop boundaries for each loop
619  it creates.
620 
621  '''
622  const = GOceanConstants()
623  for grid_offset in const.SUPPORTED_OFFSETS:
624  GOLoop._bounds_lookup[grid_offset] = {}
625  for gridpt_type in const.VALID_FIELD_GRID_TYPES:
626  GOLoop._bounds_lookup[grid_offset][gridpt_type] = {}
627  for itspace in const.VALID_ITERATES_OVER:
628  GOLoop._bounds_lookup[grid_offset][gridpt_type][
629  itspace] = {}
630 
631  # Loop bounds for a mesh with NE offset
632  GOLoop._bounds_lookup['go_offset_ne']['go_ct']['go_all_pts'] = \
633  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
634  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
635  GOLoop._bounds_lookup['go_offset_ne']['go_ct']['go_internal_pts'] = \
636  {'inner': {'start': "{start}", 'stop': "{stop}"},
637  'outer': {'start': "{start}", 'stop': "{stop}"}}
638  GOLoop._bounds_lookup['go_offset_ne']['go_cu']['go_all_pts'] = \
639  {'inner': {'start': "{start}-1", 'stop': "{stop}"},
640  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
641  GOLoop._bounds_lookup['go_offset_ne']['go_cu']['go_internal_pts'] = \
642  {'inner': {'start': "{start}", 'stop': "{stop}-1"},
643  'outer': {'start': "{start}", 'stop': "{stop}"}}
644  GOLoop._bounds_lookup['go_offset_ne']['go_cv']['go_all_pts'] = \
645  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
646  'outer': {'start': "{start}-1", 'stop': "{stop}"}}
647  GOLoop._bounds_lookup['go_offset_ne']['go_cv']['go_internal_pts'] = \
648  {'inner': {'start': "{start}", 'stop': "{stop}"},
649  'outer': {'start': "{start}", 'stop': "{stop}-1"}}
650  GOLoop._bounds_lookup['go_offset_ne']['go_cf']['go_all_pts'] = \
651  {'inner': {'start': "{start}-1", 'stop': "{stop}"},
652  'outer': {'start': "{start}-1", 'stop': "{stop}"}}
653  GOLoop._bounds_lookup['go_offset_ne']['go_cf']['go_internal_pts'] = \
654  {'inner': {'start': "{start}-1", 'stop': "{stop}-1"},
655  'outer': {'start': "{start}-1", 'stop': "{stop}-1"}}
656  # Loop bounds for a mesh with SE offset
657  GOLoop._bounds_lookup['go_offset_sw']['go_ct']['go_all_pts'] = \
658  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
659  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
660  GOLoop._bounds_lookup['go_offset_sw']['go_ct']['go_internal_pts'] = \
661  {'inner': {'start': "{start}", 'stop': "{stop}"},
662  'outer': {'start': "{start}", 'stop': "{stop}"}}
663  GOLoop._bounds_lookup['go_offset_sw']['go_cu']['go_all_pts'] = \
664  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
665  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
666  GOLoop._bounds_lookup['go_offset_sw']['go_cu']['go_internal_pts'] = \
667  {'inner': {'start': "{start}", 'stop': "{stop}+1"},
668  'outer': {'start': "{start}", 'stop': "{stop}"}}
669  GOLoop._bounds_lookup['go_offset_sw']['go_cv']['go_all_pts'] = \
670  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
671  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
672  GOLoop._bounds_lookup['go_offset_sw']['go_cv']['go_internal_pts'] = \
673  {'inner': {'start': "{start}", 'stop': "{stop}"},
674  'outer': {'start': "{start}", 'stop': "{stop}+1"}}
675  GOLoop._bounds_lookup['go_offset_sw']['go_cf']['go_all_pts'] = \
676  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
677  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
678  GOLoop._bounds_lookup['go_offset_sw']['go_cf']['go_internal_pts'] = \
679  {'inner': {'start': "{start}", 'stop': "{stop}+1"},
680  'outer': {'start': "{start}", 'stop': "{stop}+1"}}
681  # For offset 'any'
682  for gridpt_type in const.VALID_FIELD_GRID_TYPES:
683  for itspace in const.VALID_ITERATES_OVER:
684  GOLoop._bounds_lookup['go_offset_any'][gridpt_type][itspace] =\
685  {'inner': {'start': "{start}-1", 'stop': "{stop}"},
686  'outer': {'start': "{start}-1", 'stop': "{stop}"}}
687  # For 'every' grid-point type
688  for offset in const.SUPPORTED_OFFSETS:
689  for itspace in const.VALID_ITERATES_OVER:
690  GOLoop._bounds_lookup[offset]['go_every'][itspace] = \
691  {'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
692  'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
693 
694  # -------------------------------------------------------------------------
695  @staticmethod
696  def add_bounds(bound_info):
697  # pylint: disable=too-many-locals
698  '''
699  Adds a new iteration space to PSyclone. An iteration space in the
700  gocean1.0 API is for a certain offset type and field type. It defines
701  the loop boundaries for the outer and inner loop. The format is a
702  ":" separated tuple:
703 
704  >>> bound_info = offset-type:field-type:iteration-space:outer-start:
705  outer-stop:inner-start:inner-stop
706 
707  Example:
708 
709  >>> bound_info = go_offset_ne:go_ct:go_all_pts:
710  {start}-1:{stop}+1:{start}:{stop}
711 
712  The expressions {start} and {stop} will be replaced with the loop
713  indices that correspond to the inner points (i.e. non-halo or
714  boundary points) of the field. So the index {start}-1 is actually
715  on the halo / boundary.
716 
717  :param str bound_info: A string that contains a ":" separated \
718  tuple with the iteration space definition.
719 
720  :raises ValueError: if bound_info is not a string.
721  :raises ConfigurationError: if bound_info is not formatted correctly.
722 
723  '''
724  if not isinstance(bound_info, str):
725  raise InternalError(f"The parameter 'bound_info' must be a "
726  f"string, got '{bound_info}' "
727  f"(type {type(bound_info)})")
728 
729  data = bound_info.split(":")
730  if len(data) != 7:
731  raise ConfigurationError(f"An iteration space must be in the form "
732  f"\"offset-type:field-type:"
733  f"iteration-space:outer-start:"
734  f"outer-stop:inner-start:inner-stop\"\n"
735  f"But got \"{bound_info}\"")
736 
737  if not GOLoop._bounds_lookup:
738  GOLoop.setup_bounds()
739 
740  # Check that all bound specifications (min and max index) are valid.
741  # ------------------------------------------------------------------
742  # Regular expression that finds stings surrounded by {}
743  bracket_regex = re.compile("{[^}]+}")
744  for bound in data[3:7]:
745  all_expr = bracket_regex.findall(bound)
746  for bracket_expr in all_expr:
747  if bracket_expr not in ["{start}", "{stop}"]:
748  raise ConfigurationError(f"Only '{{start}}' and "
749  f"'{{stop}}' are allowed as "
750  f"bracketed expression in an "
751  f"iteration space. But got "
752  f"{bracket_expr}")
753 
754  # We need to make sure the fparser is properly initialised, which
755  # typically has not yet happened when the config file is read.
756  # Otherwise the Nonlabel_Do_Stmt cannot parse valid expressions.
757  ParserFactory().create(std="f2008")
758 
759  # Test both the outer loop indices (index 3 and 4) and inner
760  # indices (index 5 and 6):
761  for bound in data[3:7]:
762  do_string = f"do i=1, {bound}"
763  # Now replace any {start}/{stop} expression in the loop
764  # with a valid integer value:
765  do_string = do_string.format(start='15', stop='25')
766  # Check if the do loop can be parsed as a nonlabel do loop
767  try:
768  _ = Nonlabel_Do_Stmt(do_string)
769  except NoMatchError as err:
770  raise ConfigurationError(f"Expression '{bound}' is not a "
771  f"valid do loop boundary. Error "
772  f"message: '{err}'.") from err
773 
774  # All tests successful, so add the new bounds:
775  # --------------------------------------------
776  current_bounds = GOLoop._bounds_lookup # Shortcut
777  # Check offset-type exists
778  if not data[0] in current_bounds:
779  current_bounds[data[0]] = {}
780 
781  # Check field-type exists
782  if not data[1] in current_bounds[data[0]]:
783  current_bounds[data[0]][data[1]] = {}
784 
785  const = GOceanConstants()
786  # Check iteration space exists:
787  if not data[2] in current_bounds[data[0]][data[1]]:
788  current_bounds[data[0]][data[1]][data[2]] = {}
789  const.VALID_ITERATES_OVER.append(data[2])
790 
791  current_bounds[data[0]][data[1]][data[2]] = \
792  {'outer': {'start': data[3], 'stop': data[4]},
793  'inner': {'start': data[5], 'stop': data[6]}}
794 
795  def get_custom_bound_string(self, side):
796  '''
797  Get the string that represents a customized custom bound for this
798  GOLoop (provided by the add_bounds() method). It can provide the
799  'start' or 'stop' side of the bounds.
800 
801  :param str side: 'start' or 'stop' side of the bound.
802 
803  :returns: the string that represents the loop bound.
804  :rtype: str
805 
806  :raises GenerationError: if this node can not find a field in \
807  the Invoke to be the base of the infrastructure call.
808  :raises GenerationError: if no expression is known to obtain the \
809  boundaries for a loop of this characteristics, because they \
810  are not in the GOcean lookup table or the loop type is not \
811  `inner` or `outer`.
812  '''
813  api_config = Config.get().api_conf("gocean1.0")
814  # Get a field argument from the argument list
815  field = None
816  invoke = self.ancestor(InvokeSchedule)
817  if invoke:
818  for arg in invoke.symbol_table.argument_list:
819  if isinstance(arg.datatype, DataTypeSymbol):
820  if arg.datatype.name == "r2d_field":
821  field = arg
822  break
823 
824  if field is None:
825  raise GenerationError(
826  f"Cannot generate custom loop bound for loop {self}. "
827  f"Couldn't find any suitable field.")
828 
829  if self.loop_typeloop_typeloop_typeloop_typeloop_type == "inner":
830  prop_access = api_config.grid_properties["go_grid_xstop"]
831  elif self.loop_typeloop_typeloop_typeloop_typeloop_type == "outer":
832  prop_access = api_config.grid_properties["go_grid_ystop"]
833  else:
834  raise GenerationError(
835  f"Invalid loop type of '{self.loop_type}'. Expected one of "
836  f"{GOceanConstants().VALID_LOOP_TYPES}")
837 
838  stop_expr = prop_access.fortran.format(field.name)
839  try:
840  bound = self.bounds_lookupbounds_lookup[self.index_offsetindex_offset][self.field_spacefield_spacefield_spacefield_spacefield_spacefield_spacefield_space][
842  start='2', stop=stop_expr)
843  except KeyError as err:
844  raise GenerationError(
845  f"Cannot generate custom loop bound for a loop with an index-"
846  f"offset of '{self.index_offset}', a field-space of "
847  f"'{self.field_space}', an iteration-space of "
848  f"'{self.iteration_space}' and a loop-type of "
849  f"'{self.loop_type}', for the side '{side}' because "
850  f"this keys combination does not exist in the "
851  f"GOLoop.bounds_lookup table.") from err
852 
853  return bound
854 
855  # -------------------------------------------------------------------------
856  def _grid_property_psyir_expression(self, grid_property):
857  '''
858  Create a PSyIR reference expression using the supplied grid-property
859  information (which will have been read from the config file).
860 
861  :param str grid_property: the property of the grid for which to \
862  create a reference. This is the format string read from the \
863  config file or just a simple name.
864 
865  :returns: the PSyIR expression for the grid-property access.
866  :rtype: :py:class:`psyclone.psyir.nodes.Reference` or sub-class
867 
868  '''
869  members = grid_property.split("%")
870  if len(members) == 1:
871  # We don't have a derived-type reference so create a Reference to
872  # a data symbol.
873  try:
874  sym = self.scope.symbol_table.lookup(members[0])
875  except KeyError:
876  sym = self.scope.symbol_table.new_symbol(
877  members[0], symbol_type=DataSymbol, datatype=INTEGER_TYPE)
878  return Reference(sym, parent=self)
879 
880  if members[0] != "{0}":
881  raise NotImplementedError(
882  f"Supplied grid property is a derived-type reference but "
883  f"does not begin with '{{0}}': '{grid_property}'")
884 
885  fld_sym = self.scope.symbol_table.lookup(self.field_namefield_namefield_namefield_namefield_name)
886  return StructureReference.create(fld_sym, members[1:])
887 
888  def upper_bound(self):
889  ''' Creates the PSyIR of the upper bound of this loop.
890 
891  :returns: the PSyIR for the upper bound of this loop.
892  :rtype: :py:class:`psyclone.psyir.nodes.Node`
893 
894  '''
896  # Bounds are independent of the grid-offset convention in use
897  # We look-up the upper bounds by enquiring about the SIZE of
898  # the array itself
899  stop = IntrinsicCall(IntrinsicCall.Intrinsic.SIZE)
900  # Use the data property to access the member of the field that
901  # contains the actual grid points.
902  api_config = Config.get().api_conf("gocean1.0")
903  sref = self._grid_property_psyir_expression_grid_property_psyir_expression(
904  api_config.grid_properties["go_grid_data"].fortran)
905  stop.addchild(sref)
906  if self._loop_type_loop_type_loop_type == "inner":
907  stop.addchild(Literal("1", INTEGER_TYPE, parent=stop))
908  elif self._loop_type_loop_type_loop_type == "outer":
909  stop.addchild(Literal("2", INTEGER_TYPE, parent=stop))
910  return stop
911 
912  # Loop bounds are pulled from a infrastructure call from a field
913  # object. For 'go_internal_pts' and 'go_all_points' we use the
914  # 'internal' and 'whole' structures respectively. For other
915  # iteration_spaces we look if a custom expression is defined in the
916  # lookup table.
917  props = Config.get().api_conf("gocean1.0").grid_properties
918  if self.iteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_space.lower() == "go_internal_pts":
919  return self._grid_property_psyir_expression_grid_property_psyir_expression(
920  props[f"go_grid_internal_{self._loop_type}_stop"].fortran)
922  return self._grid_property_psyir_expression_grid_property_psyir_expression(
923  props[f"go_grid_whole_{self._loop_type}_stop"].fortran)
924  bound_str = self.get_custom_bound_stringget_custom_bound_string("stop")
925  return FortranReader().psyir_from_expression(
926  bound_str, self.ancestor(GOInvokeSchedule).symbol_table)
927 
928  def lower_bound(self):
929  ''' Returns the lower bound of this loop as a string.
930 
931  :returns: root of PSyIR sub-tree describing this lower bound.
932  :rtype: :py:class:`psyclone.psyir.nodes.Node`
933 
934  '''
936  # Bounds are independent of the grid-offset convention in use
937  return Literal("1", INTEGER_TYPE)
938 
939  # Loop bounds are pulled from a infrastructure call from a field
940  # object. For 'go_internal_pts' and 'go_all_points' we use the
941  # 'internal' and 'whole' structures respectively. For other
942  # iteration_spaces we look if a custom expression is defined in the
943  # lookup table.
944  props = Config.get().api_conf("gocean1.0").grid_properties
945  if self.iteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_spaceiteration_space.lower() == "go_internal_pts":
946  return self._grid_property_psyir_expression_grid_property_psyir_expression(
947  props[f"go_grid_internal_{self._loop_type}_start"].fortran)
949  return self._grid_property_psyir_expression_grid_property_psyir_expression(
950  props[f"go_grid_whole_{self._loop_type}_start"].fortran)
951  bound_str = self.get_custom_bound_stringget_custom_bound_string("start")
952  return FortranReader().psyir_from_expression(
953  bound_str, self.ancestor(GOInvokeSchedule).symbol_table)
954 
955  def _validate_loop(self):
956  ''' Validate that the GOLoop has all necessary boundaries information
957  to lower or gen_code to f2pygen.
958 
959  :raises GenerationError: if we can't find an enclosing Schedule.
960  :raises GenerationError: if this loop does not enclose a Kernel.
961  :raises GenerationError: if constant loop bounds are enabled but are \
962  not supported for the current grid offset.
963  :raises GenerationError: if the kernels within this loop expect \
964  different different grid offsets.
965 
966  '''
967  # Our schedule holds the names to use for the loop bounds.
968  # Climb up the tree looking for our enclosing GOInvokeSchedule
969  schedule = self.ancestor(GOInvokeSchedule)
970  if schedule is None:
971  raise GenerationError("Cannot find a GOInvokeSchedule ancestor "
972  "for this GOLoop.")
973 
974  # Walk down the tree looking for a kernel so that we can
975  # look-up what index-offset convention we are to use
976  go_kernels = self.walk(GOKern)
977  if not go_kernels:
978  raise GenerationError("Cannot find the "
979  "GOcean Kernel enclosed by this loop")
980  index_offset = go_kernels[0].index_offset
981 
982  # Check that all kernels enclosed by this loop expect the same
983  # grid offset
984  for kernel in go_kernels:
985  if kernel.index_offset != index_offset:
986  raise GenerationError(f"All Kernels must expect the same "
987  f"grid offset but kernel "
988  f"'{kernel.name}' has offset"
989  f" '{kernel.index_offset}' which does "
990  f"not match '{index_offset}'.")
991 
992  def gen_code(self, parent):
993  ''' Create the f2pygen AST for this loop (and update the PSyIR
994  representing the loop bounds if necessary).
995 
996  :param parent: the node in the f2pygen AST to which to add content.
997  :type parent: :py:class:`psyclone.f2pygen.SubroutineGen`
998 
999  '''
1000  # Check that it is a properly formed GOLoop
1001  self._validate_loop_validate_loop()
1002 
1003  super().gen_code(parent)
1004 
1005 
1006 # pylint: disable=too-few-public-methods
1008  ''' A GOcean-specific built-in call factory. No built-ins
1009  are supported in GOcean at the moment. '''
1010 
1011  @staticmethod
1012  def create():
1013  ''' Placeholder to create a GOocean-specific built-in call.
1014  This will require us to create a doubly-nested loop and then create
1015  the body of the particular built-in operation. '''
1016  raise GenerationError(
1017  "Built-ins are not supported for the GOcean 1.0 API")
1018 
1019 
1020 # pylint: disable=too-few-public-methods
1022  ''' A GOcean-specific kernel-call factory. A standard kernel call in
1023  GOcean consists of a doubly-nested loop (over i and j) and a call to
1024  the user-supplied kernel routine. '''
1025  @staticmethod
1026  def create(call, parent=None):
1027  ''' Create a new instance of a call to a GO kernel. Includes the
1028  looping structure as well as the call to the kernel itself.
1029 
1030  :param parent: node where the kernel call structure will be inserted.
1031  :type parent: :py:class:`psyclone.psyir.nodes.Node`
1032 
1033  :returns: new PSyIR tree representing the kernel call loop.
1034  :rtype: :py:class:`psyclone.gocean1p0.GOLoop`
1035 
1036  '''
1037  # Add temporary parent as the GOKern constructor needs to find its
1038  # way to the top-level InvokeSchedule but we still don't have the
1039  # PSyIR loops to place it in the appropriate place. We can't create
1040  # the loops first because those depend on information provided by
1041  # this kernel.
1042  gocall = GOKern(call, parent=parent)
1043 
1044  # Determine Loop information from the enclosed Kernel
1045  iteration_space = gocall.iterates_over
1046  field_space = gocall.arguments.iteration_space_arg().function_space
1047  field_name = gocall.arguments.iteration_space_arg().name
1048  index_offset = gocall.index_offset
1049 
1050  # Create the double loop structure
1051  outer_loop = GOLoop.create(loop_type="outer",
1052  iteration_space=iteration_space,
1053  field_space=field_space,
1054  field_name=field_name,
1055  index_offset=index_offset,
1056  parent=parent)
1057  inner_loop = GOLoop.create(loop_type="inner",
1058  iteration_space=iteration_space,
1059  field_space=field_space,
1060  field_name=field_name,
1061  index_offset=index_offset,
1062  parent=outer_loop.loop_body)
1063  outer_loop.loop_body.addchild(inner_loop)
1064  # Remove temporary parent
1065  # pylint: disable=protected-access
1066  gocall._parent = None
1067  inner_loop.loop_body.addchild(gocall)
1068  return outer_loop
1069 
1070 
1072  '''
1073  Stores information about GOcean Kernels as specified by the Kernel
1074  metadata. Uses this information to generate appropriate PSy layer
1075  code for the Kernel instance. Specialises the gen_code method to
1076  create the appropriate GOcean specific kernel call.
1077 
1078  :param call: information on the way in which this kernel is called \
1079  from the Algorithm layer.
1080  :type call: :py:class:`psyclone.parse.algorithm.KernelCall`
1081  :param parent: optional node where the kernel call will be inserted.
1082  :type parent: :py:class:`psyclone.psyir.nodes.Node`
1083 
1084  '''
1085  def __init__(self, call, parent=None):
1086  super().__init__(GOKernelArguments, call, parent, check=False)
1087  # Store the name of this kernel type (i.e. the name of the
1088  # Fortran derived type containing its metadata).
1089  self._metadata_name_metadata_name = call.ktype.name
1090  # Pull out the grid index-offset that this kernel expects and
1091  # store it here. This is used to check that all of the kernels
1092  # invoked by an application are using compatible index offsets.
1093  self._index_offset_index_offset = call.ktype.index_offset
1094 
1095  @staticmethod
1096  def _create_psyir_for_access(symbol, var_value, depth):
1097  '''This function creates the PSyIR of an index-expression:
1098  - if `var_value` is negative, it returns 'symbol-depth'.
1099  - if `var_value` is positive, it returns 'symbol+depth`
1100  - otherwise it just returns a Reference to `symbol`.
1101  This is used to create artificial stencil accesses for GOKernels.
1102 
1103  :param symbol: the symbol to use.
1104  :type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
1105  :param int var_value: value of the variable, which determines the \
1106  direction (adding or subtracting depth).
1107  :param int depth: the depth of the access (>0).
1108 
1109  :returns: the index expression for an access in the given direction.
1110  :rtype: union[:py:class:`psyclone.psyir.nodes.Reference`,
1111  :py:class:`psyclone.psyir.nodes.BinaryOperation`]
1112 
1113  '''
1114  if var_value == 0:
1115  return Reference(symbol)
1116  if var_value > 0:
1117  operator = BinaryOperation.Operator.ADD
1118  else:
1119  operator = BinaryOperation.Operator.SUB
1120 
1121  return BinaryOperation.create(operator,
1122  Reference(symbol),
1123  Literal(str(depth), INTEGER_TYPE))
1124 
1125  def _record_stencil_accesses(self, signature, arg, var_accesses):
1126  '''This function adds accesses to a field depending on the
1127  meta-data declaration for this argument (i.e. accounting for
1128  any stencil accesses).
1129 
1130  :param signature: signature of the variable.
1131  :type signature: :py:class:`psyclone.core.Signature`
1132  :param arg: the meta-data information for this argument.
1133  :type arg: :py:class:`psyclone.gocean1p0.GOKernelArgument`
1134  :param var_accesses: VariablesAccessInfo instance that stores the\
1135  information about the field accesses.
1136  :type var_accesses: \
1137  :py:class:`psyclone.core.VariablesAccessInfo`
1138 
1139  '''
1140  # TODO #2530: if we parse the actual kernel code, it might not
1141  # be required anymore to add these artificial accesses, instead
1142  # the actual kernel accesses could be added.
1143  sym_tab = self.ancestor(GOInvokeSchedule).symbol_table
1144  symbol_i = sym_tab.lookup_with_tag("contiguous_kidx")
1145  symbol_j = sym_tab.lookup_with_tag("noncontiguous_kidx")
1146  # Query each possible stencil direction and add corresponding
1147  # variable accesses. Note that if (i,j) itself is accessed, the
1148  # depth will be 1, so one access to (i,j) is then added.
1149  for j in [-1, 0, 1]:
1150  for i in [-1, 0, 1]:
1151  depth = arg.stencil.depth(i, j)
1152  for current_depth in range(1, depth+1):
1153  # Create PSyIR expressions for the required
1154  # i+/- and j+/- expressions
1155  i_expr = GOKern._create_psyir_for_access(symbol_i, i,
1156  current_depth)
1157  j_expr = GOKern._create_psyir_for_access(symbol_j, j,
1158  current_depth)
1159  # Even if a GOKern argument is declared to be written, it
1160  # can only ever write to (i,j), so any other references
1161  # must be read:
1162  if i == 0 and j == 0:
1163  acc = arg.access
1164  else:
1165  acc = AccessType.READ
1166 
1167  var_accesses.add_access(signature, acc, self,
1168  [i_expr, j_expr])
1169 
1170  def reference_accesses(self, var_accesses):
1171  '''Get all variable access information. All accesses are marked
1172  according to the kernel metadata.
1173 
1174  :param var_accesses: VariablesAccessInfo instance that stores the\
1175  information about variable accesses.
1176  :type var_accesses: \
1177  :py:class:`psyclone.core.VariablesAccessInfo`
1178 
1179  '''
1180  # Grid properties are accessed using one of the fields. This stores
1181  # the field used to avoid repeatedly determining the best field:
1182  field_for_grid_property = None
1183  for arg in self.argumentsarguments.args:
1184  if arg.argument_type == "grid_property":
1185  if not field_for_grid_property:
1186  field_for_grid_property = \
1187  self._arguments_arguments.find_grid_access()
1188  var_name = arg.dereference(field_for_grid_property.name)
1189  else:
1190  var_name = arg.name
1191 
1192  signature = Signature(var_name.split("%"))
1193  if arg.is_scalar:
1194  # The argument is only a variable if it is not a constant:
1195  if not arg.is_literal:
1196  var_accesses.add_access(signature, arg.access, self)
1197  else:
1198  if arg.argument_type == "field":
1199  # Now add 'artificial' accesses to this field depending
1200  # on meta-data (access-mode and stencil information):
1201  self._record_stencil_accesses_record_stencil_accesses(signature, arg,
1202  var_accesses)
1203  else:
1204  # In case of an array for now add an arbitrary array
1205  # reference to (i,j) so it is properly recognised as
1206  # an array access.
1207  sym_tab = self.ancestor(GOInvokeSchedule).symbol_table
1208  symbol_i = sym_tab.lookup_with_tag("contiguous_kidx")
1209  symbol_j = sym_tab.lookup_with_tag("noncontiguous_kidx")
1210  var_accesses.add_access(signature, arg.access,
1211  self, [Reference(symbol_i),
1212  Reference(symbol_j)])
1213  super().reference_accesses(var_accesses)
1214  var_accesses.next_location()
1215 
1216  def local_vars(self):
1217  '''Return a list of the variable (names) that are local to this loop
1218  (and must therefore be e.g. threadprivate if doing OpenMP)
1219 
1220  '''
1221  return []
1222 
1223  @property
1224  def index_offset(self):
1225  ''' The grid index-offset convention that this kernel expects '''
1226  return self._index_offset_index_offset
1227 
1229  '''
1230  :returns: a schedule representing the GOcean kernel code.
1231  :rtype: :py:class:`psyclone.gocean1p0.GOKernelSchedule`
1232 
1233  :raises GenerationError: if there is a problem raising the language- \
1234  level PSyIR of this kernel to GOcean PSyIR.
1235  '''
1236  if self._kern_schedule_kern_schedule_kern_schedule:
1237  return self._kern_schedule_kern_schedule_kern_schedule
1238 
1239  # Construct the PSyIR of the Fortran parse tree.
1240  astp = Fparser2Reader()
1241  psyir = astp.generate_psyir(self.astast)
1242  # pylint: disable=import-outside-toplevel
1244  RaisePSyIR2GOceanKernTrans)
1245  raise_trans = RaisePSyIR2GOceanKernTrans(self._metadata_name_metadata_name)
1246  try:
1247  raise_trans.apply(psyir)
1248  except Exception as err:
1249  raise GenerationError(
1250  f"Failed to raise the PSyIR for kernel '{self.name}' "
1251  f"to GOcean PSyIR. Error was:\n{err}") from err
1252  for routine in psyir.walk(Routine):
1253  if routine.name == self.namenamenamename:
1254  break
1255  # We know the above loop will find the named routine because the
1256  # previous raising transformation would have failed otherwise.
1257  # pylint: disable=undefined-loop-variable
1258  self._kern_schedule_kern_schedule_kern_schedule = routine
1259 
1260  return self._kern_schedule_kern_schedule_kern_schedule
1261 
1262 
1264  '''Provides information about GOcean kernel-call arguments
1265  collectively, as specified by the kernel argument metadata. This
1266  class ensures that initialisation is performed correctly. It also
1267  overrides the iteration_space_arg method to supply a
1268  GOcean-specific dictionary for the mapping of argument-access
1269  types.
1270 
1271  :param call: the kernel meta-data for which to extract argument info.
1272  :type call: :py:class:`psyclone.parse.KernelCall`
1273  :param parent_call: the kernel-call object.
1274  :type parent_call: :py:class:`psyclone.gocean1p0.GOKern`
1275  :param bool check: whether to check for consistency between the \
1276  kernel metadata and the algorithm layer. Defaults to \
1277  True. Currently does nothing in this API.
1278 
1279  '''
1280  def __init__(self, call, parent_call, check=True):
1281  # pylint: disable=unused-argument
1282  if False: # pylint: disable=using-constant-test
1283  self._0_to_n_0_to_n = GOKernelArgument(None, None, None) # for pyreverse
1284  Arguments.__init__(self, parent_call)
1285 
1286  self._args_args_args = []
1287  # Loop over the kernel arguments obtained from the meta data
1288  for (idx, arg) in enumerate(call.ktype.arg_descriptors):
1289  # arg is a GO1p0Descriptor object
1290  if arg.argument_type == "grid_property":
1291  # This is an argument supplied by the psy layer
1292  self._args_args_args.append(GOKernelGridArgument(arg, parent_call))
1293  elif arg.argument_type in ["scalar", "field"]:
1294  # This is a kernel argument supplied by the Algorithm layer
1295  self._args_args_args.append(GOKernelArgument(arg, call.args[idx],
1296  parent_call))
1297  else:
1298  raise ParseError(f"Invalid kernel argument type. Found "
1299  f"'{arg.argument_type}' but must be one of "
1300  f"['grid_property', 'scalar', 'field'].")
1301  self._dofs_dofs = []
1302 
1304  '''
1305  :returns: the PSyIR expressions representing this Argument list.
1306  :rtype: list of :py:class:`psyclone.psyir.nodes.Node`
1307 
1308  '''
1309  symtab = self._parent_call_parent_call.scope.symbol_table
1310  symbol1 = symtab.lookup_with_tag("contiguous_kidx")
1311  symbol2 = symtab.lookup_with_tag("noncontiguous_kidx")
1312  return ([Reference(symbol1), Reference(symbol2)] +
1313  [arg.psyir_expression() for arg in self.argsargs])
1314 
1315  def find_grid_access(self):
1316  '''
1317  Determine the best kernel argument from which to get properties of
1318  the grid. For this, an argument must be a field (i.e. not
1319  a scalar) and must be supplied by the algorithm layer
1320  (i.e. not a grid property). If possible it should also be
1321  a field that is read-only as otherwise compilers can get
1322  confused about data dependencies and refuse to SIMD
1323  vectorise.
1324  :returns: the argument object from which to get grid properties.
1325  :rtype: :py:class:`psyclone.gocean1p0.GOKernelArgument` or None
1326  '''
1327  for access in [AccessType.READ, AccessType.READWRITE,
1328  AccessType.WRITE]:
1329  for arg in self._args_args_args:
1330  if arg.argument_type == "field" and arg.access == access:
1331  return arg
1332  # We failed to find any kernel argument which could be used
1333  # to access the grid properties. This will only be a problem
1334  # if the kernel requires a grid-property argument.
1335  return None
1336 
1337  @property
1338  def dofs(self):
1339  ''' Currently required for invoke base class although this makes no
1340  sense for GOcean. Need to refactor the Invoke base class and
1341  remove the need for this property (#279). '''
1342  return self._dofs_dofs
1343 
1344  @property
1345  def acc_args(self):
1346  '''
1347  Provide the list of references (both objects and arrays) that must
1348  be present on an OpenACC device before the kernel associated with
1349  this Arguments object may be launched.
1350 
1351  :returns: list of (Fortran) quantities
1352  :rtype: list of str
1353  '''
1354  arg_list = []
1355 
1356  # First off, specify the field object which we will de-reference in
1357  # order to get any grid properties (if this kernel requires them).
1358  # We do this as some compilers do less optimisation if we get (read-
1359  # -only) grid properties from a field object that has read-write
1360  # access.
1361  grid_fld = self.find_grid_accessfind_grid_access()
1362  grid_ptr = grid_fld.name + "%grid"
1363  api_config = Config.get().api_conf("gocean1.0")
1364  # TODO: #676 go_grid_data is actually a field property
1365  data_fmt = api_config.grid_properties["go_grid_data"].fortran
1366  arg_list.extend([grid_fld.name, data_fmt.format(grid_fld.name)])
1367  for arg in self._args_args_args:
1368  if arg.argument_type == "scalar":
1369  arg_list.append(arg.name)
1370  elif arg.argument_type == "field" and arg != grid_fld:
1371  # The remote device will need the reference to the field
1372  # object *and* the reference to the array within that object.
1373  arg_list.extend([arg.name, data_fmt.format(arg.name)])
1374  elif arg.argument_type == "grid_property":
1375  if grid_ptr not in arg_list:
1376  # This kernel needs a grid property and therefore the
1377  # pointer to the grid object must be copied to the device.
1378  arg_list.append(grid_ptr)
1379  arg_list.append(grid_ptr+"%"+arg.name)
1380  return arg_list
1381 
1382  @property
1383  def fields(self):
1384  '''
1385  Provides the list of names of field objects that are required by
1386  the kernel associated with this Arguments object.
1387 
1388  :returns: List of names of (Fortran) field objects.
1389  :rtype: list of str
1390  '''
1391  args = args_filter(self._args_args_args, arg_types=["field"])
1392  return [arg.name for arg in args]
1393 
1394  @property
1395  def scalars(self):
1396  '''
1397  Provides the list of names of scalar arguments required by the
1398  kernel associated with this Arguments object. If there are none
1399  then the returned list is empty.
1400 
1401  :returns: A list of the names of scalar arguments in this object.
1402  :rtype: list of str
1403  '''
1404  args = args_filter(self._args_args_args, arg_types=["scalar"])
1405  return [arg.name for arg in args]
1406 
1407  def append(self, name, argument_type):
1408  ''' Create and append a GOKernelArgument to the Argument list.
1409 
1410  :param str name: name of the appended argument.
1411  :param str argument_type: type of the appended argument.
1412 
1413  :raises TypeError: if the given name is not a string.
1414 
1415  '''
1416  if not isinstance(name, str):
1417  raise TypeError(
1418  f"The name parameter given to GOKernelArguments.append "
1419  f"method should be a string, but found "
1420  f"'{type(name).__name__}' instead.")
1421 
1422  # Create a descriptor with the given type. `len(self.args)` gives the
1423  # position in the argument list of the argument to which this
1424  # descriptor corresponds. (This argument is appended in the code
1425  # below.)
1426  descriptor = Descriptor(None, argument_type, len(self.argsargs))
1427 
1428  # Create the argument and append it to the argument list
1429  arg = Arg("variable", name)
1430  argument = GOKernelArgument(descriptor, arg, self._parent_call_parent_call)
1431  self.argsargs.append(argument)
1432 
1433 
1435  ''' Provides information about individual GOcean kernel call arguments
1436  as specified by the kernel argument metadata. '''
1437  def __init__(self, arg, arg_info, call):
1438 
1439  self._arg_arg_arg = arg
1440  KernelArgument.__init__(self, arg, arg_info, call)
1441  # Complete the argument initialisation as in some APIs it
1442  # needs to be separated.
1443  self._complete_init_complete_init(arg_info)
1444 
1445  def psyir_expression(self):
1446  '''
1447  :returns: the PSyIR expression represented by this Argument.
1448  :rtype: :py:class:`psyclone.psyir.nodes.Node`
1449 
1450  :raises InternalError: if this Argument type is not "field" or \
1451  "scalar".
1452 
1453  '''
1454  # If the argument name is just a number (e.g. '0') we return a
1455  # constant Literal expression
1456  if self.namename.isnumeric():
1457  return Literal(self.namename, INTEGER_TYPE)
1458 
1459  # Now try for a real value. The constructor will raise an exception
1460  # if the string is not a valid floating point number.
1461  try:
1462  return Literal(self.namename, REAL_TYPE)
1463  except ValueError:
1464  pass
1465 
1466  # Otherwise it's some form of Reference
1467  symbol = self._call_call.scope.symbol_table.lookup(self.namename)
1468 
1469  # Gocean field arguments are StructureReferences to the %data attribute
1470  if self.argument_typeargument_typeargument_typeargument_type == "field":
1471  return StructureReference.create(symbol, ["data"])
1472 
1473  # Gocean scalar arguments are References to the variable
1474  if self.argument_typeargument_typeargument_typeargument_type == "scalar":
1475  return Reference(symbol)
1476 
1477  raise InternalError(f"GOcean expects the Argument.argument_type() to "
1478  f"be 'field' or 'scalar' but found "
1479  f"'{self.argument_type}'.")
1480 
1481  def infer_datatype(self):
1482  ''' Infer the datatype of this argument using the API rules.
1483 
1484  :returns: the datatype of this argument.
1485  :rtype: :py:class::`psyclone.psyir.symbols.DataType`
1486 
1487  :raises InternalError: if this Argument type is not "field" or \
1488  "scalar".
1489  :raises InternalError: if this argument is scalar but its space \
1490  property is not 'go_r_scalar' or 'go_i_scalar'.
1491 
1492  '''
1493  # All GOcean fields are r2d_field
1494  if self.argument_typeargument_typeargument_typeargument_type == "field":
1495  # r2d_field can have UnresolvedType and UnresolvedInterface because
1496  # it is an unnamed import from a module.
1497  type_symbol = self._call_call.root.symbol_table.find_or_create_tag(
1498  "r2d_field", symbol_type=DataTypeSymbol,
1499  datatype=UnresolvedType(), interface=UnresolvedInterface())
1500  return type_symbol
1501 
1502  # Gocean scalars can be REAL or INTEGER
1503  if self.argument_typeargument_typeargument_typeargument_type == "scalar":
1504  if self.spacespace.lower() == "go_r_scalar":
1505  go_wp = self._call_call.root.symbol_table.find_or_create_tag(
1506  "go_wp", symbol_type=DataSymbol, datatype=UnresolvedType(),
1507  interface=UnresolvedInterface())
1508  return ScalarType(ScalarType.Intrinsic.REAL, go_wp)
1509  if self.spacespace.lower() == "go_i_scalar":
1510  return INTEGER_TYPE
1511  raise InternalError(f"GOcean expects scalar arguments to be of "
1512  f"'go_r_scalar' or 'go_i_scalar' type but "
1513  f"found '{self.space.lower()}'.")
1514 
1515  raise InternalError(f"GOcean expects the Argument.argument_type() "
1516  f"to be 'field' or 'scalar' but found "
1517  f"'{self.argument_type}'.")
1518 
1519  @property
1520  def intrinsic_type(self):
1521  '''
1522  :returns: the intrinsic type of this argument. If it's not a scalar \
1523  integer or real it will return an empty string.
1524  :rtype: str
1525 
1526  '''
1527  if self.argument_typeargument_typeargument_typeargument_type == "scalar":
1528  if self.spacespace.lower() == "go_r_scalar":
1529  return "real"
1530  if self.spacespace.lower() == "go_i_scalar":
1531  return "integer"
1532  return ""
1533 
1534  @property
1535  def argument_type(self):
1536  '''
1537  Return the type of this kernel argument - whether it is a field,
1538  a scalar or a grid_property (to be supplied by the PSy layer).
1539  If it has no type it defaults to scalar.
1540 
1541  :returns: the type of the argument.
1542  :rtype: str
1543 
1544  '''
1545  if self._arg_arg_arg.argument_type:
1546  return self._arg_arg_arg.argument_type
1547  return "scalar"
1548 
1549  @property
1550  def function_space(self):
1551  ''' Returns the expected finite difference space for this
1552  argument as specified by the kernel argument metadata.'''
1553  return self._arg_arg_arg.function_space
1554 
1555  @property
1556  def is_scalar(self):
1557  ''':return: whether this variable is a scalar variable or not.
1558  :rtype: bool'''
1559  return self.argument_typeargument_typeargument_typeargument_type == "scalar"
1560 
1561 
1563  '''
1564  Describes arguments that supply grid properties to a kernel.
1565  These arguments are provided by the PSy layer rather than in
1566  the Algorithm layer.
1567 
1568  :param arg: the meta-data entry describing the required grid property.
1569  :type arg: :py:class:`psyclone.gocean1p0.GO1p0Descriptor`
1570  :param kernel_call: the kernel call node that this Argument belongs to.
1571  :type kernel_call: :py:class:`psyclone.gocean1p0.GOKern`
1572 
1573  :raises GenerationError: if the grid property is not recognised.
1574 
1575  '''
1576  def __init__(self, arg, kernel_call):
1577  super().__init__(None, None, arg.access)
1578  # Complete the argument initialisation as in some APIs it
1579  # needs to be separated.
1580  self._complete_init_complete_init(None)
1581 
1582  api_config = Config.get().api_conf("gocean1.0")
1583  try:
1584  deref_name = api_config.grid_properties[arg.grid_prop].fortran
1585  except KeyError as err:
1586  all_keys = str(api_config.grid_properties.keys())
1587  raise GenerationError(f"Unrecognised grid property specified. "
1588  f"Expected one of {all_keys} but found "
1589  f"'{arg.grid_prop}'") from err
1590 
1591  # Each entry is a pair (name, type). Name can be subdomain%internal...
1592  # so only take the last part after the last % as name.
1593  self._name_name_name = deref_name.split("%")[-1]
1594  # Store the original property name for easy lookup in is_scalar
1595  self._property_name_property_name = arg.grid_prop
1596 
1597  # This object always represents an argument that is a grid_property
1598  self._argument_type_argument_type = "grid_property"
1599 
1600  # Reference to the Call this argument belongs to
1601  self._call_call_call = kernel_call
1602 
1603  @property
1604  def name(self):
1605  ''' Returns the Fortran name of the grid property, which is used
1606  in error messages etc.'''
1607  return self._name_name_name
1608 
1609  def psyir_expression(self):
1610  '''
1611  :returns: the PSyIR expression represented by this Argument.
1612  :rtype: :py:class:`psyclone.psyir.nodes.Node`
1613 
1614  '''
1615  # Find field from which to access grid properties
1616  base_field = self._call_call_call.arguments.find_grid_access().name
1617  tag = "AlgArgs_" + base_field
1618  symbol = self._call_call_call.scope.symbol_table.find_or_create_tag(tag)
1619 
1620  # Get aggregate grid type accessors without the base name
1621  access = self.dereferencedereference(base_field).split('%')[1:]
1622 
1623  # Construct the PSyIR reference
1624  return StructureReference.create(symbol, access)
1625 
1626  def dereference(self, fld_name):
1627  '''Returns a Fortran string to dereference a grid property of the
1628  specified field. It queries the current config file settings for
1629  getting the proper dereference string, which is a format string
1630  where {0} represents the field name.
1631 
1632  :param str fld_name: The name of the field which is used to \
1633  dereference a grid property.
1634 
1635  :returns: the dereference string required to access a grid property
1636  in a dl_esm field (e.g. "subdomain%internal%xstart"). The name
1637  must contains a "{0}" which is replaced by the field name.
1638  :rtype: str'''
1639  api_config = Config.get().api_conf("gocean1.0")
1640  deref_name = api_config.grid_properties[self._property_name_property_name].fortran
1641  return deref_name.format(fld_name)
1642 
1643  @property
1644  def argument_type(self):
1645  ''' The type of this argument. We have this for compatibility with
1646  GOKernelArgument objects since, for this class, it will always be
1647  "grid_property". '''
1648  return self._argument_type_argument_type
1649 
1650  @property
1651  def intrinsic_type(self):
1652  '''
1653  :returns: the intrinsic_type of this argument.
1654  :rtype: str
1655 
1656  '''
1657  api_config = Config.get().api_conf("gocean1.0")
1658  return api_config.grid_properties[self._property_name_property_name].intrinsic_type
1659 
1660  @property
1661  def is_scalar(self):
1662  '''
1663  :returns: if this variable is a scalar variable or not.
1664  :rtype: bool
1665  '''
1666  # The constructor guarantees that _property_name is a valid key!
1667  api_config = Config.get().api_conf("gocean1.0")
1668  return api_config.grid_properties[self._property_name_property_name].type \
1669  == "scalar"
1670 
1671  @property
1672  def text(self):
1673  ''' The raw text used to pass data from the algorithm layer
1674  for this argument. Grid properties are not passed from the
1675  algorithm layer so None is returned.'''
1676  return None
1677 
1679  '''
1680  A grid-property argument is read-only and supplied by the
1681  PSy layer so has no dependencies
1682 
1683  :returns: None to indicate no dependencies
1684  :rtype: NoneType
1685  '''
1686  return None
1687 
1689  '''
1690  A grid-property argument is read-only and supplied by the
1691  PSy layer so has no dependencies
1692 
1693  :returns: None to indicate no dependencies
1694  :rtype: NoneType
1695  '''
1696  return None
1697 
1698 
1699 class GOStencil():
1700  '''GOcean 1.0 stencil information for a kernel argument as obtained by
1701  parsing the kernel meta-data. The expected structure of the
1702  metadata and its meaning is provided in the description of the
1703  load method
1704 
1705  '''
1706  def __init__(self):
1707  ''' Set up any internal variables. '''
1708  self._has_stencil_has_stencil = None
1709  self._stencil_stencil = [[0 for _ in range(3)] for _ in range(3)]
1710  self._name_name = None
1711  self._initialised_initialised = False
1712 
1713  # pylint: disable=too-many-branches
1714  def load(self, stencil_info, kernel_name):
1715  '''Take parsed stencil metadata information, check it is valid and
1716  store it in a convenient form. The kernel_name argument is
1717  only used to provide the location if there is an error.
1718 
1719  The stencil information should either be a name which
1720  indicates a particular type of stencil or in the form
1721  stencil(xxx,yyy,zzz) which explicitly specifies a stencil
1722  shape where xxx, yyy and zzz are triplets of integers
1723  indicating whether there is a stencil access in a particular
1724  direction and the depth of that access. For example:
1725 
1726  go_stencil(010, ! N
1727  212, ! W E
1728  010) ! S
1729 
1730  indicates that there is a stencil access of depth 1 in the
1731  "North" and "South" directions and stencil access of depth 2
1732  in the "East" and "West" directions. The value at the centre
1733  of the stencil will not be used by PSyclone but can be 0 or 1
1734  and indicates whether the local field value is accessed.
1735 
1736  The convention is for the associated arrays to be
1737  2-dimensional. If we denote the first dimension as "i" and the
1738  second dimension as "j" then the following directions are
1739  assumed:
1740 
1741 
1742  > j
1743  > ^
1744  > |
1745  > |
1746  > ---->i
1747 
1748  For example a stencil access like:
1749 
1750  a(i,j) + a(i+1,j) + a(i,j-1)
1751 
1752  would be stored as:
1753 
1754  go_stencil(000,
1755  011,
1756  010)
1757 
1758  :param stencil_info: contains the appropriate part of the parser AST
1759  :type stencil_info: :py:class:`psyclone.expression.FunctionVar`
1760  :param string kernel_name: the name of the kernel from where this \
1761  stencil information came from.
1762 
1763  :raises ParseError: if the supplied stencil information is invalid.
1764 
1765  '''
1766  self._initialised_initialised = True
1767 
1768  if not isinstance(stencil_info, expr.FunctionVar):
1769  # the stencil information is not in the expected format
1770  raise ParseError(
1771  f"Meta-data error in kernel '{kernel_name}': 3rd descriptor "
1772  f"(stencil) of field argument is '{stencil_info}' but "
1773  f"expected either a name or the format 'go_stencil(...)'")
1774 
1775  # Get the name
1776  name = stencil_info.name.lower()
1777  const = GOceanConstants()
1778 
1779  if stencil_info.args:
1780  # The stencil info is of the form 'name(a,b,...), so the
1781  # name should be 'stencil' and there should be 3
1782  # arguments'
1783  self._has_stencil_has_stencil = True
1784  args = stencil_info.args
1785  if name != "go_stencil":
1786  raise ParseError(
1787  f"Meta-data error in kernel '{kernel_name}': 3rd "
1788  f"descriptor (stencil) of field argument is '{name}' but "
1789  f"must be 'go_stencil(...)")
1790  if len(args) != 3:
1791  raise ParseError(
1792  f"Meta-data error in kernel '{kernel_name}': 3rd "
1793  f"descriptor (stencil) of field argument with format "
1794  f"'go_stencil(...)', has {len(args)} arguments but should "
1795  f"have 3")
1796  # Each of the 3 args should be of length 3 and each
1797  # character should be a digit from 0-9. Whilst we are
1798  # expecting numbers, the parser represents these numbers
1799  # as strings so we have to perform string manipulation to
1800  # check and that extract them
1801  for arg_idx in range(3):
1802  arg = args[arg_idx]
1803  if not isinstance(arg, str):
1804  raise ParseError(
1805  f"Meta-data error in kernel '{kernel_name}': 3rd "
1806  f"descriptor (stencil) of field argument with format "
1807  f"'go_stencil(...)'. Argument index {arg_idx} should "
1808  f"be a number but found '{arg}'.")
1809  if len(arg) != 3:
1810  raise ParseError(
1811  f"Meta-data error in kernel '{kernel_name}': 3rd "
1812  f"descriptor (stencil) of field argument with format "
1813  f"'go_stencil(...)'. Argument index {arg_idx} should "
1814  f"consist of 3 digits but found {len(arg)}.")
1815  # The central value is constrained to be 0 or 1
1816  if args[1][1] not in ["0", "1"]:
1817  raise ParseError(
1818  f"Meta-data error in kernel '{kernel_name}': 3rd "
1819  f"descriptor (stencil) of field argument with format "
1820  f"'go_stencil(...)'. Argument index 1 position 1 "
1821  f"should be a number from 0-1 but found {args[1][1]}.")
1822  # It is not valid to specify a zero stencil. This is
1823  # indicated by the 'pointwise' name
1824  if args[0] == "000" and \
1825  (args[1] == "000" or args[1] == "010") and \
1826  args[2] == "000":
1827  raise ParseError(
1828  f"Meta-data error in kernel '{kernel_name}': 3rd "
1829  f"descriptor (stencil) of field argument with format "
1830  f"'go_stencil(...)'. A zero sized stencil has been "
1831  f"specified. This should be specified with the "
1832  f"'go_pointwise' keyword.")
1833  # store the values in an internal array as integers in i,j
1834  # order
1835  for idx0 in range(3):
1836  for idx1 in range(3):
1837  # The j coordinate needs to be 'reversed': the first
1838  # row (index 0 in args) is 'top', which should be
1839  # accessed using '+1', and the last row (index 2 in args)
1840  # needs to be accessed using '-1' (see depth()). Using
1841  # 2-idx1 mirrors the rows appropriately.
1842  self._stencil_stencil[idx0][2-idx1] = int(args[idx1][idx0])
1843  else:
1844  # stencil info is of the form 'name' so should be one of
1845  # our valid names
1846  if name not in const.VALID_STENCIL_NAMES:
1847  raise ParseError(
1848  f"Meta-data error in kernel '{kernel_name}': 3rd "
1849  f"descriptor (stencil) of field argument is '{name}' "
1850  f"but must be one of {const.VALID_STENCIL_NAMES} or "
1851  f"go_stencil(...)")
1852  self._name_name = name
1853  # We currently only support one valid name ('pointwise')
1854  # which indicates that there is no stencil
1855  self._has_stencil_has_stencil = False
1856  # Define a 'stencil' for pointwise so that depth() can be used for
1857  # pointwise kernels without handling pointwise as special case
1858  self._stencil_stencil = [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
1859 
1860  def _check_init(self):
1861  '''Internal method which checks that the stencil information has been
1862  loaded.
1863 
1864  :raises GenerationError: if the GOStencil object has not been
1865  initialised i.e. the load() method has not been called
1866 
1867  '''
1868  if not self._initialised_initialised:
1869  raise GenerationError(
1870  "Error in class GOStencil: the object has not yet been "
1871  "initialised. Please ensure the load() method is called.")
1872 
1873  @property
1874  def has_stencil(self):
1875  '''Specifies whether this argument has stencil information or not. The
1876  only case when this is False is when the stencil information
1877  specifies 'pointwise' as this indicates that there is no
1878  stencil access.
1879 
1880  :returns: True if this argument has stencil information and False \
1881  if not.
1882  :rtype: bool
1883 
1884  '''
1885  self._check_init_check_init()
1886  return self._has_stencil_has_stencil
1887 
1888  @property
1889  def name(self):
1890  '''Provides the stencil name if one is provided
1891 
1892  :returns: the name of the type of stencil if this is provided \
1893  and 'None' if not.
1894  :rtype: str
1895 
1896  '''
1897  self._check_init_check_init()
1898  return self._name_name
1899 
1900  def depth(self, index0, index1):
1901  '''Provides the depth of the stencil in the 8 possible stencil
1902  directions in a 2d regular grid (see the description in the
1903  load class for more information). Values must be between -1
1904  and 1 as they are considered to be relative to the centre of
1905  the stencil For example:
1906 
1907  stencil(234,
1908  915,
1909  876)
1910 
1911  returns
1912 
1913  depth(-1,0) = 9
1914  depth(1,1) = 4
1915 
1916  :param int index0: the relative stencil offset for the first \
1917  index of the associated array. This value \
1918  must be between -1 and 1.
1919  :param int index1: the relative stencil offset for the second \
1920  index of the associated array. This value \
1921  must be between -1 and 1
1922 
1923  :returns: the depth of the stencil in the specified direction.
1924  :rtype: int
1925 
1926  :raises GenerationError: if the indices are out-of-bounds.
1927 
1928  '''
1929  self._check_init_check_init()
1930  if index0 < -1 or index0 > 1 or index1 < -1 or index1 > 1:
1931  raise GenerationError(
1932  f"The indices arguments to the depth method in the GOStencil "
1933  f"object must be between -1 and 1 but found "
1934  f"({index0},{index1})")
1935  return self._stencil_stencil[index0+1][index1+1]
1936 
1937 
1939  ''' Description of a GOcean 1.0 kernel argument, as obtained by
1940  parsing the kernel metadata.
1941 
1942  :param str kernel_name: the name of the kernel metadata type \
1943  that contains this metadata.
1944  :param kernel_arg: the relevant part of the parser's AST.
1945  :type kernel_arg: :py:class:`psyclone.expression.FunctionVar`
1946  :param int metadata_index: the postion of this argument in the list of \
1947  arguments specified in the metadata.
1948 
1949  :raises ParseError: if a kernel argument has an invalid grid-point type.
1950  :raises ParseError: for an unrecognised grid property.
1951  :raises ParseError: for an invalid number of arguments.
1952  :raises ParseError: for an invalid access argument.
1953 
1954  '''
1955  def __init__(self, kernel_name, kernel_arg, metadata_index):
1956  # pylint: disable=too-many-locals
1957  nargs = len(kernel_arg.args)
1958  stencil_info = None
1959 
1960  const = GOceanConstants()
1961  if nargs == 3:
1962  # This kernel argument is supplied by the Algorithm layer
1963  # and is either a field or a scalar
1964 
1965  access = kernel_arg.args[0].name
1966  funcspace = kernel_arg.args[1].name
1967  stencil_info = GOStencil()
1968  stencil_info.load(kernel_arg.args[2],
1969  kernel_name)
1970 
1971  # Valid values for the grid-point type that a kernel argument
1972  # may have. (We use the funcspace argument for this as it is
1973  # similar to the space in Finite-Element world.)
1974  valid_func_spaces = const.VALID_FIELD_GRID_TYPES + \
1975  const.VALID_SCALAR_TYPES
1976 
1977  self._grid_prop_grid_prop = ""
1978  if funcspace.lower() in const.VALID_FIELD_GRID_TYPES:
1979  self._argument_type_argument_type_argument_type = "field"
1980  elif funcspace.lower() in const.VALID_SCALAR_TYPES:
1981  self._argument_type_argument_type_argument_type = "scalar"
1982  else:
1983  raise ParseError(f"Meta-data error in kernel {kernel_name}: "
1984  f"argument grid-point type is '{funcspace}' "
1985  f"but must be one of {valid_func_spaces}")
1986 
1987  elif nargs == 2:
1988  # This kernel argument is a property of the grid. The grid
1989  # properties are special because they must be supplied by
1990  # the PSy layer.
1991  access = kernel_arg.args[0].name
1992  grid_var = kernel_arg.args[1].name
1993  funcspace = ""
1994 
1995  self._grid_prop_grid_prop = grid_var
1996  self._argument_type_argument_type_argument_type = "grid_property"
1997  api_config = Config.get().api_conf("gocean1.0")
1998 
1999  if grid_var.lower() not in api_config.grid_properties:
2000  valid_keys = str(api_config.grid_properties.keys())
2001  raise ParseError(
2002  f"Meta-data error in kernel {kernel_name}: un-recognised "
2003  f"grid property '{grid_var}' requested. Must be one of "
2004  f"{valid_keys}")
2005  else:
2006  raise ParseError(
2007  f"Meta-data error in kernel {kernel_name}: 'arg' type "
2008  f"expects 2 or 3 arguments but found '{len(kernel_arg.args)}' "
2009  f"in '{kernel_arg.args}'")
2010 
2011  api_config = Config.get().api_conf("gocean1.0")
2012  access_mapping = api_config.get_access_mapping()
2013  try:
2014  access_type = access_mapping[access]
2015  except KeyError as err:
2016  valid_names = api_config.get_valid_accesses_api()
2017  raise ParseError(
2018  f"Meta-data error in kernel {kernel_name}: argument access is "
2019  f"given as '{access}' but must be one of {valid_names}"
2020  ) from err
2021 
2022  # Finally we can call the __init__ method of our base class
2023  super().__init__(access_type, funcspace, metadata_index,
2024  stencil=stencil_info,
2025  argument_type=self._argument_type_argument_type_argument_type)
2026 
2027  def __str__(self):
2028  return repr(self)
2029 
2030  @property
2031  def grid_prop(self):
2032  '''
2033  :returns: the name of the grid-property that this argument is to \
2034  supply to the kernel.
2035  :rtype: str
2036 
2037  '''
2038  return self._grid_prop_grid_prop
2039 
2040 
2042  ''' Description of a kernel including the grid index-offset it
2043  expects and the region of the grid that it expects to
2044  operate upon '''
2045 
2046  def __str__(self):
2047  return ('GOcean 1.0 kernel ' + self._name_name + ', index-offset = ' +
2048  self._index_offset_index_offset + ', iterates-over = ' +
2049  self._iterates_over_iterates_over)
2050 
2051  def __init__(self, ast, name=None):
2052  # Initialise the base class
2053  KernelType.__init__(self, ast, name=name)
2054 
2055  # What grid offset scheme this kernel expects
2056  self._index_offset_index_offset = self._ktype_ktype.get_variable('index_offset').init
2057 
2058  const = GOceanConstants()
2059  if self._index_offset_index_offset is None:
2060  raise ParseError(f"Meta-data error in kernel {name}: an "
2061  f"INDEX_OFFSET must be specified and must be "
2062  f"one of {const.VALID_OFFSET_NAMES}")
2063 
2064  if self._index_offset_index_offset.lower() not in const.VALID_OFFSET_NAMES:
2065  raise ParseError(f"Meta-data error in kernel {name}: "
2066  f"INDEX_OFFSET has value '{self._index_offset}'"
2067  f" but must be one of {const.VALID_OFFSET_NAMES}")
2068 
2069  const = GOceanConstants()
2070  # Check that the meta-data for this kernel is valid
2071  if self._iterates_over_iterates_over is None:
2072  raise ParseError(f"Meta-data error in kernel {name}: "
2073  f"ITERATES_OVER is missing. (Valid values are: "
2074  f"{const.VALID_ITERATES_OVER})")
2075 
2076  if self._iterates_over_iterates_over.lower() not in const.VALID_ITERATES_OVER:
2077  raise ParseError(f"Meta-data error in kernel {name}: "
2078  f"ITERATES_OVER has value '"
2079  f"{self._iterates_over.lower()}' but must be "
2080  f"one of {const.VALID_ITERATES_OVER}")
2081 
2082  # The list of kernel arguments
2083  self._arg_descriptors_arg_descriptors_arg_descriptors = []
2084  have_grid_prop = False
2085  for idx, init in enumerate(self._inits_inits):
2086  if init.name != 'go_arg':
2087  raise ParseError(f"Each meta_arg value must be of type "
2088  f"'go_arg' for the gocean1.0 api, but "
2089  f"found '{init.name}'")
2090  # Pass in the name of this kernel for the purposes
2091  # of error reporting
2092  new_arg = GO1p0Descriptor(name, init, idx)
2093  # Keep track of whether this kernel requires any
2094  # grid properties
2095  have_grid_prop = (have_grid_prop or
2096  (new_arg.argument_type == "grid_property"))
2097  self._arg_descriptors_arg_descriptors_arg_descriptors.append(new_arg)
2098 
2099  # If this kernel expects a grid property then check that it
2100  # has at least one field object as an argument (which we
2101  # can use to access the grid)
2102  if have_grid_prop:
2103  have_fld = False
2104  for arg in self.arg_descriptorsarg_descriptors:
2105  if arg.argument_type == "field":
2106  have_fld = True
2107  break
2108  if not have_fld:
2109  raise ParseError(
2110  f"Kernel {name} requires a property of the grid but does "
2111  f"not have any field objects as arguments.")
2112 
2113  # Override nargs from the base class so that it returns the no.
2114  # of args specified in the algorithm layer (and thus excludes those
2115  # that must be added in the PSy layer). This is done to simplify the
2116  # check on the no. of arguments supplied in any invoke of the kernel.
2117  @property
2118  def nargs(self):
2119  ''' Count and return the number of arguments that this kernel
2120  expects the Algorithm layer to provide '''
2121  count = 0
2122  for arg in self.arg_descriptorsarg_descriptors:
2123  if arg.argument_type != "grid_property":
2124  count += 1
2125  return count
2126 
2127  @property
2128  def index_offset(self):
2129  ''' Return the grid index-offset that this kernel expects '''
2130  return self._index_offset_index_offset
2131 
2132 
2133 class GOACCEnterDataDirective(ACCEnterDataDirective):
2134  '''
2135  Sub-classes ACCEnterDataDirective to provide the dl_esm_inf infrastructure-
2136  specific interfaces to flag and update when data is on a device.
2137 
2138  '''
2139  def _read_from_device_routine(self):
2140  ''' Return the symbol of the routine that reads data from the OpenACC
2141  device, if it doesn't exist create the Routine and the Symbol.
2142 
2143  :returns: the symbol representing the read_from_device routine.
2144  :rtype: :py:class:`psyclone.psyir.symbols.symbol`
2145  '''
2146  symtab = self.root.symbol_table
2147  try:
2148  return symtab.lookup_with_tag("openacc_read_func")
2149  except KeyError:
2150  # If the subroutines does not exist, it needs to be
2151  # generated first.
2152  pass
2153 
2154  # Create the symbol for the routine and add it to the symbol table.
2155  subroutine_name = symtab.new_symbol(
2156  "read_from_device", symbol_type=RoutineSymbol,
2157  tag="openacc_read_func").name
2158 
2159  code = '''
2160  subroutine read_openacc(from, to, startx, starty, nx, ny, blocking)
2161  use iso_c_binding, only: c_ptr
2162  use kind_params_mod, only: go_wp
2163  type(c_ptr), intent(in) :: from
2164  real(go_wp), dimension(:,:), intent(inout), target :: to
2165  integer, intent(in) :: startx, starty, nx, ny
2166  logical, intent(in) :: blocking
2167  end subroutine read_openacc
2168  '''
2169 
2170  # Obtain the PSyIR representation of the code above
2171  fortran_reader = FortranReader()
2172  container = fortran_reader.psyir_from_source(code)
2173  subroutine = container.children[0]
2174  # Add an ACCUpdateDirective inside the subroutine
2175  subroutine.addchild(ACCUpdateDirective([Signature("to")], "host",
2176  if_present=False))
2177 
2178  # Rename subroutine
2179  subroutine.name = subroutine_name
2180 
2181  # Insert the routine as a child of the ancestor Container
2182  if not self.ancestor(Container):
2183  raise GenerationError(
2184  f"The GOACCEnterDataDirective can only be generated/lowered "
2185  f"inside a Container in order to insert a sibling "
2186  f"subroutine, but '{self}' is not inside a Container.")
2187  self.ancestor(Container).addchild(subroutine.detach())
2188 
2189  return symtab.lookup_with_tag("openacc_read_func")
2190 
2192  '''
2193  In-place replacement of DSL or high-level concepts into generic PSyIR
2194  constructs. In addition to calling this method in the base class, the
2195  GOACCEnterDataDirective sets up the 'data_on_device' flag for
2196  each of the fields accessed.
2197 
2198  :returns: the lowered version of this node.
2199  :rtype: :py:class:`psyclone.psyir.node.Node`
2200 
2201  '''
2202  self._acc_dirs_acc_dirs = self.ancestor(InvokeSchedule).walk(
2203  (ACCParallelDirective, ACCKernelsDirective))
2204  obj_list = []
2205  for pdir in self._acc_dirs_acc_dirs:
2206  for var in pdir.fields:
2207  if var not in obj_list:
2208  obj_list.append(var)
2209 
2210  read_routine_symbol = self._read_from_device_routine_read_from_device_routine()
2211 
2212  for var in obj_list:
2213  symbol = self.scope.symbol_table.lookup(var)
2214  assignment = Assignment.create(
2215  StructureReference.create(symbol, ['data_on_device']),
2216  Literal("true", BOOLEAN_TYPE))
2217  self.parent.children.insert(self.position, assignment)
2218 
2219  # Use a CodeBlock to encode a Fortran pointer assignment
2220  reader = FortranReader()
2221  codeblock = reader.psyir_from_statement(
2222  f"{symbol.name}%read_from_device_f => "
2223  f"{read_routine_symbol.name}\n",
2224  self.scope.symbol_table)
2225 
2226  self.parent.children.insert(self.position, codeblock)
2227 
2228  return super().lower_to_language_level()
2229 
2230 
2231 class GOKernelSchedule(KernelSchedule):
2232  '''
2233  Sub-classes KernelSchedule to provide a GOcean-specific implementation.
2234 
2235  :param str name: Kernel subroutine name
2236  '''
2237  # Polymorphic parameter to initialize the Symbol Table of the Schedule
2238  _symbol_table_class = GOSymbolTable
2239 
2240 
2242  '''GOcean specific halo exchange class which can be added to and
2243  manipulated in a schedule.
2244 
2245  :param field: the field that this halo exchange will act on.
2246  :type field: :py:class:`psyclone.gocean1p0.GOKernelArgument`
2247  :param bool check_dirty: optional argument default False (contrary to \
2248  its generic class - revisit in #856) indicating whether this halo \
2249  exchange should be subject to a run-time check for clean/dirty halos.
2250  :param parent: optional PSyIR parent node (default None) of this object.
2251  :type parent: :py:class:`psyclone.psyir.nodes.Node`
2252  '''
2253  def __init__(self, field, check_dirty=False, parent=None):
2254  super().__init__(field, check_dirty=check_dirty, parent=parent)
2255 
2256  # Name of the HaloExchange method in the GOcean infrastructure.
2257  self._halo_exchange_name_halo_exchange_name = "halo_exchange"
2258 
2260  '''
2261  In-place replacement of DSL or high-level concepts into generic
2262  PSyIR constructs. A GOHaloExchange is replaced by a call to the
2263  appropriate library method.
2264 
2265  :returns: the lowered version of this node.
2266  :rtype: :py:class:`psyclone.psyir.node.Node`
2267 
2268  '''
2269  # TODO 856: Wrap Halo call with an is_dirty flag when necessary.
2270 
2271  # TODO 886: Currently only stencils of depth 1 are accepted by this
2272  # API, so the HaloExchange is hardcoded to depth 1.
2273 
2274  # Call the halo_exchange routine with depth argument to 1
2275  # Currently we create an symbol name with % as a workaround of not
2276  # having type bound routines.
2277  rsymbol = RoutineSymbol(self.fieldfield.name + "%" +
2278  self._halo_exchange_name_halo_exchange_name)
2279  call_node = Call.create(rsymbol, [Literal("1", INTEGER_TYPE)])
2280  self.replace_with(call_node)
2281  return call_node
2282 
2283 
2284 # For Sphinx AutoAPI documentation generation
2285 __all__ = ['GOPSy', 'GOInvokes', 'GOInvoke', 'GOInvokeSchedule', 'GOLoop',
2286  'GOBuiltInCallFactory', 'GOKernCallFactory', 'GOKern',
2287  'GOKernelArguments', 'GOKernelArgument',
2288  'GOKernelGridArgument', 'GOStencil', 'GO1p0Descriptor',
2289  'GOKernelType1p0', 'GOACCEnterDataDirective',
2290  'GOKernelSchedule', 'GOHaloExchange']
def field_space(self, my_field_space)
Definition: psyloop.py:179
def gen_code(self, parent)
Definition: gocean1p0.py:255
def gen_code(self, parent)
Definition: gocean1p0.py:169
def create(call, parent=None)
Definition: gocean1p0.py:1026
def reference_accesses(self, var_accesses)
Definition: gocean1p0.py:1170
def _record_stencil_accesses(self, signature, arg, var_accesses)
Definition: gocean1p0.py:1125
def append(self, name, argument_type)
Definition: gocean1p0.py:1407
def create(parent, loop_type, field_name="", field_space="", iteration_space="", index_offset="")
Definition: gocean1p0.py:403
def get_custom_bound_string(self, side)
Definition: gocean1p0.py:795
def iteration_space(self, it_space)
Definition: gocean1p0.py:482
def add_bounds(bound_info)
Definition: gocean1p0.py:696
def _grid_property_psyir_expression(self, grid_property)
Definition: gocean1p0.py:856
def field_space(self, my_field_space)
Definition: gocean1p0.py:443
def create_halo_exchanges(self)
Definition: gocean1p0.py:576
def gen_code(self, parent)
Definition: gocean1p0.py:992
def independent_iterations(self, test_all_variables=False, signatures_to_ignore=None, dep_tools=None)
Definition: gocean1p0.py:520
def _add_halo_exchange(self, halo_field)
Definition: gocean1p0.py:602
def depth(self, index0, index1)
Definition: gocean1p0.py:1900
def load(self, stencil_info, kernel_name)
Definition: gocean1p0.py:1714
def _complete_init(self, arg_info)
Definition: psyGen.py:2183
def argument_type(self)
Definition: psyGen.py:2298
def psy_unique_var_names(self)
Definition: psyGen.py:501
def schedule(self, obj)
Definition: psyGen.py:512
def schedule(self)
Definition: psyGen.py:508
def name(self, value)
Definition: psyGen.py:1321
def name(self)
Definition: psyGen.py:1313
def arguments(self)
Definition: psyGen.py:1309
def name(self)
Definition: psyGen.py:282
def invokes(self)
Definition: psyGen.py:275
def container(self)
Definition: psyGen.py:264