From c9b487dd544de087e791e8f39ed31b541f2b94e2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 14:43:26 +0000 Subject: [PATCH 01/15] #2381 sketch out new classes [skip ci] --- .../common/psylayer/global_reduction.py | 6 ++ .../domain/common/psylayer/global_sum.py | 73 ++++++++++++++++++ src/psyclone/domain/lfric/lfric_global_min.py | 75 ++++++++++++++++++ src/psyclone/domain/lfric/lfric_global_sum.py | 66 ++++++++++++++++ src/psyclone/domain/lfric/lfric_invoke.py | 8 +- src/psyclone/lfric.py | 76 +------------------ src/psyclone/psyGen.py | 59 -------------- 7 files changed, 227 insertions(+), 136 deletions(-) create mode 100644 src/psyclone/domain/common/psylayer/global_reduction.py create mode 100644 src/psyclone/domain/common/psylayer/global_sum.py create mode 100644 src/psyclone/domain/lfric/lfric_global_min.py create mode 100644 src/psyclone/domain/lfric/lfric_global_sum.py diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py new file mode 100644 index 0000000000..72a35352c0 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -0,0 +1,6 @@ +from enum import Enum + +class GlobalReduction(): + ''' + ''' + class Reduction(Enum) diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py new file mode 100644 index 0000000000..61301ddcea --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -0,0 +1,73 @@ +from psyclone.domain.common.psylayer import GlobalReduction + + +class GlobalSum(GlobalReduction): + ''' + Generic Global Sum class which can be added to and manipulated + in, a schedule. + + :param scalar: the scalar that the global sum is stored into + :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` + :param parent: optional parent (default None) of this object + :type parent: :py:class:`psyclone.psyir.nodes.Node` + + ''' + # Textual description of the node. + _children_valid_format = "" + _text_name = "GlobalSum" + _colour = "cyan" + + def __init__(self, scalar, parent=None): + # Check that distributed memory is enabled + if not Config.get().distributed_memory: + raise GenerationError( + f"It makes no sense to create a {self._text_name} object " + f"when distributed memory is not enabled (dm=False).") + # Check that the global sum argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"{self._text_name}.init(): A global sum argument should be a " + f"scalar but found argument of type '{scalar.argument_type}'.") + + Node.__init__(self, children=[], parent=parent) + import copy + self._scalar = copy.copy(scalar) + if scalar: + # Update scalar values appropriately + # Here "readwrite" denotes how the class GlobalSum + # accesses/updates a scalar + self._scalar.access = AccessType.READWRITE + self._scalar.call = self + + @property + def scalar(self): + ''' Return the scalar field that this global sum acts on ''' + return self._scalar + + @property + def dag_name(self): + ''' + :returns: the name to use in the DAG for this node. + :rtype: str + ''' + return f"globalsum({self._scalar.name})_{self.position}" + + @property + def args(self): + ''' Return the list of arguments associated with this node. Override + the base method and simply return our argument.''' + return [self._scalar] + + def node_str(self, colour=True): + ''' + Returns a text description of this node with (optional) control codes + to generate coloured output in a terminal that supports it. + + :param bool colour: whether or not to include colour control codes. + + :returns: description of this node, possibly coloured. + :rtype: str + ''' + return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" + + diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py new file mode 100644 index 0000000000..17156a734c --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -0,0 +1,75 @@ +from psyclone.configuration import Config +from psyclone.domain.common.psylayer import GlobalReduction + + +class LFRicGlobalMin(GlobalReduction): + ''' + LFRic specific global min class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global min. + :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` + :param parent: the parent node of this node in the PSyIR. + :type parent: :py:class:`psyclone.psyir.nodes.Node` + + :raises GenerationError: if distributed memory is not enabled. + :raises InternalError: if the supplied argument is not a scalar. + :raises GenerationError: if the scalar is not of "real" intrinsic type. + + ''' + def __init__(self, scalar, parent=None): + # Check that distributed memory is enabled + if not Config.get().distributed_memory: + raise GenerationError( + "It makes no sense to create an LFRicGlobalSum object when " + "distributed memory is not enabled (dm=False).") + # Check that the global sum argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"LFRicGlobalSum.init(): A global sum argument should be a " + f"scalar but found argument of type '{scalar.argument_type}'.") + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalSum currently only supports real scalars, but " + f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + # Initialise the parent class + super().__init__(scalar, parent=parent) + + def lower_to_language_level(self): + ''' + :returns: this node lowered to language-level PSyIR. + :rtype: :py:class:`psyclone.psyir.nodes.Node` + ''' + + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + sum_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + sum_name = symtab.find_or_create_tag("global_sum", + symbol_type=DataSymbol, + datatype=sum_type) + tmp_var = symtab.lookup(name) + + # Create the assignments + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Perform global sum" + self.parent.addchild(assign1, self.position) + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py new file mode 100644 index 0000000000..2db8a1901e --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -0,0 +1,66 @@ +from psyclone.configuration import Config +from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.psyir.nodes.node import Node + + +class LFRicGlobalSum(GlobalReduction): + ''' + LFRic specific global sum class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global sum. + :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` + :param parent: the parent node of this node in the PSyIR. + :type parent: :py:class:`psyclone.psyir.nodes.Node` + + :raises GenerationError: if distributed memory is not enabled. + :raises InternalError: if the supplied argument is not a scalar. + :raises GenerationError: if the scalar is not of "real" intrinsic type. + + ''' + def __init__(self, scalar, parent=None): + super.__init__(scalar, parent=parent) + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalSum currently only supports real scalars, but " + f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + # Initialise the parent class + super().__init__(scalar, parent=parent) + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + ''' + + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + sum_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + sum_name = symtab.find_or_create_tag("global_sum", + symbol_type=DataSymbol, + datatype=sum_type) + tmp_var = symtab.lookup(name) + + # Create the assignments + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Perform global sum" + self.parent.addchild(assign1, self.position) + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index 8da97a72ef..bcfaf067c4 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -85,14 +85,15 @@ def __init__(self, alg_invocation, idx, invokes): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel - from psyclone.lfric import (LFRicFunctionSpaces, LFRicGlobalSum, + from psyclone.lfric import (LFRicFunctionSpaces, LFRicLMAOperators, LFRicReferenceElement, LFRicCMAOperators, LFRicBasisFunctions, LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( - LFRicCellIterators, LFRicHaloDepths, LFRicLoopBounds, + LFRicCellIterators, LFRicGlobalSum, LFRicGlobalMin, + LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicScalarArrayArgs, LFRicFields, LFRicDofmaps, LFRicStencils) @@ -190,6 +191,9 @@ def __init__(self, alg_invocation, idx, invokes): if kern.reduction_type == LFRicBuiltIn.ReductionType.SUM: global_red = LFRicGlobalSum(kern.reduction_arg, parent=loop.parent) + elif kern.reduction_type == LFRicBuiltIn.ReductionType.MIN: + global_red = LFRicGlobalMin(kern.reduction_arg, + parent=loop.parent) else: raise GenerationError( f"TODO #2381 - currently only global *sum* " diff --git a/src/psyclone/lfric.py b/src/psyclone/lfric.py index 89d2b0c842..d1bfe2531b 100644 --- a/src/psyclone/lfric.py +++ b/src/psyclone/lfric.py @@ -63,7 +63,7 @@ from psyclone.parse.kernel import getkerneldescriptors from psyclone.parse.utils import ParseError from psyclone.psyGen import (Arguments, DataAccess, InvokeSchedule, Kern, - KernelArgument, HaloExchange, GlobalSum) + KernelArgument, HaloExchange) from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import ( Reference, ACCEnterDataDirective, ArrayOfStructuresReference, @@ -3894,79 +3894,6 @@ def initialise(self, cursor): return cursor -class LFRicGlobalSum(GlobalSum): - ''' - LFRic specific global sum class which can be added to and - manipulated in a schedule. - - :param scalar: the kernel argument for which to perform a global sum. - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` - :param parent: the parent node of this node in the PSyIR. - :type parent: :py:class:`psyclone.psyir.nodes.Node` - - :raises GenerationError: if distributed memory is not enabled. - :raises InternalError: if the supplied argument is not a scalar. - :raises GenerationError: if the scalar is not of "real" intrinsic type. - - ''' - def __init__(self, scalar, parent=None): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - "It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False).") - # Check that the global sum argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"LFRicGlobalSum.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") - # Check scalar intrinsic types that this class supports (only - # "real" for now) - if scalar.intrinsic_type != "real": - raise GenerationError( - f"LFRicGlobalSum currently only supports real scalars, but " - f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " - f"'{scalar.intrinsic_type}' intrinsic type.") - # Initialise the parent class - super().__init__(scalar, parent=parent) - - def lower_to_language_level(self): - ''' - :returns: this node lowered to language-level PSyIR. - :rtype: :py:class:`psyclone.psyir.nodes.Node` - ''' - - # Get the name strings to use - name = self._scalar.name - type_name = self._scalar.data_type - mod_name = self._scalar.module_name - - # Get the symbols from the given names - symtab = self.ancestor(InvokeSchedule).symbol_table - sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) - sum_type = symtab.find_or_create(type_name, - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(sum_mod)) - sum_name = symtab.find_or_create_tag("global_sum", - symbol_type=DataSymbol, - datatype=sum_type) - tmp_var = symtab.lookup(name) - - # Create the assignments - assign1 = Assignment.create( - lhs=StructureReference.create(sum_name, ["value"]), - rhs=Reference(tmp_var) - ) - assign1.preceding_comment = "Perform global sum" - self.parent.addchild(assign1, self.position) - assign2 = Assignment.create( - lhs=Reference(tmp_var), - rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) - ) - return self.replace_with(assign2) - - def _create_depth_list(halo_info_list, parent): '''Halo exchanges may have more than one dependency. This method simplifies multiple dependencies to remove duplicates and any @@ -6603,7 +6530,6 @@ def data_on_device(self, _): 'LFRicInterGrid', 'LFRicBasisFunctions', 'LFRicBoundaryConditions', - 'LFRicGlobalSum', 'LFRicHaloExchange', 'LFRicHaloExchangeStart', 'LFRicHaloExchangeEnd', diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index dd7ed4f505..f3ca7ee0d6 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -693,65 +693,6 @@ def __str__(self): return result -class GlobalSum(Statement): - ''' - Generic Global Sum class which can be added to and manipulated - in, a schedule. - - :param scalar: the scalar that the global sum is stored into - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` - :param parent: optional parent (default None) of this object - :type parent: :py:class:`psyclone.psyir.nodes.Node` - - ''' - # Textual description of the node. - _children_valid_format = "" - _text_name = "GlobalSum" - _colour = "cyan" - - def __init__(self, scalar, parent=None): - Node.__init__(self, children=[], parent=parent) - import copy - self._scalar = copy.copy(scalar) - if scalar: - # Update scalar values appropriately - # Here "readwrite" denotes how the class GlobalSum - # accesses/updates a scalar - self._scalar.access = AccessType.READWRITE - self._scalar.call = self - - @property - def scalar(self): - ''' Return the scalar field that this global sum acts on ''' - return self._scalar - - @property - def dag_name(self): - ''' - :returns: the name to use in the DAG for this node. - :rtype: str - ''' - return f"globalsum({self._scalar.name})_{self.position}" - - @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' - return [self._scalar] - - def node_str(self, colour=True): - ''' - Returns a text description of this node with (optional) control codes - to generate coloured output in a terminal that supports it. - - :param bool colour: whether or not to include colour control codes. - - :returns: description of this node, possibly coloured. - :rtype: str - ''' - return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" - - class HaloExchange(Statement): ''' Generic Halo Exchange class which can be added to and From 0ea6d71427328b37cb6fd1c65084eea883549ed8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 15:18:34 +0000 Subject: [PATCH 02/15] #2381 fix imports and mv common class attributes --- .../common/psylayer/global_reduction.py | 27 ++++++++-- .../domain/common/psylayer/global_sum.py | 23 +++----- src/psyclone/domain/lfric/lfric_global_min.py | 8 ++- src/psyclone/domain/lfric/lfric_global_sum.py | 8 ++- src/psyclone/psyGen.py | 54 +++++++++++-------- 5 files changed, 77 insertions(+), 43 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 72a35352c0..8290b447d8 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -1,6 +1,27 @@ -from enum import Enum +from psyclone.configuration import Config +from psyclone.errors import GenerationError +from psyclone.psyir.nodes import Statement +from psyclone.psyir.nodes.node import Node -class GlobalReduction(): + +class GlobalReduction(Statement): ''' + Represents a global-reduction in the PSyIR. + + :raises GenerationError: if distributed memory is not enabled. + ''' - class Reduction(Enum) + # TODO is this really a leaf - it could have operands as children? + #: Textual description of the node. + _children_valid_format = "" + _text_name = "GlobalReduction" + #: The colour to use when creating a view of this node. + _colour = "cyan" + + def __init__(self, parent: Node = None): + super().__init__(children=[], parent=parent) + # Check that distributed memory is enabled + if not Config.get().distributed_memory: + raise GenerationError( + f"It makes no sense to create a {self._text_name} object " + f"when distributed memory is not enabled (dm=False).") diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py index 61301ddcea..3e9dbdf764 100644 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -1,35 +1,28 @@ +from psyclone.core import AccessType from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.errors import InternalError +from psyclone.psyGen import KernelArgument +from psyclone.psyir.nodes.node import Node class GlobalSum(GlobalReduction): ''' - Generic Global Sum class which can be added to and manipulated - in, a schedule. + Generic GlobalSum class which can be added to a Schedule. :param scalar: the scalar that the global sum is stored into - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` :param parent: optional parent (default None) of this object - :type parent: :py:class:`psyclone.psyir.nodes.Node` ''' - # Textual description of the node. - _children_valid_format = "" _text_name = "GlobalSum" - _colour = "cyan" - def __init__(self, scalar, parent=None): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - f"It makes no sense to create a {self._text_name} object " - f"when distributed memory is not enabled (dm=False).") + def __init__(self, scalar: KernelArgument, parent: Node = None): + super().__init__(parent=parent) # Check that the global sum argument is indeed a scalar if not scalar.is_scalar: raise InternalError( f"{self._text_name}.init(): A global sum argument should be a " f"scalar but found argument of type '{scalar.argument_type}'.") - Node.__init__(self, children=[], parent=parent) import copy self._scalar = copy.copy(scalar) if scalar: @@ -69,5 +62,3 @@ def node_str(self, colour=True): :rtype: str ''' return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" - - diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 17156a734c..46e8b36284 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -1,5 +1,12 @@ from psyclone.configuration import Config from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import ( + Assignment, Call, Reference, StructureReference) +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) class LFRicGlobalMin(GlobalReduction): @@ -12,7 +19,6 @@ class LFRicGlobalMin(GlobalReduction): :param parent: the parent node of this node in the PSyIR. :type parent: :py:class:`psyclone.psyir.nodes.Node` - :raises GenerationError: if distributed memory is not enabled. :raises InternalError: if the supplied argument is not a scalar. :raises GenerationError: if the scalar is not of "real" intrinsic type. diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 2db8a1901e..3d2a5f815f 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,6 +1,12 @@ -from psyclone.configuration import Config from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.errors import GenerationError +from psyclone.psyGen import InvokeSchedule +from psyclone.psyir.nodes import ( + Assignment, Call, Reference, StructureReference) from psyclone.psyir.nodes.node import Node +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) class LFRicGlobalSum(GlobalReduction): diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index f3ca7ee0d6..dea18e81da 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1779,12 +1779,12 @@ class DataAccess(): def __init__(self, arg): '''Store the argument associated with the instance of this class and - the Call, HaloExchange or GlobalSum (or a subclass thereof) + the Call, HaloExchange or GlobalReduction (or a subclass thereof) instance with which the argument is associated. - :param arg: the argument that we are concerned with. An \ - argument can be found in a `Kern` a `HaloExchange` or a \ - `GlobalSum` (or a subclass thereof) + :param arg: the argument that we are concerned with. An + argument can be found in a `Kern` a `HaloExchange` or a + `GlobalReduction` (or a subclass thereof) :type arg: :py:class:`psyclone.psyGen.Argument` ''' @@ -2158,9 +2158,9 @@ def call(self, value): def backward_dependence(self): '''Returns the preceding argument that this argument has a direct dependence with, or None if there is not one. The argument may - exist in a call, a haloexchange, or a globalsum. + exist in a call, a haloexchange, or a GlobalReduction. - :returns: the first preceding argument that has a dependence \ + :returns: the first preceding argument that has a dependence on this argument. :rtype: :py:class:`psyclone.psyGen.Argument` @@ -2171,14 +2171,14 @@ def backward_dependence(self): def forward_write_dependencies(self, ignore_halos=False): '''Returns a list of following write arguments that this argument has dependencies with. The arguments may exist in a call, a - haloexchange (unless `ignore_halos` is `True`), or a globalsum. If - none are found then return an empty list. If self is not a + haloexchange (unless `ignore_halos` is `True`), or a GlobalReduction. + If none are found then return an empty list. If self is not a reader then return an empty list. - :param bool ignore_halos: if `True` then any write dependencies \ + :param bool ignore_halos: if `True` then any write dependencies involving a halo exchange are ignored. Defaults to `False`. - :returns: a list of arguments that have a following write \ + :returns: a list of arguments that have a following write dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2190,15 +2190,15 @@ def forward_write_dependencies(self, ignore_halos=False): def backward_write_dependencies(self, ignore_halos=False): '''Returns a list of previous write arguments that this argument has dependencies with. The arguments may exist in a call, a - haloexchange (unless `ignore_halos` is `True`), or a globalsum. If - none are found then return an empty list. If self is not a + haloexchange (unless `ignore_halos` is `True`), or a GlobalReduction. + If none are found then return an empty list. If self is not a reader then return an empty list. - :param ignore_halos: if `True` then any write dependencies \ + :param ignore_halos: if `True` then any write dependencies involving a halo exchange are ignored. Defaults to `False`. :type ignore_halos: bool - :returns: a list of arguments that have a preceding write \ + :returns: a list of arguments that have a preceding write dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2210,9 +2210,9 @@ def backward_write_dependencies(self, ignore_halos=False): def forward_dependence(self): '''Returns the following argument that this argument has a direct dependence on, or `None` if there is not one. The argument may - exist in a call, a haloexchange, or a globalsum. + exist in a call, a haloexchange, or a GlobalReduction. - :returns: the first following argument that has a dependence \ + :returns: the first following argument that has a dependence on this argument. :rtype: :py:class:`psyclone.psyGen.Argument` @@ -2223,11 +2223,11 @@ def forward_dependence(self): def forward_read_dependencies(self): '''Returns a list of following read arguments that this argument has dependencies with. The arguments may exist in a call, a - haloexchange, or a globalsum. If none are found then + haloexchange, or a GlobalReduction. If none are found then return an empty list. If self is not a writer then return an empty list. - :returns: a list of following arguments that have a read \ + :returns: a list of following arguments that have a read dependence on this argument. :rtype: list of :py:class:`psyclone.psyGen.Argument` @@ -2246,8 +2246,11 @@ def _find_argument(self, nodes): :rtype: :py:class:`psyclone.psyGen.Argument` ''' + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer import GlobalReduction nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, HaloExchange, GlobalSum))] + isinstance(x, (Kern, HaloExchange, + GlobalReduction))] for node in nodes_with_args: for argument in node.args: if self._depends_on(argument): @@ -2271,9 +2274,13 @@ def _find_read_arguments(self, nodes): # I am not a writer so there will be no read dependencies return [] + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer import GlobalReduction + # We only need consider nodes that have arguments nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, HaloExchange, GlobalSum))] + isinstance(x, (Kern, HaloExchange, + GlobalReduction))] access = DataAccess(self) arguments = [] for node in nodes_with_args: @@ -2312,9 +2319,12 @@ def _find_write_arguments(self, nodes, ignore_halos=False): # I am not a reader so there will be no write dependencies return [] + # pylint: disable=import-outside-toplevel + from psyclone.domain.common.psylayer import GlobalReduction + # We only need consider nodes that have arguments nodes_with_args = [x for x in nodes if - isinstance(x, (Kern, GlobalSum)) or + isinstance(x, (Kern, GlobalReduction)) or (isinstance(x, HaloExchange) and not ignore_halos)] access = DataAccess(self) arguments = [] @@ -2776,6 +2786,6 @@ def validate_options(self, **kwargs): # For Sphinx AutoAPI documentation generation __all__ = ['PSyFactory', 'PSy', 'Invokes', 'Invoke', 'InvokeSchedule', - 'GlobalSum', 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', + 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern', 'BuiltIn', 'Arguments', 'DataAccess', 'Argument', 'KernelArgument', 'TransInfo', 'Transformation'] From 5f3f940f3c60594616eaf5cc7ea94f9decdfb2ca Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 15:47:59 +0000 Subject: [PATCH 03/15] #2381 update imports --- .../domain/common/psylayer/__init__.py | 4 +- .../domain/common/psylayer/global_min.py | 53 +++++++++++++++++++ .../common/psylayer/global_reduction.py | 11 ++++ .../domain/common/psylayer/global_sum.py | 13 +++-- src/psyclone/domain/lfric/__init__.py | 33 +----------- src/psyclone/domain/lfric/lfric_global_min.py | 26 +++------ src/psyclone/domain/lfric/lfric_global_sum.py | 9 ++-- .../psyir/transformations/extract_trans.py | 3 +- src/psyclone/tests/psyGen_test.py | 9 ++-- 9 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 src/psyclone/domain/common/psylayer/global_min.py diff --git a/src/psyclone/domain/common/psylayer/__init__.py b/src/psyclone/domain/common/psylayer/__init__.py index d581422170..94837f77e4 100644 --- a/src/psyclone/domain/common/psylayer/__init__.py +++ b/src/psyclone/domain/common/psylayer/__init__.py @@ -35,6 +35,8 @@ '''A package module for psyclone.domain.common.psylayer''' +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.domain.common.psylayer.global_min import GlobalMin +from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.domain.common.psylayer.psyloop import PSyLoop -__all__ = ["PSyLoop"] diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py new file mode 100644 index 0000000000..fb0a34210e --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -0,0 +1,53 @@ +import copy + +from psyclone.core import AccessType +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.errors import InternalError +from psyclone.psyGen import KernelArgument +from psyclone.psyir.nodes.node import Node + + +class GlobalMin(GlobalReduction): + ''' + Generic GlobalMin class which can be added to a Schedule. + + :param scalar: the scalar that the global mimimum is computed for and + the result stored into. + :param parent: optional parent (default None) of this object + + ''' + _text_name = "GlobalMin" + + def __init__(self, scalar: KernelArgument, parent: Node = None): + super().__init__(parent=parent) + # Check that the argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"{self._text_name}.init(): A global min argument should be a " + f"scalar but found argument of type '{scalar.argument_type}'.") + + self._scalar = copy.copy(scalar) + if scalar: + # Update scalar values appropriately + # Here "readwrite" denotes how the class GlobalMin + # accesses/updates a scalar + self._scalar.access = AccessType.READWRITE + self._scalar.call = self + + @property + def scalar(self): + ''' Return the scalar quantity that this global min acts on ''' + return self._scalar + + @property + def dag_name(self) -> str: + ''' + :returns: the name to use in the DAG for this node. + ''' + return f"globalmin({self._scalar.name})_{self.position}" + + @property + def args(self): + ''' Return the list of arguments associated with this node. Override + the base method and simply return our argument.''' + return [self._scalar] diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 8290b447d8..fb03065cf7 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -25,3 +25,14 @@ def __init__(self, parent: Node = None): raise GenerationError( f"It makes no sense to create a {self._text_name} object " f"when distributed memory is not enabled (dm=False).") + + def node_str(self, colour: bool = True) -> str: + ''' + Returns a text description of this node with (optional) control codes + to generate coloured output in a terminal that supports it. + + :param colour: whether or not to include colour control codes. + + :returns: description of this node, possibly coloured. + ''' + return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py index 3e9dbdf764..14e85a2956 100644 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -1,5 +1,7 @@ +import copy + from psyclone.core import AccessType -from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction from psyclone.errors import InternalError from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes.node import Node @@ -23,7 +25,6 @@ def __init__(self, scalar: KernelArgument, parent: Node = None): f"{self._text_name}.init(): A global sum argument should be a " f"scalar but found argument of type '{scalar.argument_type}'.") - import copy self._scalar = copy.copy(scalar) if scalar: # Update scalar values appropriately @@ -38,10 +39,9 @@ def scalar(self): return self._scalar @property - def dag_name(self): + def dag_name(self) -> str: ''' :returns: the name to use in the DAG for this node. - :rtype: str ''' return f"globalsum({self._scalar.name})_{self.position}" @@ -51,14 +51,13 @@ def args(self): the base method and simply return our argument.''' return [self._scalar] - def node_str(self, colour=True): + def node_str(self, colour: bool = True) -> str: ''' Returns a text description of this node with (optional) control codes to generate coloured output in a terminal that supports it. - :param bool colour: whether or not to include colour control codes. + :param colour: whether or not to include colour control codes. :returns: description of this node, possibly coloured. - :rtype: str ''' return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" diff --git a/src/psyclone/domain/lfric/__init__.py b/src/psyclone/domain/lfric/__init__.py index 1cf86ec9b5..913c258427 100644 --- a/src/psyclone/domain/lfric/__init__.py +++ b/src/psyclone/domain/lfric/__init__.py @@ -68,6 +68,8 @@ from psyclone.domain.lfric.lfric_kern_call_factory import LFRicKernCallFactory from psyclone.domain.lfric.lfric_collection import LFRicCollection from psyclone.domain.lfric.lfric_fields import LFRicFields +from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin +from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum from psyclone.domain.lfric.lfric_run_time_checks import LFRicRunTimeChecks from psyclone.domain.lfric.lfric_invokes import LFRicInvokes from psyclone.domain.lfric.lfric_scalar_args import LFRicScalarArgs @@ -78,34 +80,3 @@ from psyclone.domain.lfric.lfric_invoke_schedule import LFRicInvokeSchedule from psyclone.domain.lfric.lfric_dofmaps import LFRicDofmaps from psyclone.domain.lfric.lfric_stencils import LFRicStencils - - -__all__ = [ - 'ArgOrdering', - 'FunctionSpace', - 'KernCallAccArgList', - 'KernCallArgList', - 'KernelInterface', - 'KernStubArgList', - 'LFRicArgDescriptor', - 'LFRicCellIterators', - 'LFRicCollection', - 'LFRicConstants', - 'LFRicDofmaps', - 'LFRicDriverCreator', - 'LFRicFields', - 'LFRicHaloDepths', - 'LFRicInvoke', - 'LFRicInvokes', - 'LFRicInvokeSchedule', - 'LFRicKern', - 'LFRicKernCallFactory', - 'LFRicKernMetadata', - 'LFRicLoop', - 'LFRicLoopBounds', - 'LFRicPSy', - 'LFRicRunTimeChecks', - 'LFRicScalarArgs', - 'LFRicScalarArrayArgs', - 'LFRicStencils', - 'LFRicSymbolTable'] diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 46e8b36284..6898672821 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -1,15 +1,15 @@ -from psyclone.configuration import Config -from psyclone.domain.common.psylayer import GlobalReduction -from psyclone.errors import GenerationError, InternalError +from psyclone.domain.common.psylayer import GlobalMin +from psyclone.errors import GenerationError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( Assignment, Call, Reference, StructureReference) +from psyclone.psyir.nodes.node import Node from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, UnresolvedType) -class LFRicGlobalMin(GlobalReduction): +class LFRicGlobalMin(GlobalMin): ''' LFRic specific global min class which can be added to and manipulated in a schedule. @@ -24,16 +24,9 @@ class LFRicGlobalMin(GlobalReduction): ''' def __init__(self, scalar, parent=None): - # Check that distributed memory is enabled - if not Config.get().distributed_memory: - raise GenerationError( - "It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False).") - # Check that the global sum argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"LFRicGlobalSum.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") + # Initialise the parent class + super().__init__(scalar, parent=parent) + # Check scalar intrinsic types that this class supports (only # "real" for now) if scalar.intrinsic_type != "real": @@ -41,13 +34,10 @@ def __init__(self, scalar, parent=None): f"LFRicGlobalSum currently only supports real scalars, but " f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " f"'{scalar.intrinsic_type}' intrinsic type.") - # Initialise the parent class - super().__init__(scalar, parent=parent) - def lower_to_language_level(self): + def lower_to_language_level(self) -> Node: ''' :returns: this node lowered to language-level PSyIR. - :rtype: :py:class:`psyclone.psyir.nodes.Node` ''' # Get the name strings to use diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index 3d2a5f815f..fea11ecea5 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,4 +1,4 @@ -from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.errors import GenerationError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( @@ -9,7 +9,7 @@ UnresolvedType) -class LFRicGlobalSum(GlobalReduction): +class LFRicGlobalSum(GlobalSum): ''' LFRic specific global sum class which can be added to and manipulated in a schedule. @@ -25,7 +25,8 @@ class LFRicGlobalSum(GlobalReduction): ''' def __init__(self, scalar, parent=None): - super.__init__(scalar, parent=parent) + # Initialise the parent class + super().__init__(scalar, parent=parent) # Check scalar intrinsic types that this class supports (only # "real" for now) if scalar.intrinsic_type != "real": @@ -33,8 +34,6 @@ def __init__(self, scalar, parent=None): f"LFRicGlobalSum currently only supports real scalars, but " f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " f"'{scalar.intrinsic_type}' intrinsic type.") - # Initialise the parent class - super().__init__(scalar, parent=parent) def lower_to_language_level(self) -> Node: ''' diff --git a/src/psyclone/psyir/transformations/extract_trans.py b/src/psyclone/psyir/transformations/extract_trans.py index 550c4cb57b..b167968735 100644 --- a/src/psyclone/psyir/transformations/extract_trans.py +++ b/src/psyclone/psyir/transformations/extract_trans.py @@ -39,7 +39,8 @@ of an Invoke into a stand-alone application." ''' -from psyclone.psyGen import BuiltIn, Kern, HaloExchange, GlobalSum +from psyclone.domain.common.psylayer import GlobalSum +from psyclone.psyGen import BuiltIn, Kern, HaloExchange from psyclone.psyir.nodes import (CodeBlock, ExtractNode, Loop, Schedule, Directive, OMPParallelDirective, ACCParallelDirective) diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 8db4845f44..66ea8d7184 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -58,11 +58,12 @@ from psyclone import transformations from psyclone.configuration import Config from psyclone.core.access_type import AccessType -from psyclone.domain.common.psylayer import PSyLoop -from psyclone.domain.lfric import (lfric_builtins, LFRicInvokeSchedule, +from psyclone.domain.common.psylayer import GlobalSum, PSyLoop +from psyclone.domain.lfric import (lfric_builtins, LFRicGlobalSum, + LFRicInvokeSchedule, LFRicKern, LFRicKernMetadata) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans -from psyclone.lfric import LFRicGlobalSum, LFRicKernelArguments +from psyclone.lfric import LFRicKernelArguments from psyclone.errors import FieldNotFoundError, GenerationError, InternalError from psyclone.generator import generate from psyclone.gocean1p0 import GOKern @@ -70,7 +71,7 @@ from psyclone.psyGen import (TransInfo, PSyFactory, InlinedKern, object_index, HaloExchange, Invoke, DataAccess, Kern, Arguments, CodedKern, Argument, - GlobalSum, InvokeSchedule) + InvokeSchedule) from psyclone.psyir.nodes import (Assignment, BinaryOperation, Container, Literal, Loop, Node, KernelSchedule, Call, colored, Schedule) From e6984b2a06a0c3665686dae87a5b1a4cfd46ba8e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 21:07:57 +0000 Subject: [PATCH 04/15] #2381 mv GlobalSum tests to own file --- .../domain/common/psylayer/global_sum_test.py | 113 ++++++++++++++++++ src/psyclone/tests/psyGen_test.py | 71 ----------- 2 files changed, 113 insertions(+), 71 deletions(-) create mode 100644 src/psyclone/tests/domain/common/psylayer/global_sum_test.py diff --git a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py new file mode 100644 index 0000000000..06c0b94d88 --- /dev/null +++ b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py @@ -0,0 +1,113 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Authors: R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab +# Modified: I. Kavcic, L. Turner, O. Brunt and J. G. Wallwork, Met Office +# Modified: A. B. G. Chalk, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Performs py.test tests on the GlobalSum class. ''' + +import pytest + +from psyclone.core import AccessType +from psyclone.errors import GenerationError +from psyclone.domain.common.psylayer import GlobalSum +from psyclone.psyir.nodes import Literal +from psyclone.psyir.nodes.node import colored +from psyclone.psyir.symbols import INTEGER_TYPE +from psyclone.tests.utilities import get_invoke + + +def test_globalsum_node_str(): + '''test the node_str method in the GlobalSum class. The simplest way + to do this is to use an LFRic builtin example which contains a + scalar and then call node_str() on that. + + ''' + _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", + api="lfric", + dist_mem=True, idx=0) + + gsum = None + for child in invoke.schedule.children: + if isinstance(child, GlobalSum): + gsum = child + break + assert gsum + output = gsum.node_str() + expected_output = (colored("GlobalSum", GlobalSum._colour) + + "[scalar='asum']") + assert expected_output in output + + +def test_globalsum_children_validation(): + '''Test that children added to GlobalSum are validated. A GlobalSum node + does not accept any children. + + ''' + _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", api="lfric", + idx=0, dist_mem=True) + gsum = None + for child in invoke.schedule.children: + if isinstance(child, GlobalSum): + gsum = child + break + with pytest.raises(GenerationError) as excinfo: + gsum.addchild(Literal("2", INTEGER_TYPE)) + assert ("Item 'Literal' can't be child 0 of 'GlobalSum'. GlobalSum is a" + " LeafNode and doesn't accept children.") in str(excinfo.value) + + +def test_globalsum_arg(): + ''' Check that the globalsum argument is defined as gh_readwrite and + points to the GlobalSum node ''' + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", idx=0, dist_mem=True) + schedule = invoke.schedule + glob_sum = schedule.children[2] + glob_sum_arg = glob_sum.scalar + assert glob_sum_arg.access == AccessType.READWRITE + assert glob_sum_arg.call == glob_sum + + +def test_globalsum_args(): + '''Test that the globalsum class args method returns the appropriate + argument ''' + _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + global_sum = schedule.children[2] + assert len(global_sum.args) == 1 + assert global_sum.args[0] == global_sum.scalar + diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 66ea8d7184..1cdb549d7a 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -1019,48 +1019,6 @@ def test_haloexchange_unknown_halo_depth(): assert halo_exchange._halo_depth is None -def test_globalsum_node_str(): - '''test the node_str method in the GlobalSum class. The simplest way - to do this is to use an LFRic builtin example which contains a - scalar and then call node_str() on that. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "15.9.1_X_innerproduct_Y_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - gsum = None - for child in psy.invokes.invoke_list[0].schedule.children: - if isinstance(child, LFRicGlobalSum): - gsum = child - break - assert gsum - output = gsum.node_str() - expected_output = (colored("GlobalSum", GlobalSum._colour) + - "[scalar='asum']") - assert expected_output in output - - -def test_globalsum_children_validation(): - '''Test that children added to GlobalSum are validated. A GlobalSum node - does not accept any children. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "15.9.1_X_innerproduct_Y_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - gsum = None - for child in psy.invokes.invoke_list[0].schedule.children: - if isinstance(child, LFRicGlobalSum): - gsum = child - break - with pytest.raises(GenerationError) as excinfo: - gsum.addchild(Literal("2", INTEGER_TYPE)) - assert ("Item 'Literal' can't be child 0 of 'GlobalSum'. GlobalSum is a" - " LeafNode and doesn't accept children.") in str(excinfo.value) - - def test_args_filter(): '''the args_filter() method is in both Loop() and Arguments() classes with the former method calling the latter. This example tests the @@ -1431,21 +1389,6 @@ def test_argument_find_read_arguments(): assert result[idx] == loop.loop_body[0].arguments.args[3] -def test_globalsum_arg(): - ''' Check that the globalsum argument is defined as gh_readwrite and - points to the GlobalSum node ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - glob_sum = schedule.children[2] - glob_sum_arg = glob_sum.scalar - assert glob_sum_arg.access == AccessType.READWRITE - assert glob_sum_arg.call == glob_sum - - def test_haloexchange_arg(): '''Check that the HaloExchange argument is defined as gh_readwrite and points to the HaloExchange node''' @@ -1701,20 +1644,6 @@ def test_haloexchange_args(): assert haloexchange.args[0] == haloexchange.field -def test_globalsum_args(): - '''Test that the globalsum class args method returns the appropriate - argument ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - global_sum = schedule.children[2] - assert len(global_sum.args) == 1 - assert global_sum.args[0] == global_sum.scalar - - def test_call_forward_dependence(): '''Test that the Call class forward_dependence method returns the closest dependent call after the current call in the schedule or From f497d12e84d7ae8c299157eee3d801559d432ca2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 21:09:17 +0000 Subject: [PATCH 05/15] #2381 linting --- src/psyclone/tests/domain/common/psylayer/global_sum_test.py | 1 - src/psyclone/tests/psyGen_test.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py index 06c0b94d88..93b0f8905b 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py @@ -110,4 +110,3 @@ def test_globalsum_args(): global_sum = schedule.children[2] assert len(global_sum.args) == 1 assert global_sum.args[0] == global_sum.scalar - diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 1cdb549d7a..dbe410cc94 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -58,8 +58,8 @@ from psyclone import transformations from psyclone.configuration import Config from psyclone.core.access_type import AccessType -from psyclone.domain.common.psylayer import GlobalSum, PSyLoop -from psyclone.domain.lfric import (lfric_builtins, LFRicGlobalSum, +from psyclone.domain.common.psylayer import PSyLoop +from psyclone.domain.lfric import (lfric_builtins, LFRicInvokeSchedule, LFRicKern, LFRicKernMetadata) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans From 8e3955ae4bcd382d307d7e0769874ae6bba12bde Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Feb 2026 21:50:44 +0000 Subject: [PATCH 06/15] #2381 mv LFRicGlobalSum tests to own file --- .../domain/lfric/lfric_global_sum_test.py | 63 +++++++++++++++++ src/psyclone/tests/lfric_test.py | 67 +------------------ 2 files changed, 64 insertions(+), 66 deletions(-) create mode 100644 src/psyclone/tests/domain/lfric/lfric_global_sum_test.py diff --git a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py new file mode 100644 index 0000000000..ffb5cd036c --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py @@ -0,0 +1,63 @@ +import pytest + +from psyclone.domain.lfric import LFRicGlobalSum +from psyclone.errors import GenerationError, InternalError +from psyclone.tests.utilities import get_invoke + +TEST_API = "lfric" + + +def test_lfricglobalsum_unsupported_argument(): + ''' Check that an instance of the LFRicGlobalSum class raises an + exception for an unsupported argument type. ''' + # Get an instance of a non-scalar argument + _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + schedule = invoke.schedule + loop = schedule.children[4] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(InternalError) as err: + _ = LFRicGlobalSum(argument) + assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " + "but found argument of type 'gh_field'." in str(err.value)) + + +def test_lfricglobalsum_unsupported_scalar(): + ''' Check that an instance of the LFRicGlobalSum class raises an + exception if an unsupported scalar type is provided when distributed + memory is enabled (dm=True). + + ''' + # Get an instance of an integer scalar + _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + schedule = invoke.schedule + loop = schedule.children[4] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[1] + with pytest.raises(GenerationError) as err: + _ = LFRicGlobalSum(argument) + assert ("LFRicGlobalSum currently only supports real scalars, but " + "argument 'iflag' in Kernel 'testkern_one_int_scalar_code' " + "has 'integer' intrinsic type." in str(err.value)) + + +def test_lfricglobalsum_nodm_error(): + ''' Check that an instance of the LFRicGlobalSum class raises an + exception if it is instantiated with no distributed memory enabled + (dm=False). + + ''' + # Get an instance of a real scalar + _, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", + api=TEST_API, dist_mem=False, idx=0) + schedule = invoke.schedule + loop = schedule.children[0] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(GenerationError) as err: + _ = LFRicGlobalSum(argument) + assert ("It makes no sense to create an LFRicGlobalSum object when " + "distributed memory is not enabled (dm=False)." + in str(err.value)) diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index a26a0c6b3d..d81e126ea2 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -53,7 +53,7 @@ LFRicKernMetadata, LFRicLoop) from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.lfric import ( - LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicGlobalSum, + LFRicACCEnterDataDirective, LFRicBoundaryConditions, LFRicKernelArgument, LFRicKernelArguments, LFRicProxies, HaloReadAccess, KernCallArgList) from psyclone.errors import FieldNotFoundError, GenerationError, InternalError @@ -2954,71 +2954,6 @@ def test_haloexchange_correct_parent(): assert child.parent == schedule -def test_lfricglobalsum_unsupported_argument(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception for an unsupported argument type. ''' - # Get an instance of a non-scalar argument - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.6.1_single_invoke_1_int_scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[4] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(InternalError) as err: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " - "but found argument of type 'gh_field'." in str(err.value)) - - -def test_lfricglobalsum_unsupported_scalar(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception if an unsupported scalar type is provided when distributed - memory is enabled (dm=True). - - ''' - # Get an instance of an integer scalar - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.6.1_single_invoke_1_int_scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[4] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[1] - with pytest.raises(GenerationError) as err: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum currently only supports real scalars, but " - "argument 'iflag' in Kernel 'testkern_one_int_scalar_code' " - "has 'integer' intrinsic type." in str(err.value)) - - -def test_lfricglobalsum_nodm_error(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception if it is instantiated with no distributed memory enabled - (dm=False). - - ''' - # Get an instance of a real scalar - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.9_single_invoke_2_real_scalars.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=False).create(invoke_info) - schedule = psy.invokes.invoke_list[0].schedule - loop = schedule.children[0] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(GenerationError) as err: - _ = LFRicGlobalSum(argument) - assert ("It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False)." - in str(err.value)) - - def test_no_updated_args(): ''' Check that we raise the expected exception when we encounter a kernel that does not write to any of its arguments ''' From 77254035ccc430a684b0a5210b8f4ecc563b4993 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 10:00:38 +0000 Subject: [PATCH 07/15] #2381 fix remaining tests --- .../domain/common/psylayer/global_sum_test.py | 38 ++++++++- .../domain/lfric/lfric_global_sum_test.py | 78 ++++++++++--------- .../lfric_transformations_test.py | 6 +- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py index 93b0f8905b..cf9e4c1d0b 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py @@ -41,7 +41,7 @@ import pytest from psyclone.core import AccessType -from psyclone.errors import GenerationError +from psyclone.errors import GenerationError, InternalError from psyclone.domain.common.psylayer import GlobalSum from psyclone.psyir.nodes import Literal from psyclone.psyir.nodes.node import colored @@ -89,6 +89,42 @@ def test_globalsum_children_validation(): " LeafNode and doesn't accept children.") in str(excinfo.value) +def test_globalsum_nodm_error(): + ''' Check that an instance of the GlobalSum class raises an + exception if it is instantiated with no distributed memory enabled + (dm=False). We use the LFRic API to test this. + + ''' + # Get an instance of a real scalar + _, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", + api="lfric", dist_mem=False, idx=0) + schedule = invoke.schedule + loop = schedule.children[0] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(GenerationError) as err: + _ = GlobalSum(argument) + assert ("It makes no sense to create a GlobalSum object when " + "distributed memory is not enabled (dm=False)." + in str(err.value)) + + +def test_globalsum_unsupported_argument(): + ''' Check that an instance of the GlobalSum class raises an + exception for an unsupported argument type. ''' + # Get an instance of a non-scalar argument + _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", + api="lfric", dist_mem=True, idx=0) + schedule = invoke.schedule + loop = schedule.children[4] + kernel = loop.loop_body[0] + argument = kernel.arguments.args[0] + with pytest.raises(InternalError) as err: + _ = GlobalSum(argument) + assert ("GlobalSum.init(): A global sum argument should be a scalar " + "but found argument of type 'gh_field'." in str(err.value)) + + def test_globalsum_arg(): ''' Check that the globalsum argument is defined as gh_readwrite and points to the GlobalSum node ''' diff --git a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py index ffb5cd036c..ff476efe48 100644 --- a/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_global_sum_test.py @@ -1,28 +1,52 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2017-2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Authors: R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab +# Modified: I. Kavcic, L. Turner, O. Brunt and J. G. Wallwork, Met Office +# Modified: A. B. G. Chalk, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Performs py.test tests on the LFRicGlobalSum class. ''' + import pytest from psyclone.domain.lfric import LFRicGlobalSum -from psyclone.errors import GenerationError, InternalError +from psyclone.errors import GenerationError from psyclone.tests.utilities import get_invoke TEST_API = "lfric" -def test_lfricglobalsum_unsupported_argument(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception for an unsupported argument type. ''' - # Get an instance of a non-scalar argument - _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", - api=TEST_API, dist_mem=True, idx=0) - schedule = invoke.schedule - loop = schedule.children[4] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(InternalError) as err: - _ = LFRicGlobalSum(argument) - assert ("LFRicGlobalSum.init(): A global sum argument should be a scalar " - "but found argument of type 'gh_field'." in str(err.value)) - - def test_lfricglobalsum_unsupported_scalar(): ''' Check that an instance of the LFRicGlobalSum class raises an exception if an unsupported scalar type is provided when distributed @@ -41,23 +65,3 @@ def test_lfricglobalsum_unsupported_scalar(): assert ("LFRicGlobalSum currently only supports real scalars, but " "argument 'iflag' in Kernel 'testkern_one_int_scalar_code' " "has 'integer' intrinsic type." in str(err.value)) - - -def test_lfricglobalsum_nodm_error(): - ''' Check that an instance of the LFRicGlobalSum class raises an - exception if it is instantiated with no distributed memory enabled - (dm=False). - - ''' - # Get an instance of a real scalar - _, invoke = get_invoke("1.9_single_invoke_2_real_scalars.f90", - api=TEST_API, dist_mem=False, idx=0) - schedule = invoke.schedule - loop = schedule.children[0] - kernel = loop.loop_body[0] - argument = kernel.arguments.args[0] - with pytest.raises(GenerationError) as err: - _ = LFRicGlobalSum(argument) - assert ("It makes no sense to create an LFRicGlobalSum object when " - "distributed memory is not enabled (dm=False)." - in str(err.value)) diff --git a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index 8305e42ade..b908636f45 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -48,11 +48,11 @@ from psyclone.core import AccessType, Signature from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans -from psyclone.domain.lfric import LFRicLoop +from psyclone.domain.lfric import LFRicGlobalSum, LFRicLoop from psyclone.lfric import (LFRicHaloExchangeStart, LFRicHaloExchangeEnd, LFRicHaloExchange) from psyclone.errors import GenerationError, InternalError -from psyclone.psyGen import InvokeSchedule, GlobalSum, BuiltIn +from psyclone.psyGen import InvokeSchedule, BuiltIn from psyclone.psyir.backend.visitor import VisitorError from psyclone.psyir.nodes import ( colored, Loop, Schedule, Literal, Directive, OMPDoDirective, @@ -3837,7 +3837,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): ompdefault = colored("OMPDefaultClause", Directive._colour) ompprivate = colored("OMPPrivateClause", Directive._colour) ompfprivate = colored("OMPFirstprivateClause", Directive._colour) - gsum = colored("GlobalSum", GlobalSum._colour) + gsum = colored("GlobalSum", LFRicGlobalSum._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) From 9caaf13c28ea8baaaf506351f6bccd60ecd3860e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 10:54:48 +0000 Subject: [PATCH 08/15] #2381 add global-max support --- .../domain/common/psylayer/__init__.py | 1 + .../domain/common/psylayer/global_max.py | 53 ++++++++++++++ src/psyclone/domain/lfric/__init__.py | 1 + src/psyclone/domain/lfric/lfric_global_max.py | 70 +++++++++++++++++++ src/psyclone/domain/lfric/lfric_global_min.py | 29 ++++---- src/psyclone/domain/lfric/lfric_invoke.py | 7 +- .../tests/domain/lfric/lfric_builtins_test.py | 19 ++--- 7 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 src/psyclone/domain/common/psylayer/global_max.py create mode 100644 src/psyclone/domain/lfric/lfric_global_max.py diff --git a/src/psyclone/domain/common/psylayer/__init__.py b/src/psyclone/domain/common/psylayer/__init__.py index 94837f77e4..507e6ce160 100644 --- a/src/psyclone/domain/common/psylayer/__init__.py +++ b/src/psyclone/domain/common/psylayer/__init__.py @@ -36,6 +36,7 @@ '''A package module for psyclone.domain.common.psylayer''' from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.domain.common.psylayer.global_max import GlobalMax from psyclone.domain.common.psylayer.global_min import GlobalMin from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.domain.common.psylayer.psyloop import PSyLoop diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py new file mode 100644 index 0000000000..4190e74001 --- /dev/null +++ b/src/psyclone/domain/common/psylayer/global_max.py @@ -0,0 +1,53 @@ +import copy + +from psyclone.core import AccessType +from psyclone.domain.common.psylayer.global_reduction import GlobalReduction +from psyclone.errors import InternalError +from psyclone.psyGen import KernelArgument +from psyclone.psyir.nodes.node import Node + + +class GlobalMax(GlobalReduction): + ''' + Generic GlobalMax class which can be added to a Schedule. + + :param scalar: the scalar that the global maximum is computed for and + the result stored into. + :param parent: optional parent (default None) of this object + + ''' + _text_name = "GlobalMax" + + def __init__(self, scalar: KernelArgument, parent: Node = None): + super().__init__(parent=parent) + # Check that the argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"{self._text_name}.init(): A global max argument should be a " + f"scalar but found argument of type '{scalar.argument_type}'.") + + self._scalar = copy.copy(scalar) + if scalar: + # Update scalar values appropriately + # Here "readwrite" denotes how the class GlobalMax + # accesses/updates a scalar + self._scalar.access = AccessType.READWRITE + self._scalar.call = self + + @property + def scalar(self): + ''' Return the scalar quantity that this global reduction acts on ''' + return self._scalar + + @property + def dag_name(self) -> str: + ''' + :returns: the name to use in the DAG for this node. + ''' + return f"globalmax({self._scalar.name})_{self.position}" + + @property + def args(self): + ''' Return the list of arguments associated with this node. Override + the base method and simply return our argument.''' + return [self._scalar] diff --git a/src/psyclone/domain/lfric/__init__.py b/src/psyclone/domain/lfric/__init__.py index 913c258427..25837a226b 100644 --- a/src/psyclone/domain/lfric/__init__.py +++ b/src/psyclone/domain/lfric/__init__.py @@ -68,6 +68,7 @@ from psyclone.domain.lfric.lfric_kern_call_factory import LFRicKernCallFactory from psyclone.domain.lfric.lfric_collection import LFRicCollection from psyclone.domain.lfric.lfric_fields import LFRicFields +from psyclone.domain.lfric.lfric_global_max import LFRicGlobalMax from psyclone.domain.lfric.lfric_global_min import LFRicGlobalMin from psyclone.domain.lfric.lfric_global_sum import LFRicGlobalSum from psyclone.domain.lfric.lfric_run_time_checks import LFRicRunTimeChecks diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py new file mode 100644 index 0000000000..ac428a06de --- /dev/null +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -0,0 +1,70 @@ +from psyclone.domain.common.psylayer import GlobalMax +from psyclone.errors import GenerationError +from psyclone.lfric import LFRicKernelArgument +from psyclone.psyGen import InvokeSchedule, KernelArgument +from psyclone.psyir.nodes import ( + Assignment, Call, Reference, StructureReference) +from psyclone.psyir.nodes.node import Node +from psyclone.psyir.symbols import ( + ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, + UnresolvedType) + + +class LFRicGlobalMax(GlobalMax): + ''' + LFRic specific global max class which can be added to and + manipulated in a schedule. + + :param scalar: the kernel argument for which to perform a global min. + :param parent: the parent node of this node in the PSyIR. + + :raises InternalError: if the supplied argument is not a scalar. + :raises GenerationError: if the scalar is not of "real" intrinsic type. + + ''' + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): + # Initialise the parent class + super().__init__(scalar, parent=parent) + + # Check scalar intrinsic types that this class supports (only + # "real" for now) + if scalar.intrinsic_type != "real": + raise GenerationError( + f"LFRicGlobalMax currently only supports real scalars, but " + f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"'{scalar.intrinsic_type}' intrinsic type.") + + def lower_to_language_level(self) -> Node: + ''' + :returns: this node lowered to language-level PSyIR. + + ''' + # Get the name strings to use + name = self._scalar.name + type_name = self._scalar.data_type + mod_name = self._scalar.module_name + + # Get the symbols from the given names + symtab = self.ancestor(InvokeSchedule).symbol_table + sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) + sum_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + sum_name = symtab.find_or_create_tag("global_max", + symbol_type=DataSymbol, + datatype=sum_type) + tmp_var = symtab.lookup(name) + + # Create the assignments + assign1 = Assignment.create( + lhs=StructureReference.create(sum_name, ["value"]), + rhs=Reference(tmp_var) + ) + assign1.preceding_comment = "Perform global max" + self.parent.addchild(assign1, self.position) + assign2 = Assignment.create( + lhs=Reference(tmp_var), + rhs=Call.create(StructureReference.create(sum_name, ["get_max"])) + ) + return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 6898672821..e9ac50e593 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -29,10 +29,11 @@ def __init__(self, scalar, parent=None): # Check scalar intrinsic types that this class supports (only # "real" for now) - if scalar.intrinsic_type != "real": + if scalar.intrinsic_type not in ["real", "integer"]: raise GenerationError( - f"LFRicGlobalSum currently only supports real scalars, but " - f"argument '{scalar.name}' in Kernel '{scalar.call.name}' has " + f"LFRicGlobalMin currently only supports real or integer " + f"scalars, but argument '{scalar.name}' in Kernel " + f"'{scalar.call.name}' has " f"'{scalar.intrinsic_type}' intrinsic type.") def lower_to_language_level(self) -> Node: @@ -47,25 +48,29 @@ def lower_to_language_level(self) -> Node: # Get the symbols from the given names symtab = self.ancestor(InvokeSchedule).symbol_table + # The Container from which to import the scalar type. sum_mod = symtab.find_or_create(mod_name, symbol_type=ContainerSymbol) - sum_type = symtab.find_or_create(type_name, - symbol_type=DataTypeSymbol, - datatype=UnresolvedType(), - interface=ImportInterface(sum_mod)) - sum_name = symtab.find_or_create_tag("global_sum", + # The scalar type. + scal_type = symtab.find_or_create(type_name, + symbol_type=DataTypeSymbol, + datatype=UnresolvedType(), + interface=ImportInterface(sum_mod)) + # An instance of scalar type that we will use to get the global min. + sum_name = symtab.find_or_create_tag("global_min", symbol_type=DataSymbol, - datatype=sum_type) + datatype=scal_type) tmp_var = symtab.lookup(name) - # Create the assignments + # Assign the value of the local scalar to the new scalar_type quantity assign1 = Assignment.create( lhs=StructureReference.create(sum_name, ["value"]), rhs=Reference(tmp_var) ) - assign1.preceding_comment = "Perform global sum" + assign1.preceding_comment = "Obtain global min" self.parent.addchild(assign1, self.position) + # Use the 'get_min' method to compute the global min. assign2 = Assignment.create( lhs=Reference(tmp_var), - rhs=Call.create(StructureReference.create(sum_name, ["get_sum"])) + rhs=Call.create(StructureReference.create(sum_name, ["get_min"])) ) return self.replace_with(assign2) diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index bcfaf067c4..f8c466469f 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -92,7 +92,7 @@ def __init__(self, alg_invocation, idx, invokes): LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( - LFRicCellIterators, LFRicGlobalSum, LFRicGlobalMin, + LFRicCellIterators, LFRicGlobalMax, LFRicGlobalSum, LFRicGlobalMin, LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicScalarArrayArgs, LFRicFields, LFRicDofmaps, @@ -194,9 +194,12 @@ def __init__(self, alg_invocation, idx, invokes): elif kern.reduction_type == LFRicBuiltIn.ReductionType.MIN: global_red = LFRicGlobalMin(kern.reduction_arg, parent=loop.parent) + elif kern.reduction_type == LFRicBuiltIn.ReductionType.MAX: + global_red = LFRicGlobalMax(kern.reduction_arg, + parent=loop.parent) else: raise GenerationError( - f"TODO #2381 - currently only global *sum* " + f"TODO #2381 - currently only global SUM/MAX/MIN " f"reductions are supported but kernel '{kern.name}' " f"performs a {kern.reduction_type}") loop.parent.children.insert(loop.position+1, global_red) diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 6aca9ecd4c..38e4e89f32 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1975,7 +1975,7 @@ def test_int_to_real_x_precision(tmpdir, kind_name): assert LFRicBuild(tmpdir).code_compiles(psy) -def test_minmaxval_x(fortran_writer): +def test_minmaxval_x(fortran_writer, tmp_path): ''' Tests for the minval_x and maxval_x builtins. ''' @@ -2005,14 +2005,17 @@ def test_minmaxval_x(fortran_writer): code = fortran_writer(kerns[1]) assert "amax = MAX(amax, f1_data(df))" in code, code - # Currently psy-layer generation with DM enabled won't work because we only - # have support for global sums. TODO #2381. - with pytest.raises(GenerationError) as err: - _ = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, - dist_mem=True) - assert ("TODO #2381 - currently only global *sum* reductions are supported" - in str(err.value)) + psy, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, + dist_mem=True) + output = fortran_writer(invoke.schedule) + assert "global_min%value = amin" in output + assert "amin = global_min%get_min()" in output + assert "global_max%value = amax" in output + assert "amax = global_max%get_max()" in output + # Test compilation of generated code + assert LFRicBuild(tmp_path).code_compiles(psy) + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' From 852178e6c1b8a7ee8cb3dbbeba64d84114856624 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 11:01:06 +0000 Subject: [PATCH 09/15] #2381 linting and docstrings --- .../domain/common/psylayer/global_max.py | 38 +++++++++++++++++++ .../domain/common/psylayer/global_min.py | 38 +++++++++++++++++++ src/psyclone/domain/lfric/lfric_global_max.py | 2 +- .../tests/domain/lfric/lfric_builtins_test.py | 2 +- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py index 4190e74001..3d0edd035a 100644 --- a/src/psyclone/domain/common/psylayer/global_max.py +++ b/src/psyclone/domain/common/psylayer/global_max.py @@ -1,3 +1,41 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Contains the implementation of the GlobalMax class. ''' + import copy from psyclone.core import AccessType diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py index fb0a34210e..6513d03b88 100644 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -1,3 +1,41 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +''' Contains the implementation of the GlobalMin class. ''' + import copy from psyclone.core import AccessType diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py index ac428a06de..729b405970 100644 --- a/src/psyclone/domain/lfric/lfric_global_max.py +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -1,7 +1,7 @@ from psyclone.domain.common.psylayer import GlobalMax from psyclone.errors import GenerationError from psyclone.lfric import LFRicKernelArgument -from psyclone.psyGen import InvokeSchedule, KernelArgument +from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( Assignment, Call, Reference, StructureReference) from psyclone.psyir.nodes.node import Node diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index 38e4e89f32..b9f1c42263 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -2015,7 +2015,7 @@ def test_minmaxval_x(fortran_writer, tmp_path): # Test compilation of generated code assert LFRicBuild(tmp_path).code_compiles(psy) - + def test_real_to_int_x(fortran_writer): ''' Test the metadata, str and lower_to_language_level builtin methods. ''' From 7e488cf84b5e278f7761dfa23105d762d9e80d0f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 12:01:03 +0000 Subject: [PATCH 10/15] #2381 WIP updating tests [skip ci] --- .../domain/common/psylayer/global_max.py | 39 -------------- .../domain/common/psylayer/global_min.py | 44 --------------- .../common/psylayer/global_reduction.py | 41 +++++++++++++- .../domain/common/psylayer/global_sum.py | 53 ------------------- .../domain/common/psylayer/global_sum_test.py | 7 +-- src/psyclone/tests/psyir/nodes/node_test.py | 4 +- 6 files changed, 44 insertions(+), 144 deletions(-) diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py index 3d0edd035a..2721a7e560 100644 --- a/src/psyclone/domain/common/psylayer/global_max.py +++ b/src/psyclone/domain/common/psylayer/global_max.py @@ -36,13 +36,8 @@ ''' Contains the implementation of the GlobalMax class. ''' -import copy -from psyclone.core import AccessType from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.errors import InternalError -from psyclone.psyGen import KernelArgument -from psyclone.psyir.nodes.node import Node class GlobalMax(GlobalReduction): @@ -55,37 +50,3 @@ class GlobalMax(GlobalReduction): ''' _text_name = "GlobalMax" - - def __init__(self, scalar: KernelArgument, parent: Node = None): - super().__init__(parent=parent) - # Check that the argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"{self._text_name}.init(): A global max argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") - - self._scalar = copy.copy(scalar) - if scalar: - # Update scalar values appropriately - # Here "readwrite" denotes how the class GlobalMax - # accesses/updates a scalar - self._scalar.access = AccessType.READWRITE - self._scalar.call = self - - @property - def scalar(self): - ''' Return the scalar quantity that this global reduction acts on ''' - return self._scalar - - @property - def dag_name(self) -> str: - ''' - :returns: the name to use in the DAG for this node. - ''' - return f"globalmax({self._scalar.name})_{self.position}" - - @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' - return [self._scalar] diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py index 6513d03b88..bdab9496a1 100644 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ b/src/psyclone/domain/common/psylayer/global_min.py @@ -36,56 +36,12 @@ ''' Contains the implementation of the GlobalMin class. ''' -import copy - -from psyclone.core import AccessType from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.errors import InternalError -from psyclone.psyGen import KernelArgument -from psyclone.psyir.nodes.node import Node class GlobalMin(GlobalReduction): ''' Generic GlobalMin class which can be added to a Schedule. - :param scalar: the scalar that the global mimimum is computed for and - the result stored into. - :param parent: optional parent (default None) of this object - ''' _text_name = "GlobalMin" - - def __init__(self, scalar: KernelArgument, parent: Node = None): - super().__init__(parent=parent) - # Check that the argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"{self._text_name}.init(): A global min argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") - - self._scalar = copy.copy(scalar) - if scalar: - # Update scalar values appropriately - # Here "readwrite" denotes how the class GlobalMin - # accesses/updates a scalar - self._scalar.access = AccessType.READWRITE - self._scalar.call = self - - @property - def scalar(self): - ''' Return the scalar quantity that this global min acts on ''' - return self._scalar - - @property - def dag_name(self) -> str: - ''' - :returns: the name to use in the DAG for this node. - ''' - return f"globalmin({self._scalar.name})_{self.position}" - - @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' - return [self._scalar] diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index fb03065cf7..24809030cb 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -1,5 +1,9 @@ +import copy + from psyclone.configuration import Config -from psyclone.errors import GenerationError +from psyclone.core import AccessType +from psyclone.errors import GenerationError, InternalError +from psyclone.psyGen import KernelArgument from psyclone.psyir.nodes import Statement from psyclone.psyir.nodes.node import Node @@ -9,6 +13,7 @@ class GlobalReduction(Statement): Represents a global-reduction in the PSyIR. :raises GenerationError: if distributed memory is not enabled. + :raises InternalError: if the supplied argument doesn't represent a scalar. ''' # TODO is this really a leaf - it could have operands as children? @@ -18,7 +23,7 @@ class GlobalReduction(Statement): #: The colour to use when creating a view of this node. _colour = "cyan" - def __init__(self, parent: Node = None): + def __init__(self, scalar: KernelArgument, parent: Node = None): super().__init__(children=[], parent=parent) # Check that distributed memory is enabled if not Config.get().distributed_memory: @@ -26,6 +31,20 @@ def __init__(self, parent: Node = None): f"It makes no sense to create a {self._text_name} object " f"when distributed memory is not enabled (dm=False).") + # Check that the global sum argument is indeed a scalar + if not scalar.is_scalar: + raise InternalError( + f"{self._text_name}.init(): A global sum argument should be a " + f"scalar but found argument of type '{scalar.argument_type}'.") + + self._scalar = copy.copy(scalar) + if scalar: + # Update scalar values appropriately + # Here "readwrite" denotes how the class GlobalSum + # accesses/updates a scalar + self._scalar.access = AccessType.READWRITE + self._scalar.call = self + def node_str(self, colour: bool = True) -> str: ''' Returns a text description of this node with (optional) control codes @@ -36,3 +55,21 @@ def node_str(self, colour: bool = True) -> str: :returns: description of this node, possibly coloured. ''' return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" + + @property + def scalar(self): + ''' Return the scalar field that this global reduction acts on ''' + return self._scalar + + @property + def dag_name(self) -> str: + ''' + :returns: the name to use in the DAG for this node. + ''' + return f"{self._text_name}({self._scalar.name})_{self.position}" + + @property + def args(self): + ''' Return the list of arguments associated with this node. Override + the base method and simply return our argument.''' + return [self._scalar] diff --git a/src/psyclone/domain/common/psylayer/global_sum.py b/src/psyclone/domain/common/psylayer/global_sum.py index 14e85a2956..e891ad4674 100644 --- a/src/psyclone/domain/common/psylayer/global_sum.py +++ b/src/psyclone/domain/common/psylayer/global_sum.py @@ -1,63 +1,10 @@ -import copy -from psyclone.core import AccessType from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.errors import InternalError -from psyclone.psyGen import KernelArgument -from psyclone.psyir.nodes.node import Node class GlobalSum(GlobalReduction): ''' Generic GlobalSum class which can be added to a Schedule. - :param scalar: the scalar that the global sum is stored into - :param parent: optional parent (default None) of this object - ''' _text_name = "GlobalSum" - - def __init__(self, scalar: KernelArgument, parent: Node = None): - super().__init__(parent=parent) - # Check that the global sum argument is indeed a scalar - if not scalar.is_scalar: - raise InternalError( - f"{self._text_name}.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") - - self._scalar = copy.copy(scalar) - if scalar: - # Update scalar values appropriately - # Here "readwrite" denotes how the class GlobalSum - # accesses/updates a scalar - self._scalar.access = AccessType.READWRITE - self._scalar.call = self - - @property - def scalar(self): - ''' Return the scalar field that this global sum acts on ''' - return self._scalar - - @property - def dag_name(self) -> str: - ''' - :returns: the name to use in the DAG for this node. - ''' - return f"globalsum({self._scalar.name})_{self.position}" - - @property - def args(self): - ''' Return the list of arguments associated with this node. Override - the base method and simply return our argument.''' - return [self._scalar] - - def node_str(self, colour: bool = True) -> str: - ''' - Returns a text description of this node with (optional) control codes - to generate coloured output in a terminal that supports it. - - :param colour: whether or not to include colour control codes. - - :returns: description of this node, possibly coloured. - ''' - return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']" diff --git a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py index cf9e4c1d0b..48f70aaef3 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_sum_test.py @@ -49,9 +49,9 @@ from psyclone.tests.utilities import get_invoke -def test_globalsum_node_str(): - '''test the node_str method in the GlobalSum class. The simplest way - to do this is to use an LFRic builtin example which contains a +def test_globalsum_node_str_and_dag_name(): + '''test the node_str and dag_name methods in the GlobalSum class. The + simplest way to do this is to use an LFRic builtin example which contains a scalar and then call node_str() on that. ''' @@ -69,6 +69,7 @@ def test_globalsum_node_str(): expected_output = (colored("GlobalSum", GlobalSum._colour) + "[scalar='asum']") assert expected_output in output + assert gsum.dag_name == "GlobalSum(asum)_1" def test_globalsum_children_validation(): diff --git a/src/psyclone/tests/psyir/nodes/node_test.py b/src/psyclone/tests/psyir/nodes/node_test.py index 2a2343152c..5f67cf78db 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -763,15 +763,13 @@ def test_dag_names(): idx = aref.children[0].detach() assert idx.dag_name == "Literal_0" - # GlobalSum and BuiltIn also have specialised dag_names + # BuiltIn also has specialised dag_names _, invoke_info = parse( os.path.join(BASE_PATH, "15.14.3_sum_setval_field_builtin.f90"), api="lfric") psy = PSyFactory("lfric", distributed_memory=True).create(invoke_info) invoke = psy.invokes.invoke_list[0] schedule = invoke.schedule - global_sum = schedule.children[2] - assert global_sum.dag_name == "globalsum(asum)_2" builtin = schedule.children[1].loop_body[0] assert builtin.dag_name == "builtin_sum_x_12" From 251662932b3fce944ea1610b6812bd448525ef17 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 12:20:17 +0000 Subject: [PATCH 11/15] #2381 WIP removing common max/min/sum classes [skip ci] --- src/psyclone/domain/lfric/lfric_global_max.py | 6 ++++-- src/psyclone/domain/lfric/lfric_global_min.py | 6 ++++-- src/psyclone/domain/lfric/lfric_global_sum.py | 6 ++++-- src/psyclone/psyir/transformations/extract_trans.py | 8 ++++---- .../lfric/transformations/lfric_transformations_test.py | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_global_max.py b/src/psyclone/domain/lfric/lfric_global_max.py index 729b405970..694bb50e21 100644 --- a/src/psyclone/domain/lfric/lfric_global_max.py +++ b/src/psyclone/domain/lfric/lfric_global_max.py @@ -1,4 +1,4 @@ -from psyclone.domain.common.psylayer import GlobalMax +from psyclone.domain.common.psylayer import GlobalReduction from psyclone.errors import GenerationError from psyclone.lfric import LFRicKernelArgument from psyclone.psyGen import InvokeSchedule @@ -10,7 +10,7 @@ UnresolvedType) -class LFRicGlobalMax(GlobalMax): +class LFRicGlobalMax(GlobalReduction): ''' LFRic specific global max class which can be added to and manipulated in a schedule. @@ -22,6 +22,8 @@ class LFRicGlobalMax(GlobalMax): :raises GenerationError: if the scalar is not of "real" intrinsic type. ''' + _text_name = "LFRicGlobalMax" + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): # Initialise the parent class super().__init__(scalar, parent=parent) diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index e9ac50e593..8d0b4bd4bc 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -1,4 +1,4 @@ -from psyclone.domain.common.psylayer import GlobalMin +from psyclone.domain.common.psylayer import GlobalReduction from psyclone.errors import GenerationError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( @@ -9,7 +9,7 @@ UnresolvedType) -class LFRicGlobalMin(GlobalMin): +class LFRicGlobalMin(GlobalReduction): ''' LFRic specific global min class which can be added to and manipulated in a schedule. @@ -23,6 +23,8 @@ class LFRicGlobalMin(GlobalMin): :raises GenerationError: if the scalar is not of "real" intrinsic type. ''' + _text_name = "LFRicGlobalMin" + def __init__(self, scalar, parent=None): # Initialise the parent class super().__init__(scalar, parent=parent) diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index fea11ecea5..bdcaf39fcf 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,4 +1,4 @@ -from psyclone.domain.common.psylayer.global_sum import GlobalSum +from psyclone.domain.common.psylayer.global_sum import GlobalReduction from psyclone.errors import GenerationError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( @@ -9,7 +9,7 @@ UnresolvedType) -class LFRicGlobalSum(GlobalSum): +class LFRicGlobalSum(GlobalReduction): ''' LFRic specific global sum class which can be added to and manipulated in a schedule. @@ -24,6 +24,8 @@ class LFRicGlobalSum(GlobalSum): :raises GenerationError: if the scalar is not of "real" intrinsic type. ''' + _text_name = "LFRicGlobalSum" + def __init__(self, scalar, parent=None): # Initialise the parent class super().__init__(scalar, parent=parent) diff --git a/src/psyclone/psyir/transformations/extract_trans.py b/src/psyclone/psyir/transformations/extract_trans.py index b167968735..920d2b7992 100644 --- a/src/psyclone/psyir/transformations/extract_trans.py +++ b/src/psyclone/psyir/transformations/extract_trans.py @@ -39,7 +39,7 @@ of an Invoke into a stand-alone application." ''' -from psyclone.domain.common.psylayer import GlobalSum +from psyclone.domain.common.psylayer import GlobalReduction from psyclone.psyGen import BuiltIn, Kern, HaloExchange from psyclone.psyir.nodes import (CodeBlock, ExtractNode, Loop, Schedule, Directive, OMPParallelDirective, @@ -65,15 +65,15 @@ class ExtractTrans(PSyDataTrans): Loops containing a Kernel or BuiltIn call) or entire Invokes. This functionality does not support distributed memory. - :param node_class: The Node class of which an instance will be inserted \ + :param node_class: The Node class of which an instance will be inserted into the tree (defaults to ExtractNode), but can be any derived class. - :type node_class: :py:class:`psyclone.psyir.nodes.ExtractNode` or \ + :type node_class: :py:class:`psyclone.psyir.nodes.ExtractNode` or derived class ''' # The types of node that this transformation cannot enclose excluded_node_types = (CodeBlock, ExtractNode, - HaloExchange, GlobalSum) + HaloExchange, GlobalReduction) def __init__(self, node_class=ExtractNode): # This function is required to provide the appropriate default diff --git a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index b908636f45..bb2e3328e0 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -3837,7 +3837,7 @@ def test_reprod_view(monkeypatch, annexed, dist_mem): ompdefault = colored("OMPDefaultClause", Directive._colour) ompprivate = colored("OMPPrivateClause", Directive._colour) ompfprivate = colored("OMPFirstprivateClause", Directive._colour) - gsum = colored("GlobalSum", LFRicGlobalSum._colour) + gsum = colored("LFRicGlobalSum", LFRicGlobalSum._colour) loop = colored("Loop", Loop._colour) call = colored("BuiltIn", BuiltIn._colour) sched = colored("Schedule", Schedule._colour) From 64c0c53c3ff79de2fe435a8bdb550d03ada75c70 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 12:41:56 +0000 Subject: [PATCH 12/15] #2381 remove common max/min/sum classes --- .../domain/common/psylayer/__init__.py | 3 - .../domain/common/psylayer/global_max.py | 52 ----------------- .../domain/common/psylayer/global_min.py | 47 --------------- .../common/psylayer/global_reduction.py | 5 +- ...l_sum_test.py => global_reduction_test.py} | 58 +++++++++---------- 5 files changed, 31 insertions(+), 134 deletions(-) delete mode 100644 src/psyclone/domain/common/psylayer/global_max.py delete mode 100644 src/psyclone/domain/common/psylayer/global_min.py rename src/psyclone/tests/domain/common/psylayer/{global_sum_test.py => global_reduction_test.py} (75%) diff --git a/src/psyclone/domain/common/psylayer/__init__.py b/src/psyclone/domain/common/psylayer/__init__.py index 507e6ce160..3983be9973 100644 --- a/src/psyclone/domain/common/psylayer/__init__.py +++ b/src/psyclone/domain/common/psylayer/__init__.py @@ -36,8 +36,5 @@ '''A package module for psyclone.domain.common.psylayer''' from psyclone.domain.common.psylayer.global_reduction import GlobalReduction -from psyclone.domain.common.psylayer.global_max import GlobalMax -from psyclone.domain.common.psylayer.global_min import GlobalMin -from psyclone.domain.common.psylayer.global_sum import GlobalSum from psyclone.domain.common.psylayer.psyloop import PSyLoop diff --git a/src/psyclone/domain/common/psylayer/global_max.py b/src/psyclone/domain/common/psylayer/global_max.py deleted file mode 100644 index 2721a7e560..0000000000 --- a/src/psyclone/domain/common/psylayer/global_max.py +++ /dev/null @@ -1,52 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2026, Science and Technology Facilities Council. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# ----------------------------------------------------------------------------- -# Author: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -''' Contains the implementation of the GlobalMax class. ''' - - -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction - - -class GlobalMax(GlobalReduction): - ''' - Generic GlobalMax class which can be added to a Schedule. - - :param scalar: the scalar that the global maximum is computed for and - the result stored into. - :param parent: optional parent (default None) of this object - - ''' - _text_name = "GlobalMax" diff --git a/src/psyclone/domain/common/psylayer/global_min.py b/src/psyclone/domain/common/psylayer/global_min.py deleted file mode 100644 index bdab9496a1..0000000000 --- a/src/psyclone/domain/common/psylayer/global_min.py +++ /dev/null @@ -1,47 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2026, Science and Technology Facilities Council. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# ----------------------------------------------------------------------------- -# Author: A. R. Porter, STFC Daresbury Lab -# ----------------------------------------------------------------------------- - -''' Contains the implementation of the GlobalMin class. ''' - -from psyclone.domain.common.psylayer.global_reduction import GlobalReduction - - -class GlobalMin(GlobalReduction): - ''' - Generic GlobalMin class which can be added to a Schedule. - - ''' - _text_name = "GlobalMin" diff --git a/src/psyclone/domain/common/psylayer/global_reduction.py b/src/psyclone/domain/common/psylayer/global_reduction.py index 24809030cb..9c76f89d8d 100644 --- a/src/psyclone/domain/common/psylayer/global_reduction.py +++ b/src/psyclone/domain/common/psylayer/global_reduction.py @@ -34,8 +34,9 @@ def __init__(self, scalar: KernelArgument, parent: Node = None): # Check that the global sum argument is indeed a scalar if not scalar.is_scalar: raise InternalError( - f"{self._text_name}.init(): A global sum argument should be a " - f"scalar but found argument of type '{scalar.argument_type}'.") + f"{self._text_name}.init(): A global reduction argument should" + f" be a scalar but found argument of type " + f"'{scalar.argument_type}'.") self._scalar = copy.copy(scalar) if scalar: diff --git a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py similarity index 75% rename from src/psyclone/tests/domain/common/psylayer/global_sum_test.py rename to src/psyclone/tests/domain/common/psylayer/global_reduction_test.py index 48f70aaef3..04f2e7dd58 100644 --- a/src/psyclone/tests/domain/common/psylayer/global_sum_test.py +++ b/src/psyclone/tests/domain/common/psylayer/global_reduction_test.py @@ -36,21 +36,21 @@ # Modified: A. B. G. Chalk, STFC Daresbury Lab # ----------------------------------------------------------------------------- -''' Performs py.test tests on the GlobalSum class. ''' +''' Performs py.test tests on the GlobalReduction class. ''' import pytest from psyclone.core import AccessType from psyclone.errors import GenerationError, InternalError -from psyclone.domain.common.psylayer import GlobalSum +from psyclone.domain.common.psylayer import GlobalReduction from psyclone.psyir.nodes import Literal from psyclone.psyir.nodes.node import colored from psyclone.psyir.symbols import INTEGER_TYPE from psyclone.tests.utilities import get_invoke -def test_globalsum_node_str_and_dag_name(): - '''test the node_str and dag_name methods in the GlobalSum class. The +def test_globalreduction_node_str_and_dag_name(): + '''test the node_str and dag_name methods in the GlobalReduction class. The simplest way to do this is to use an LFRic builtin example which contains a scalar and then call node_str() on that. @@ -58,40 +58,38 @@ def test_globalsum_node_str_and_dag_name(): _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", api="lfric", dist_mem=True, idx=0) - gsum = None for child in invoke.schedule.children: - if isinstance(child, GlobalSum): + if isinstance(child, GlobalReduction): gsum = child break assert gsum - output = gsum.node_str() - expected_output = (colored("GlobalSum", GlobalSum._colour) + + gred = GlobalReduction(gsum.scalar) + output = gred.node_str() + expected_output = (colored("GlobalReduction", GlobalReduction._colour) + "[scalar='asum']") assert expected_output in output - assert gsum.dag_name == "GlobalSum(asum)_1" - + assert gred.dag_name == "GlobalReduction(asum)_0" -def test_globalsum_children_validation(): - '''Test that children added to GlobalSum are validated. A GlobalSum node - does not accept any children. - ''' +def test_globalreduction_children_validation(): + '''Test that a GlobalReduction does not accept any children.''' _, invoke = get_invoke("15.9.1_X_innerproduct_Y_builtin.f90", api="lfric", idx=0, dist_mem=True) gsum = None for child in invoke.schedule.children: - if isinstance(child, GlobalSum): + if isinstance(child, GlobalReduction): gsum = child break with pytest.raises(GenerationError) as excinfo: gsum.addchild(Literal("2", INTEGER_TYPE)) - assert ("Item 'Literal' can't be child 0 of 'GlobalSum'. GlobalSum is a" - " LeafNode and doesn't accept children.") in str(excinfo.value) + assert ("Item 'Literal' can't be child 0 of 'LFRicGlobalSum'. " + "LFRicGlobalSum is a LeafNode and doesn't accept children." + in str(excinfo.value)) def test_globalsum_nodm_error(): - ''' Check that an instance of the GlobalSum class raises an + ''' Check that an instance of the GlobalReduction class raises an exception if it is instantiated with no distributed memory enabled (dm=False). We use the LFRic API to test this. @@ -104,14 +102,14 @@ def test_globalsum_nodm_error(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(GenerationError) as err: - _ = GlobalSum(argument) - assert ("It makes no sense to create a GlobalSum object when " + _ = GlobalReduction(argument) + assert ("It makes no sense to create a GlobalReduction object when " "distributed memory is not enabled (dm=False)." in str(err.value)) -def test_globalsum_unsupported_argument(): - ''' Check that an instance of the GlobalSum class raises an +def test_globalreduction_unsupported_argument(): + ''' Check that an instance of the GlobalReduction class raises an exception for an unsupported argument type. ''' # Get an instance of a non-scalar argument _, invoke = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", @@ -121,14 +119,14 @@ def test_globalsum_unsupported_argument(): kernel = loop.loop_body[0] argument = kernel.arguments.args[0] with pytest.raises(InternalError) as err: - _ = GlobalSum(argument) - assert ("GlobalSum.init(): A global sum argument should be a scalar " - "but found argument of type 'gh_field'." in str(err.value)) + _ = GlobalReduction(argument) + assert ("GlobalReduction.init(): A global reduction argument should be a " + "scalar but found argument of type 'gh_field'." in str(err.value)) -def test_globalsum_arg(): - ''' Check that the globalsum argument is defined as gh_readwrite and - points to the GlobalSum node ''' +def test_globalreduction_arg(): + ''' Check that the global-reduction argument is defined as gh_readwrite and + points to the GlobalReduction node ''' _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", api="lfric", idx=0, dist_mem=True) schedule = invoke.schedule @@ -138,8 +136,8 @@ def test_globalsum_arg(): assert glob_sum_arg.call == glob_sum -def test_globalsum_args(): - '''Test that the globalsum class args method returns the appropriate +def test_globalreduction_args(): + '''Test that the globalreduction class args method returns the appropriate argument ''' _, invoke = get_invoke("15.14.3_sum_setval_field_builtin.f90", api="lfric", dist_mem=True, idx=0) From 6ae28cc736221a71346e39dc18e623ab4f450c75 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 21:25:30 +0000 Subject: [PATCH 13/15] #2381 add separate test file for LFRicInvoke --- src/psyclone/domain/lfric/lfric_invoke.py | 42 ++--- .../tests/domain/lfric/lfric_invoke_test.py | 165 ++++++++++++++++++ src/psyclone/tests/lfric_test.py | 110 ------------ .../lfric/15.10.9_min_max_X_builtin.f90 | 7 +- 4 files changed, 191 insertions(+), 133 deletions(-) create mode 100644 src/psyclone/tests/domain/lfric/lfric_invoke_test.py diff --git a/src/psyclone/domain/lfric/lfric_invoke.py b/src/psyclone/domain/lfric/lfric_invoke.py index f8c466469f..6411adc1e2 100644 --- a/src/psyclone/domain/lfric/lfric_invoke.py +++ b/src/psyclone/domain/lfric/lfric_invoke.py @@ -40,10 +40,15 @@ ''' This module implements the LFRic-specific implementation of the Invoke base class from psyGen.py. ''' +from typing import TYPE_CHECKING + from psyclone.configuration import Config from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn +if TYPE_CHECKING: # pragma: no cover + from psyclone.domain.lfric.lfric_invokes import LFRicInvokes from psyclone.domain.lfric.lfric_loop import LFRicLoop -from psyclone.errors import GenerationError, FieldNotFoundError +from psyclone.errors import FieldNotFoundError, GenerationError, InternalError +from psyclone.parse.algorithm import InvokeCall from psyclone.psyGen import Invoke from psyclone.psyir.nodes import Assignment, Reference, Call, Literal from psyclone.psyir.symbols import ( @@ -57,26 +62,25 @@ class LFRicInvoke(Invoke): require. :param alg_invocation: object containing the invoke call information. - :type alg_invocation: :py:class:`psyclone.parse.algorithm.InvokeCall` - :param int idx: the position of the invoke in the list of invokes - contained in the Algorithm. - :param invokes: the Invokes object containing this LFRicInvoke - object. - :type invokes: :py:class:`psyclone.domain.lfric.LFRicInvokes` + :param idx: the position of the invoke in the list of invokes + contained in the Algorithm. + :param invokes: the Invokes object containing this LFRicInvoke. :raises GenerationError: if integer reductions are required in the - PSy-layer. - :raises GenerationError: if a global reduction operation other than sum - is required - TODO #2381. + PSy-layer. + :raises InternalError: if an unrecognised global reduction operation + is encountered. ''' # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-locals - def __init__(self, alg_invocation, idx, invokes): + def __init__(self, + alg_invocation: InvokeCall, + idx: int, + invokes: "LFRicInvokes"): # Import here to avoid circular dependency # pylint: disable=import-outside-toplevel from psyclone.domain.lfric import LFRicInvokeSchedule - Invoke.__init__(self, alg_invocation, idx, LFRicInvokeSchedule, - invokes) + super().__init__(alg_invocation, idx, LFRicInvokeSchedule, invokes) # The base class works out the algorithm code's unique argument # list and stores it in the 'self._alg_unique_args' @@ -92,9 +96,8 @@ def __init__(self, alg_invocation, idx, invokes): LFRicMeshes, LFRicBoundaryConditions, LFRicProxies, LFRicMeshProperties) from psyclone.domain.lfric import ( - LFRicCellIterators, LFRicGlobalMax, LFRicGlobalSum, LFRicGlobalMin, - LFRicHaloDepths, LFRicLoopBounds, - LFRicRunTimeChecks, + LFRicCellIterators, LFRicGlobalMax, LFRicGlobalMin, LFRicGlobalSum, + LFRicHaloDepths, LFRicLoopBounds, LFRicRunTimeChecks, LFRicScalarArgs, LFRicScalarArrayArgs, LFRicFields, LFRicDofmaps, LFRicStencils) @@ -198,10 +201,9 @@ def __init__(self, alg_invocation, idx, invokes): global_red = LFRicGlobalMax(kern.reduction_arg, parent=loop.parent) else: - raise GenerationError( - f"TODO #2381 - currently only global SUM/MAX/MIN " - f"reductions are supported but kernel '{kern.name}' " - f"performs a {kern.reduction_type}") + raise InternalError( + f"Unrecognised reduction '{kern.reduction_type}' " + f"found for kernel '{kern.name}'.") loop.parent.children.insert(loop.position+1, global_red) # Add the halo depth(s) for any kernel(s) that operate in the halos diff --git a/src/psyclone/tests/domain/lfric/lfric_invoke_test.py b/src/psyclone/tests/domain/lfric/lfric_invoke_test.py new file mode 100644 index 0000000000..dac8a32e19 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_invoke_test.py @@ -0,0 +1,165 @@ +import pytest + + +from psyclone.configuration import Config +from psyclone.domain.lfric import ( + FunctionSpace, LFRicConstants, LFRicGlobalMax, LFRicGlobalSum) +from psyclone.psyir.nodes import Loop +from psyclone.psyir.transformations import OMPParallelTrans +from psyclone.errors import GenerationError, InternalError +from psyclone.tests.lfric_build import LFRicBuild +from psyclone.tests.utilities import get_invoke +from psyclone.transformations import LFRicOMPLoopTrans + +TEST_API = "lfric" + + +def test_lfricinvoke_first_access(): + ''' Tests that we raise an error if LFRicInvoke.first_access(name) is + called for an argument name that doesn't exist ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + with pytest.raises(GenerationError) as excinfo: + invoke.first_access("not_an_arg") + assert ("Failed to find any kernel argument with name" + in str(excinfo.value)) + + +def test_lfricinvoke_arg_for_fs(): + ''' Test that LFRicInvoke.arg_for_funcspace() raises an error if + passed an invalid or unused function space. + + ''' + _, invoke = get_invoke("1_single_invoke.f90", api=TEST_API, idx=0, + dist_mem=True) + with pytest.raises(InternalError) as err: + _ = invoke.arg_for_funcspace(FunctionSpace("waah", "waah")) + const = LFRicConstants() + assert (f"Unrecognised function space 'waah'. The supported spaces are " + f"{const.VALID_FUNCTION_SPACE_NAMES}" in str(err.value)) + with pytest.raises(GenerationError) as excinfo: + invoke.arg_for_funcspace(FunctionSpace("wtheta", None)) + assert "No argument found on 'wtheta' space" in str(excinfo.value) + + +def test_lfricinvoke_uniq_declns_intent_inv_argtype(): + ''' Tests that we raise an error when LFRicInvoke.unique_declns_by_intent() + is called with at least one invalid argument type. ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", + api=TEST_API, dist_mem=True, idx=0) + with pytest.raises(InternalError) as excinfo: + invoke.unique_declns_by_intent(["gh_invalid"]) + const = LFRicConstants() + assert (f"Invoke.unique_declns_by_intent() called with at least one " + f"invalid argument type. Expected one of " + f"{const.VALID_ARG_TYPE_NAMES} but found ['gh_invalid']." + in str(excinfo.value)) + + +def test_lfricinvoke_uniq_declns_intent_invalid_intrinsic(): + ''' Tests that we raise an error when Invoke.unique_declns_by_intent() + is called for an invalid intrinsic type. ''' + _, invoke = get_invoke("1.7_single_invoke_3scalar.f90", idx=0, + api=TEST_API, dist_mem=True) + with pytest.raises(InternalError) as excinfo: + invoke.unique_declns_by_intent(["gh_scalar"], intrinsic_type="triple") + const = LFRicConstants() + assert (f"Invoke.unique_declns_by_intent() called with an invalid " + f"intrinsic argument data type. Expected one of " + f"{const.VALID_INTRINSIC_TYPES} but found 'triple'." + in str(excinfo.value)) + + +def test_lfricinvoke_uniq_declns_intent_ops(tmp_path): + ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct + list of arguments for operator arguments. ''' + psy, invoke = get_invoke("4.4_multikernel_invokes.f90", idx=0, + api=TEST_API, dist_mem=True) + args = invoke.unique_declns_by_intent(["gh_operator"]) + assert args['inout'] == [] + args_out = [arg.declaration_name for arg in args['out']] + assert args_out == ['op'] + assert args['in'] == [] + + assert LFRicBuild(tmp_path).code_compiles(psy) + + +def test_lfricinvoke_uniq_declns_intent_cma_ops(tmp_path): + ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct + list of arguments for columnwise operator arguments. ''' + psy, invoke = get_invoke("20.5_multi_cma_invoke.f90", idx=0, + api=TEST_API, dist_mem=True) + args = invoke.unique_declns_by_intent(["gh_columnwise_operator"]) + args_out = [arg.declaration_name for arg in args['out']] + assert args_out == ['cma_op1'] + args_inout = [arg.declaration_name for arg in args['inout']] + assert args_inout == ['cma_opc'] + args_in = [arg.declaration_name for arg in args['in']] + assert args_in == ['cma_opb'] + + assert LFRicBuild(tmp_path).code_compiles(psy) + + +def test_lfricinvoke_global_reductions(): + ''' + Check the construction of an LFRicInvoke containing a GlobalSum. + ''' + _, invoke = get_invoke("15.9.2_X_innerproduct_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert isinstance(invoke.schedule[1], LFRicGlobalSum) + _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert isinstance(invoke.schedule[4], LFRicGlobalMax) + + +def test_lfricinvoke_setup_psy_layer_symbols(monkeypatch, dist_mem): + ''' + Tests for the setup_psy_layer_symbols() method. + ''' + config = Config.get() + monkeypatch.setattr(config, "_reproducible_reductions", True) + _, invoke = get_invoke("15.9.2_X_innerproduct_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=dist_mem) + schedule = invoke.schedule + otrans = LFRicOMPLoopTrans() + rtrans = OMPParallelTrans() + # Apply an OpenMP do to the loop + for child in schedule.children: + if isinstance(child, Loop): + otrans.apply(child, {"reprod": True}) + # Apply an OpenMP Parallel for all loops + rtrans.apply(schedule.children[0:2]) + # Check that setup_psy_layer_symbols() populates the symbol table. + assert "f1_proxy" not in invoke.schedule.symbol_table + invoke.setup_psy_layer_symbols() + assert "f1_proxy" in invoke.schedule.symbol_table + assert invoke.schedule.symbol_table.lookup_with_tag("omp_num_threads") + assert "omp_get_max_threads" in invoke.schedule.symbol_table + + +def test_lfricinvoke_invalid_reduction(monkeypatch): + ''' + Check that the LFRicInvoke constructor raises the expected error if it + encounters an unknown type of reduction. + + ''' + # This is not easy to trigger so we resort to monkeypatching the definition + # of one of the kernels to give it an invalid reduction type. + from psyclone.domain.lfric.lfric_builtins import LFRicMaxvalXKern + monkeypatch.setattr(LFRicMaxvalXKern, "_reduction_type", "wrong") + + with pytest.raises(InternalError) as err: + _ = get_invoke("15.10.9_min_max_X_builtin.f90", idx=0, + api=TEST_API, dist_mem=True) + assert ("Unrecognised reduction 'wrong' found for kernel 'maxval_x'" + in str(err.value)) + + +def test_lfricinvoke_halo_depths(): + ''' + Test that the construction of an LFRicInvoke sets up the symbols + holding the various halo depths. + ''' + _, invoke = get_invoke("1.4_into_halos_invoke.f90", idx=0, + api=TEST_API, dist_mem=True) + assert invoke._alg_unique_halo_depth_args == ["hdepth"] diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index d81e126ea2..11e7f27792 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -614,100 +614,6 @@ def test_invoke_uniq_declns_valid_access(): assert fields_proxy_readwritten == ["f1_proxy"] -def test_lfricinvoke_first_access(): - ''' Tests that we raise an error if LFRicInvoke.first_access(name) is - called for an argument name that doesn't exist ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(GenerationError) as excinfo: - psy.invokes.invoke_list[0].first_access("not_an_arg") - assert ("Failed to find any kernel argument with name" - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_inv_argtype(): - ''' Tests that we raise an error when LFRicInvoke.unique_declns_by_intent() - is called with at least one invalid argument type. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(InternalError) as excinfo: - psy.invokes.invoke_list[0].unique_declns_by_intent(["gh_invalid"]) - const = LFRicConstants() - assert (f"Invoke.unique_declns_by_intent() called with at least one " - f"invalid argument type. Expected one of " - f"{const.VALID_ARG_TYPE_NAMES} but found ['gh_invalid']." - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_invalid_intrinsic(): - ''' Tests that we raise an error when Invoke.unique_declns_by_intent() - is called for an invalid intrinsic type. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(InternalError) as excinfo: - psy.invokes.invoke_list[0].unique_declns_by_intent( - ["gh_scalar"], intrinsic_type="triple") - const = LFRicConstants() - assert (f"Invoke.unique_declns_by_intent() called with an invalid " - f"intrinsic argument data type. Expected one of " - f"{const.VALID_INTRINSIC_TYPES} but found 'triple'." - in str(excinfo.value)) - - -def test_lfricinvoke_uniq_declns_intent_ops(tmpdir): - ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct - list of arguments for operator arguments. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "4.4_multikernel_invokes.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - args = psy.invokes.invoke_list[0].unique_declns_by_intent(["gh_operator"]) - assert args['inout'] == [] - args_out = [arg.declaration_name for arg in args['out']] - assert args_out == ['op'] - assert args['in'] == [] - - assert LFRicBuild(tmpdir).code_compiles(psy) - - -def test_lfricinvoke_uniq_declns_intent_cma_ops(tmpdir): - ''' Tests that LFRicInvoke.unique_declns_by_intent() returns the correct - list of arguments for columnwise operator arguments. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "20.5_multi_cma_invoke.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - args = psy.invokes.invoke_list[0]\ - .unique_declns_by_intent(["gh_columnwise_operator"]) - args_out = [arg.declaration_name for arg in args['out']] - assert args_out == ['cma_op1'] - args_inout = [arg.declaration_name for arg in args['inout']] - assert args_inout == ['cma_opc'] - args_in = [arg.declaration_name for arg in args['in']] - assert args_in == ['cma_opb'] - - assert LFRicBuild(tmpdir).code_compiles(psy) - - -def test_lfricinvoke_arg_for_fs(): - ''' Tests that we raise an error when LFRicInvoke.arg_for_funcspace() is - called for an unused space. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - with pytest.raises(GenerationError) as excinfo: - psy.invokes.invoke_list[0].arg_for_funcspace(FunctionSpace("wtheta", - None)) - assert "No argument found on 'wtheta' space" in str(excinfo.value) - - def test_kernel_specific(tmpdir): ''' Test that a call to enforce boundary conditions is *not* added following a call to the matrix_vector_kernel_type kernel. Boundary @@ -2400,22 +2306,6 @@ def test_func_descriptor_str(): assert output in func_str -def test_lfrickern_arg_for_fs(): - ''' Test that LFRicInvoke.arg_for_funcspace() raises an error if - passed an invalid function space. - - ''' - _, invoke_info = parse(os.path.join(BASE_PATH, "1_single_invoke.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) - first_invoke = psy.invokes.invoke_list[0] - with pytest.raises(InternalError) as err: - _ = first_invoke.arg_for_funcspace(FunctionSpace("waah", "waah")) - const = LFRicConstants() - assert (f"Unrecognised function space 'waah'. The supported spaces are " - f"{const.VALID_FUNCTION_SPACE_NAMES}" in str(err.value)) - - def test_dist_memory_true(): ''' Test that the distributed memory flag is on by default. ''' Config._instance = None diff --git a/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 b/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 index e83f626a0a..26707bed7e 100644 --- a/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 +++ b/src/psyclone/tests/test_files/lfric/15.10.9_min_max_X_builtin.f90 @@ -36,8 +36,8 @@ program single_invoke - ! Description: single point-wise operation (min/max of field elements) - ! specified in an invoke call. + ! Description: three point-wise operations (setval, min and max of field + ! elements) specified in an invoke call. use constants_mod, only: r_def use field_mod, only: field_type @@ -46,7 +46,8 @@ program single_invoke type(field_type) :: f1 real(r_def) :: amin, amax - call invoke( minval_X(amin, f1), & + call invoke( setval_C(f1, 1.0_r_def), & + minval_X(amin, f1), & maxval_X(amax, f1) ) end program single_invoke From dcc9fb20c188d838a407bcd231166fcd2c07cfb6 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 21:27:05 +0000 Subject: [PATCH 14/15] #2381 fix builtins test --- src/psyclone/tests/domain/lfric/lfric_builtins_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py index b9f1c42263..d879d182d4 100644 --- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py @@ -1995,14 +1995,14 @@ def test_minmaxval_x(fortran_writer, tmp_path): _, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, dist_mem=False) kerns = invoke.schedule.kernels() - assert str(kerns[0]) == ("Built-in: minval_X (compute the global minimum " + assert str(kerns[1]) == ("Built-in: minval_X (compute the global minimum " "value contained in a field)") - code = fortran_writer(kerns[0]) + code = fortran_writer(kerns[1]) assert "amin = MIN(amin, f1_data(df))" in code, code - assert str(kerns[1]) == ("Built-in: maxval_X (compute the global maximum " + assert str(kerns[2]) == ("Built-in: maxval_X (compute the global maximum " "value contained in a field)") - code = fortran_writer(kerns[1]) + code = fortran_writer(kerns[2]) assert "amax = MAX(amax, f1_data(df))" in code, code psy, invoke = get_invoke("15.10.9_min_max_X_builtin.f90", api=API, idx=0, From a5dbcf3342756d154714f3950c23495e37f4b0ec Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Feb 2026 22:01:42 +0000 Subject: [PATCH 15/15] #2381 begin test file for LFRicGlobalMin [skip ci] --- src/psyclone/domain/lfric/lfric_global_min.py | 11 +++++------ src/psyclone/domain/lfric/lfric_global_sum.py | 7 ++----- .../tests/domain/lfric/lfric_global_min_test.py | 7 +++++++ 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 src/psyclone/tests/domain/lfric/lfric_global_min_test.py diff --git a/src/psyclone/domain/lfric/lfric_global_min.py b/src/psyclone/domain/lfric/lfric_global_min.py index 8d0b4bd4bc..eaeae13e86 100644 --- a/src/psyclone/domain/lfric/lfric_global_min.py +++ b/src/psyclone/domain/lfric/lfric_global_min.py @@ -1,8 +1,9 @@ from psyclone.domain.common.psylayer import GlobalReduction +from psyclone.lfric import LFRicKernelArgument from psyclone.errors import GenerationError from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( - Assignment, Call, Reference, StructureReference) + Assignment, Call, Node, Reference, StructureReference) from psyclone.psyir.nodes.node import Node from psyclone.psyir.symbols import ( ContainerSymbol, DataSymbol, DataTypeSymbol, ImportInterface, @@ -15,17 +16,15 @@ class LFRicGlobalMin(GlobalReduction): manipulated in a schedule. :param scalar: the kernel argument for which to perform a global min. - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` :param parent: the parent node of this node in the PSyIR. - :type parent: :py:class:`psyclone.psyir.nodes.Node` - :raises InternalError: if the supplied argument is not a scalar. - :raises GenerationError: if the scalar is not of "real" intrinsic type. + :raises GenerationError: if the scalar is not of "real" or "integer" + intrinsic type. ''' _text_name = "LFRicGlobalMin" - def __init__(self, scalar, parent=None): + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): # Initialise the parent class super().__init__(scalar, parent=parent) diff --git a/src/psyclone/domain/lfric/lfric_global_sum.py b/src/psyclone/domain/lfric/lfric_global_sum.py index bdcaf39fcf..4e3e109e75 100644 --- a/src/psyclone/domain/lfric/lfric_global_sum.py +++ b/src/psyclone/domain/lfric/lfric_global_sum.py @@ -1,5 +1,6 @@ from psyclone.domain.common.psylayer.global_sum import GlobalReduction from psyclone.errors import GenerationError +from psyclone.lfric import LFRicKernelArgument from psyclone.psyGen import InvokeSchedule from psyclone.psyir.nodes import ( Assignment, Call, Reference, StructureReference) @@ -15,18 +16,14 @@ class LFRicGlobalSum(GlobalReduction): manipulated in a schedule. :param scalar: the kernel argument for which to perform a global sum. - :type scalar: :py:class:`psyclone.lfric.LFRicKernelArgument` :param parent: the parent node of this node in the PSyIR. - :type parent: :py:class:`psyclone.psyir.nodes.Node` - :raises GenerationError: if distributed memory is not enabled. - :raises InternalError: if the supplied argument is not a scalar. :raises GenerationError: if the scalar is not of "real" intrinsic type. ''' _text_name = "LFRicGlobalSum" - def __init__(self, scalar, parent=None): + def __init__(self, scalar: LFRicKernelArgument, parent: Node = None): # Initialise the parent class super().__init__(scalar, parent=parent) # Check scalar intrinsic types that this class supports (only diff --git a/src/psyclone/tests/domain/lfric/lfric_global_min_test.py b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py new file mode 100644 index 0000000000..e7331325e0 --- /dev/null +++ b/src/psyclone/tests/domain/lfric/lfric_global_min_test.py @@ -0,0 +1,7 @@ +from psyclone.domain.lfric_global_min import LFRicGlobalMin + + +def test_lfricglobalmin(): + ''' + ''' + lgm = LFRicGlobalMin(arg)