Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c871676
#2381 add initial GlobalReduction class
arporter Oct 3, 2025
b0c458c
#2381 linting
arporter Oct 3, 2025
0d5dadf
#2381 mv LFRicGlobalSum out of lfric.py and rename
arporter Oct 6, 2025
6856abd
#2381 fix linting
arporter Oct 6, 2025
279ec53
#2381 ensure any pre-existing Config is wiped by test fixture
arporter Oct 7, 2025
4a2f3d4
#2381 WIP fixing/moving tests [skip ci]
arporter Oct 7, 2025
9eb6784
#2381 fix all tests
arporter Oct 7, 2025
5921958
#2381 tidy and improve tests
arporter Oct 7, 2025
de51056
Merge branch 'master' into 2381_arp_global_reduction
arporter Oct 7, 2025
49b856b
#2381 get full unit coverage for global_reduction
arporter Oct 7, 2025
6aa870c
Merge branch 'master' into 2381_arp_global_reduction
arporter Oct 7, 2025
355a900
#2381 update from global sum to global reduction
arporter Oct 7, 2025
88956f8
#2381 fix linting
arporter Oct 7, 2025
488d0a5
#2381 tidy test file
arporter Oct 9, 2025
c27c9ab
Merge branch 'master' into 2381_arp_global_reduction
arporter Oct 13, 2025
7fe581b
Merge branch '2381_arp_global_reduction' of github.com:stfc/PSyclone …
arporter Oct 13, 2025
5b4d364
Merge branch 'master' into 2381_arp_global_reduction
arporter Oct 15, 2025
faa440b
Merge branch '2381_arp_global_reduction' of github.com:stfc/PSyclone …
arporter Oct 15, 2025
d26c56c
#2381 update docs
arporter Oct 15, 2025
ae2fb6d
Merge branch 'master' into 2381_arp_global_reduction
arporter Nov 4, 2025
c6d6366
Merge branch 'master' into 2381_arp_global_reduction
arporter Nov 5, 2025
226cfeb
#2381 rejig class hierarchy
arporter Nov 5, 2025
134954d
#2381 start adding an LFRicGlobalMin to test new structure
arporter Nov 5, 2025
72dd101
#2381 tidy for lint
arporter Nov 5, 2025
cf9c5c3
Merge branch 'master' into 2381_no_domain_global_reduction
arporter Nov 13, 2025
fead32a
Merge branch 'master' into 2381_no_domain_global_reduction
arporter Nov 14, 2025
5a0654a
#2381 WIP updating lower_to_language_level [skip-ci]
arporter Nov 14, 2025
eee9422
Merge branch 'master' into 2381_no_domain_global_reduction
arporter Dec 12, 2025
9fc397b
#2381 flesh out LFRicGlobalMin
arporter Dec 12, 2025
0523346
#2381 make dag_name more general
arporter Dec 12, 2025
4bceba7
#2381 mv common init functionality to parent class
arporter Dec 15, 2025
833782e
Merge branch 'master' into 2381_no_domain_global_reduction
arporter Dec 16, 2025
438e405
#2381 add MIN and MAX AccessTypes and appropriate init in Kern
arporter Dec 16, 2025
3d53e98
#2381 tidy-up the reduction classes
arporter Dec 16, 2025
b460b3c
#2381 fix failing test
arporter Dec 16, 2025
c475f0c
#2381 WIP fixing tests after extending access-modes [skip ci]
arporter Dec 16, 2025
64874cd
#2381 fix all LFRic tests
arporter Dec 17, 2025
650f584
#2381 use template for max/min reduction
arporter Dec 17, 2025
8aeffda
#2381 fix linting
arporter Dec 17, 2025
e5e9af5
#2381 fix cov of psyGen
arporter Dec 17, 2025
bd0c70a
#2381 rationalise class structure
arporter Dec 17, 2025
0eea98f
#2381 update docstrings and copyright
arporter Dec 17, 2025
19e1982
#2381 mv mpi-obj init into LFRicProxies
arporter Dec 17, 2025
e672b5a
#2381 WIP moving mpi-related setup to LFRicProxies class [skip ci]
arporter Dec 17, 2025
dfde2c6
#2381 fix lint [skip ci]
arporter Dec 17, 2025
a7cbe5e
#2381 begin adding min/max builtins [skip ci]
arporter Dec 18, 2025
3c6187e
#2381 WIP on new builtins
arporter Dec 18, 2025
3af3abb
#2381 revert reduction-related changes
arporter Dec 18, 2025
2f49897
#3263 fix remaining tests
arporter Dec 18, 2025
e3dc204
#3263 document new builtins
arporter Dec 18, 2025
60db4b9
#3263 improve testing
arporter Dec 18, 2025
abd054f
#3263 rm the min_max version and ensure code-gen is rejected when DM …
arporter Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/psyclone.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ FORTRAN_STANDARD = f2008
# ==================================
[lfric]
access_mapping = gh_read: read, gh_write: write, gh_readwrite: readwrite,
gh_inc: inc, gh_readinc: readinc, gh_sum: sum
gh_inc: inc, gh_readinc: readinc, gh_sum: sum,
gh_min: min, gh_max: max
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tab to spaces :)


# Specify whether we compute annexed dofs when a kernel is written so
# that it iterates over dofs. This is currently only the case for
Expand Down
31 changes: 30 additions & 1 deletion doc/user_guide/lfric.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2797,7 +2797,8 @@ are listed in the table below.
+===============+=====================+================+====================+
| GH_SCALAR | GH_INTEGER | n/a | GH_READ |
+---------------+---------------------+----------------+--------------------+
| GH_SCALAR | GH_REAL | n/a | GH_READ, GH_SUM |
| GH_SCALAR | GH_REAL | n/a | GH_READ, GH_SUM, |
| | | | GH_MIN, GH_MAX |
Comment on lines +2800 to +2801
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why do we need GH_SUM, GH_MIN or GH_MAX. These is the output scalar of the built-in which will only be written to, Why not GH_WRITE?

+---------------+---------------------+----------------+--------------------+
| GH_FIELD | GH_REAL, GH_INTEGER | ANY_SPACE_<n> | GH_READ, GH_WRITE, |
| | | | GH_READWRITE |
Expand Down Expand Up @@ -3455,6 +3456,34 @@ the same field (``X = min(a, X)``)::

field(:) = MIN(rscalar, field(:))

Global minimum and maximum field-element values
###############################################

Built-ins which scan through all elements of a field and return its
maximum or minimum value.

.. warning::
Support for these built-ins is not yet complete and therefore they
cannot currently be used. TODO #2381.

minval_X
^^^^^^^^

**minval_X** (*rscalar*, **field**)

Returns the minimum value held in the field *field*::

rscalar = MINVAL(field(:))

maxval_X
^^^^^^^^

**maxval_X** (*rscalar*, **field**)

Returns the maximum value held in the field *field*::

rscalar = MAXVAL(field(:))

Conversion of ``real`` field elements
#####################################

Expand Down
2 changes: 1 addition & 1 deletion src/psyclone/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ def __init__(self, section):
# having to specify those mappings.
self._access_mapping = {"read": "read", "write": "write",
"readwrite": "readwrite", "inc": "inc",
"sum": "sum"}
"sum": "sum", "min": "min", "max": "max"}
# Get the mapping if one exists and convert it into a
# dictionary. The input is in the format: key1:value1,
# key2=value2, ...
Expand Down
10 changes: 7 additions & 3 deletions src/psyclone/core/access_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

'''This module implements the AccessType used throughout PSyclone.'''

from __future__ import annotations
from enum import Enum
from psyclone.configuration import Config

Expand Down Expand Up @@ -75,6 +76,10 @@ class AccessType(Enum):
#: is available at compile-time and can be used for type properties such
#: as kinds or dimensions.
CONSTANT = 10
#: Is the output of a MIN reduction (i.e. global minimum value).
MIN = 11
#: Is the output of a MAX reduction (i.e. global maximum value).
MAX = 12
Comment on lines +79 to +82
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again MIN/MAX is not an AccessType (same with the pre-existing SUM). It is even more confusing that this refer to the output scalar, which is just written to. So why do we differentiate them?


def __str__(self) -> str:
'''Convert to a string representation, returning just the
Expand Down Expand Up @@ -132,12 +137,11 @@ def all_read_accesses():
AccessType.READINC]

@staticmethod
def get_valid_reduction_modes():
def get_valid_reduction_modes() -> list[AccessType]:
'''
:returns: A list of valid reduction access modes.
:rtype: List of py:class:`psyclone.core.access_type.AccessType`.
'''
return [AccessType.SUM]
return [AccessType.SUM, AccessType.MIN, AccessType.MAX]

@staticmethod
def get_valid_reduction_names():
Expand Down
95 changes: 91 additions & 4 deletions src/psyclone/domain/lfric/lfric_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ def _validate(self):
write_count = 0 # Only one argument must be written to
field_count = 0 # We must have one or more fields as arguments
spaces = set() # All field arguments must be on the same space
# Built-ins update fields DoF by DoF and therefore can have
# WRITE/READWRITE access
write_access_modes = AccessType.get_valid_reduction_modes() + [
AccessType.WRITE, AccessType.READWRITE]
# Field data types must be the same except for the conversion built-ins
data_types = set()
for arg in self.arg_descriptors:
Expand All @@ -328,10 +332,8 @@ def _validate(self):
f"must have one of {const.VALID_BUILTIN_DATA_TYPES} as "
f"a data type but kernel '{self.name}' has an argument "
f"of data type '{arg.data_type}'.")
# Built-ins update fields DoF by DoF and therefore can have
# WRITE/READWRITE access
if arg.access in [AccessType.WRITE, AccessType.SUM,
AccessType.READWRITE]:
# Check for write accesses
if arg.access in write_access_modes:
write_count += 1
if arg.argument_type in const.VALID_FIELD_NAMES:
field_count += 1
Expand Down Expand Up @@ -2743,6 +2745,88 @@ def lower_to_language_level(self) -> Node:
# Create assignment and replace node
return self._replace_with_assignment(lhs, rhs)

# ------------------------------------------------------------------- #
# ============ Minimum, maximum value of real field elements) ======= #
# ------------------------------------------------------------------- #


class LFRicMinvalXKern(LFRicBuiltIn):
'''
Computes the (global) minimum scalar value held in
the supplied field.
'''
_case_name = "minval_X"
_datatype = "real"

@classmethod
def metadata(cls) -> LFRicKernelMetadata:
"""
:returns: kernel metadata describing this built-in.
"""
return cls._builtin_metadata([
ScalarArgMetadata("gh_real", "gh_min"),
FieldArgMetadata("gh_real", "gh_read", "any_space_1")])

def __str__(self):
return (f"Built-in: {self._case_name} (compute the global minimum "
f"value contained in a field)")

def lower_to_language_level(self) -> Node:
'''
Lowers this LFRic-specific built-in kernel to language-level PSyIR.
This BuiltIn node is replaced by an Assignment node.

:returns: the lowered version of this node.

'''
super().lower_to_language_level()
# Get indexed references for the field (proxy) argument.
arg_refs = self.get_indexed_field_argument_references()
# Get a reference for the kernel scalar reduction argument.
lhs = self._reduction_reference()
minval = IntrinsicCall.create(IntrinsicCall.Intrinsic.MIN,
[lhs.copy(), arg_refs[0]])
return self._replace_with_assignment(lhs, minval)


class LFRicMaxvalXKern(LFRicBuiltIn):
'''
Computes the (global) maximum scalar value held in
the supplied field.
'''
_case_name = "maxval_X"
_datatype = "real"

@classmethod
def metadata(cls) -> LFRicKernelMetadata:
"""
:returns: kernel metadata describing this built-in.
"""
return cls._builtin_metadata([
ScalarArgMetadata("gh_real", "gh_max"),
FieldArgMetadata("gh_real", "gh_read", "any_space_1")])

def __str__(self):
return (f"Built-in: {self._case_name} (compute the global maximum "
f"value contained in a field)")

def lower_to_language_level(self) -> Node:
'''
Lowers this LFRic-specific built-in kernel to language-level PSyIR.
This BuiltIn node is replaced by an Assignment node.

:returns: the lowered version of this node.

'''
super().lower_to_language_level()
# Get indexed references for the field (proxy) argument.
arg_refs = self.get_indexed_field_argument_references()
# Get a reference for the kernel scalar reduction argument.
lhs = self._reduction_reference()
minval = IntrinsicCall.create(IntrinsicCall.Intrinsic.MAX,
[lhs.copy(), arg_refs[0]])
return self._replace_with_assignment(lhs, minval)

# ------------------------------------------------------------------- #
# ============== Converting real to integer field elements ========== #
# ------------------------------------------------------------------- #
Expand Down Expand Up @@ -3270,6 +3354,9 @@ def lower_to_language_level(self) -> Node:
# Minimum of a real scalar value and real field elements
"min_aX": LFRicMinAXKern,
"inc_min_aX": LFRicIncMinAXKern,
# Minimum and maximum values contained in a field
"minval_X": LFRicMinvalXKern,
"maxval_X": LFRicMaxvalXKern,
# Converting real to integer field elements
"real_to_int_X": LFRicRealToIntXKern,
# Converting real to real field elements
Expand Down
8 changes: 5 additions & 3 deletions src/psyclone/domain/lfric/lfric_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ def __init__(self) -> None:
# pylint: disable=too-many-instance-attributes

# Supported access types
# gh_sum for scalars is restricted to iterates_over == 'dof'
LFRicConstants.VALID_SCALAR_ACCESS_TYPES = ["gh_read", "gh_sum"]
# gh_sum/min/max for scalars is restricted to iterates_over == 'dof'
LFRicConstants.VALID_SCALAR_ACCESS_TYPES = ["gh_read", "gh_sum",
"gh_min", "gh_max"]
LFRicConstants.VALID_ARRAY_ACCESS_TYPES = ["gh_read"]
LFRicConstants.VALID_FIELD_ACCESS_TYPES = [
"gh_read", "gh_write", "gh_readwrite", "gh_inc", "gh_readinc"]
Expand All @@ -125,7 +126,8 @@ def __init__(self) -> None:
"gh_read", "gh_write", "gh_readwrite", "gh_inc", "gh_readinc"]

LFRicConstants.WRITE_ACCESSES = [
"gh_write", "gh_readwrite", "gh_inc", "gh_readinc", "gh_sum"]
"gh_write", "gh_readwrite", "gh_inc", "gh_readinc", "gh_sum",
"gh_min", "gh_max"]

# Supported LFRic API stencil types and directions
LFRicConstants.VALID_STENCIL_TYPES = ["x1d", "y1d", "xory1d", "cross",
Expand Down
7 changes: 6 additions & 1 deletion src/psyclone/domain/lfric/lfric_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class LFRicInvoke(Invoke):

: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.
'''
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-locals
Expand Down Expand Up @@ -185,6 +186,10 @@ def __init__(self, alg_invocation, idx, invokes):
arg_types=const.VALID_SCALAR_NAMES,
arg_accesses=AccessType.get_valid_reduction_modes(),
unique=True):
if scalar.access != AccessType.SUM:
raise GenerationError(
"TODO #2381 - currently only global *sum* "
"reductions are supported.")
global_sum = LFRicGlobalSum(scalar, parent=loop.parent)
loop.parent.children.insert(loop.position+1, global_sum)

Expand Down
33 changes: 33 additions & 0 deletions src/psyclone/parse/lfric_builtins_mod.f90
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,32 @@ module lfric_builtins_mod
procedure, nopass :: inc_min_aX_code
end type inc_min_aX

! ------------------------------------------------------------------- !
! ============ Minimum, maximum value of real field elements) ======= !
! ------------------------------------------------------------------- !

type, public, extends(kernel_type) :: minval_X
private
type(arg_type) :: meta_args(2) = (/ &
arg_type(GH_SCALAR, GH_REAL, GH_MIN ), &
arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) &
/)
integer :: operates_on = DOF
contains
procedure, nopass :: minval_X_code
end type minval_X

type, public, extends(kernel_type) :: maxval_X
private
type(arg_type) :: meta_args(2) = (/ &
arg_type(GH_SCALAR, GH_REAL, GH_MAX ), &
arg_type(GH_FIELD, GH_REAL, GH_READ, ANY_SPACE_1) &
/)
integer :: operates_on = DOF
contains
procedure, nopass :: maxval_X_code
end type maxval_X

! ------------------------------------------------------------------- !
! ============== Converting real to integer field elements ========== !
! ------------------------------------------------------------------- !
Expand Down Expand Up @@ -1169,6 +1195,13 @@ end subroutine min_aX_code
subroutine inc_min_aX_code()
end subroutine inc_min_aX_code

! Minimum and maximum values contained within a field
subroutine minval_X_code()
end subroutine minval_X_code

subroutine maxval_X_code()
end subroutine maxval_X_code

! Converting real to integer field elements
subroutine real_to_int_X_code()
end subroutine real_to_int_X_code
Expand Down
15 changes: 8 additions & 7 deletions src/psyclone/psyir/tools/reduction_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,18 @@ def _get_reduction_operator(self, node: Node) -> \
Union[BinaryOperation.Operator, IntrinsicCall.Intrinsic]:
'''
:param node: the node to match against.

:returns: the reduction operator at the root of the given
DataNode or None if there isn't one.
DataNode or None if there isn't one.
'''
if isinstance(node, BinaryOperation):
for op in self.red_ops:
if node.operator == op:
return node.operator
if node.operator in self.red_ops:
return node.operator

if isinstance(node, IntrinsicCall):
for op in self.red_ops:
if node.intrinsic == op:
return node.intrinsic
if node.intrinsic in self.red_ops:
return node.intrinsic

return None

@staticmethod
Expand Down
3 changes: 3 additions & 0 deletions src/psyclone/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ def setup_psyclone_config():
independent of a potential psyclone config file installed by
the user.
'''
# Ensure any Config object that has already been loaded is wiped.
Config._instance = None

config_file = Config.get_repository_config_file()

# In case that PSyclone is installed and tested (e.g. GitHub Actions),
Expand Down
12 changes: 8 additions & 4 deletions src/psyclone/tests/core/access_type_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ def test_api_specific_name():
assert AccessType.INQUIRY.api_specific_name() == "inquiry"
assert AccessType.CONSTANT.api_specific_name() == "constant"
assert AccessType.UNKNOWN.api_specific_name() == "unknown"
assert AccessType.get_valid_reduction_modes() == [AccessType.SUM]
assert AccessType.get_valid_reduction_names() == ["gh_sum"]
assert (set(AccessType.get_valid_reduction_modes()) ==
set([AccessType.SUM, AccessType.MIN, AccessType.MAX]))
assert (set(AccessType.get_valid_reduction_names()) ==
set(["gh_sum", "gh_min", "gh_max"]))
# Use set to make this independent of the order:
assert set(AccessType.all_write_accesses()) == set([AccessType.WRITE,
AccessType.READWRITE,
AccessType.INC,
AccessType.READINC,
AccessType.SUM])
AccessType.SUM,
AccessType.MAX,
AccessType.MIN])
assert set(AccessType.all_read_accesses()) == set([AccessType.READ,
AccessType.READWRITE,
AccessType.READINC,
Expand Down Expand Up @@ -112,7 +116,7 @@ def test_all_write_accesses():

all_write_accesses = AccessType.all_write_accesses()
assert isinstance(all_write_accesses, list)
assert len(all_write_accesses) == 5
assert len(all_write_accesses) == 7
assert (len(all_write_accesses) ==
len(set(all_write_accesses)))
assert all(isinstance(write_access, AccessType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

from fparser.two import Fortran2003

from psyclone.domain.lfric import LFRicConstants
from psyclone.domain.lfric.kernel import EvaluatorTargetsMetadata


Expand Down Expand Up @@ -145,7 +146,7 @@ def test_setter_errors():

with pytest.raises(ValueError) as info:
metadata.evaluator_targets = ["invalid"]
assert ("The 'evaluator_targets' metadata should be a recognised value "
"(one of ['w3', 'wtheta', 'w2v', 'w2vtrace', 'w2broken', 'w0', "
"'w1', 'w2', 'w2trace', 'w2h', 'w2htrace', 'any_w2', 'wchi']) "
"but found 'invalid'." in str(info.value))
const = LFRicConstants()
assert (f"The 'evaluator_targets' metadata should be a recognised value "
f"(one of {const.VALID_FUNCTION_SPACES}) but found "
f"'invalid'." in str(info.value))
Loading
Loading