Repository: stfc/PSyclone

Issue:
Extend integration tests to run 'LFRic' on GPU
Up to now, out testing of this locally has been ad-hoc. It would be good to capture what we have so far (optimisation script and compiler options) in the integration tests.

Currently however, I don't think that LFRic can be built with the latest NVIDIA compiler (24.5) without some source modifications to work around compiler bugs. Perhaps we could patch those in as part of the test?

Discussion:

Patch:
diff --git a/changelog b/changelog
index 9a07f4abf..0cf4c670b 100644
--- a/changelog
+++ b/changelog
@@ -223,6 +223,8 @@
 	conditional statements with many branches. Disable final tree copy
 	when generating code.
 
+	77) PR #2708 for #2663. Add LFRic OpenACC in the integration tests.
+
 release 2.5.0 14th of February 2024
 
 	1) PR #2199 for #2189. Fix bugs with missing maps in enter data
diff --git a/examples/lfric/scripts/KGOs/lfric_3269_nvidia.patch b/examples/lfric/scripts/KGOs/lfric_3269_nvidia.patch
new file mode 100644
index 000000000..d3e178318
--- /dev/null
+++ b/examples/lfric/scripts/KGOs/lfric_3269_nvidia.patch
@@ -0,0 +1,836 @@
+diff --git a/lfric/components/driver/source/driver_mesh_mod.f90 b/lfric/components/driver/source/driver_mesh_mod.f90
+index 2237d181..f075436d 100644
+--- a/lfric/components/driver/source/driver_mesh_mod.f90
++++ b/lfric/components/driver/source/driver_mesh_mod.f90
+@@ -168,7 +168,8 @@ subroutine init_mesh( configuration,           &
+   !============================================================================
+   if (present(alt_names)) then
+     if (size(alt_names) == size(mesh_names)) then
+-      allocate(names, source=alt_names)
++      !allocate(names, source=alt_names)
++      names=alt_names
+     else
+       write(log_scratch_space, '(A)')                   &
+           'Specified alternative mesh names to does '// &
+@@ -176,7 +177,8 @@ subroutine init_mesh( configuration,           &
+       call log_event(log_scratch_space, LOG_LEVEL_ERROR)
+     end if
+   else
+-    allocate(names, source=mesh_names)
++    !allocate(names, source=mesh_names)
++    names=mesh_names
+   end if
+ 
+ 
+diff --git a/lfric/infrastructure/build/compile.mk b/lfric/infrastructure/build/compile.mk
+index 19c9cff9..b5cd3014 100644
+--- a/lfric/infrastructure/build/compile.mk
++++ b/lfric/infrastructure/build/compile.mk
+@@ -140,18 +140,26 @@ $(LIB_DIR)/lib%.a: $$($$(shell basename $$* | tr a-z A-Z)_OBJS) | $(LIB_DIR)
+ .PRECIOUS: %.o
+ %.o: %.f90 | $(MOD_DIR)
+ 	$(call MESSAGE,Compile,$<)
++	$(eval TMP_MOD_DIR := $(shell mktemp -d))
++	mkdir -p $(TMP_MOD_DIR)
+ 	$(Q)$(TIME_TOOL) $(FC) $(FFLAGS_BASE) $(FFLAGS_EXTRA)\
+-	          $(MODULE_DESTINATION_ARGUMENT) \
++	          $(F_MOD_DESTINATION_ARG)$(TMP_MOD_DIR) \
+ 	          $(MODULE_SOURCE_ARGUMENT) \
+-	          $(INCLUDE_ARGS) -c -o $(basename $@).o $<
++	          $(INCLUDE_ARGS) -Imod -c -o $(basename $@).o $<
++	bash $(LFRIC_BUILD)/update_mod.sh $(TMP_MOD_DIR) $(MOD_DIR)
++	rm -rf $(TMP_MOD_DIR)
+ 	$(call MESSAGE,Compiled,$<)
+ 
+ %.o: %.F90 | $(MOD_DIR)
+ 	$(call MESSAGE,Pre-process and compile,$<)
++	$(eval TMP_MOD_DIR := $(shell mktemp -d))
++	mkdir -p $(TMP_MOD_DIR)
+ 	$(Q)$(TIME_TOOL) $(FC) $(FFLAGS_BASE) $(FFLAGS_EXTRA) \
+-	          $(MODULE_DESTINATION_ARGUMENT) \
++	          $(F_MOD_DESTINATION_ARG)$(TMP_MOD_DIR) \
+ 	          $(MODULE_SOURCE_ARGUMENT) \
+-	          $(INCLUDE_ARGS) $(MACRO_ARGS) -c -o $(basename $@).o $<
++	          $(INCLUDE_ARGS) -Imod $(MACRO_ARGS) -c -o $(basename $@).o $<
++	bash $(LFRIC_BUILD)/update_mod.sh $(TMP_MOD_DIR) $(MOD_DIR)
++	rm -rf $(TMP_MOD_DIR)
+ 	$(call MESSAGE,Compiled,$<)
+ 
+ 
+diff --git a/lfric/infrastructure/build/cxx/nvc++.mk b/lfric/infrastructure/build/cxx/nvc++.mk
+new file mode 100644
+index 00000000..13b17a10
+--- /dev/null
++++ b/lfric/infrastructure/build/cxx/nvc++.mk
+@@ -0,0 +1,9 @@
++##############################################################################
++# (c) Crown copyright 2017 Met Office. All rights reserved.
++# The file LICENCE, distributed with this code, contains details of the terms
++# under which the code may be used.
++##############################################################################
++
++$(info ** Chosen NVC++ compiler)
++
++CXX_RUNTIME_LIBRARY=stdc++
+diff --git a/lfric/infrastructure/build/fortran/nvfortran.mk b/lfric/infrastructure/build/fortran/nvfortran.mk
+new file mode 100644
+index 00000000..cfed52c1
+--- /dev/null
++++ b/lfric/infrastructure/build/fortran/nvfortran.mk
+@@ -0,0 +1,38 @@
++##############################################################################
++# Copyright (c) 2017,  Met Office, on behalf of HMSO and Queen's Printer
++# For further details please refer to the file LICENCE.original which you
++# should have received as part of this distribution.
++##############################################################################
++# Various things specific to the Portland Fortran compiler.
++##############################################################################
++#
++# This macro is evaluated now (:= syntax) so it may be used as many times as
++# desired without wasting time rerunning it.
++#
++F_MOD_DESTINATION_ARG = -module$(SPACE)
++OPENMP_ARG            = -mp
++
++FFLAGS_COMPILER           =
++FFLAGS_NO_OPTIMISATION    = -O0
++FFLAGS_SAFE_OPTIMISATION  = -O2
++FFLAGS_RISKY_OPTIMISATION = -O4
++FFLAGS_DEBUG              = -g -traceback
++FFLAGS_RUNTIME            = -Mchkptr -Mchkstk
++# Option for checking code meets Fortran standard (not available for PGI)
++FFLAGS_FORTRAN_STANDARD   =
++
++LDFLAGS_COMPILER = -g
++
++FPP = cpp -traditional-cpp
++FPPFLAGS = -P
++FC = mpif90
++
++# FS#34981 (nvbug 4648082)
++science/src/um/src/atmosphere/large_scale_precipitation/ls_ppnc.o: private FFLAGS_RUNTIME = -Mchkstk
++
++# FS#35751
++mesh/create_mesh_mod.o: private FFLAGS_RUNTIME = -Mchkstk
++
++# 24.3
++science/src/socrates/src/cosp_github/subsample_and_optics_example/optics/quickbeam_optics/optics_lib.o: private FFLAGS_SAFE_OPTIMISATION = -O1
++science/src/socrates/src/cosp_github/subsample_and_optics_example/optics/quickbeam_optics/optics_lib.o: private FFLAGS_RISKY_OPTIMISATION = -O1
+diff --git a/lfric/infrastructure/build/tools/DependencyRules b/lfric/infrastructure/build/tools/DependencyRules
+index 9d4db390..e37384fc 100755
+--- a/lfric/infrastructure/build/tools/DependencyRules
++++ b/lfric/infrastructure/build/tools/DependencyRules
+@@ -101,6 +101,8 @@ if __name__ == '__main__':
+                         prereq_strings.append(str(prereq))
+                 print(f'{filename} : {" ".join(prereq_strings)}',
+                       file=listFile)
++            print('%.mod:\n\t@:',
++                  file=listFile)
+     except:
+         os.unlink(args.output)
+         traceback.print_exc(file=sys.stdout)
+diff --git a/lfric/infrastructure/build/tools/configurator/templates/loader.f90.jinja b/lfric/infrastructure/build/tools/configurator/templates/loader.f90.jinja
+index fbd03f17..ba9f035f 100644
+--- a/lfric/infrastructure/build/tools/configurator/templates/loader.f90.jinja
++++ b/lfric/infrastructure/build/tools/configurator/templates/loader.f90.jinja
+@@ -27,7 +27,8 @@ module {{moduleName}}
+ {{' '*indent}}{{listname}}_is_loaded, &
+ {{' '*indent}}{{listname}}_reset_load_status, &
+ {{' '*indent}}{{listname}}_final, &
+-{{' '*indent}}get_{{listname}}_nml
++{{' '*indent}}get_{{listname}}_nml, &
++{{' '*indent}}get_{{listname}}_nml2
+ {%-   endfor %}
+ {%- endif %}
+ 
+@@ -192,7 +193,10 @@ contains
+ 
+     type(namelist_collection_type), intent(inout) :: nml_bank
+ 
+-    type(namelist_type) :: nml_obj
++    ! FS#35515
++{%- for listname in namelists %}
++    type(namelist_type), save :: nml_obj_{{listname}}
++{%- endfor %}
+ 
+     integer(i_def) :: i, j
+ 
+@@ -225,8 +229,9 @@ contains
+             call read_{{listname}}_namelist( unit, local_rank, scan )
+             if (.not. scan) then
+               call postprocess_{{listname}}_namelist()
+-              nml_obj = get_{{listname}}_nml()
+-              call nml_bank%add_namelist(nml_obj)
++              !nml_obj = get_{{listname}}_nml()
++              !call nml_bank%add_namelist(nml_obj)
++              call get_{{listname}}_nml2(nml_obj_{{listname}},nml_bank)
+             end if
+           else
+             write( log_scratch_space, '(A)' )      &
+diff --git a/lfric/infrastructure/build/tools/configurator/templates/namelist.f90.jinja b/lfric/infrastructure/build/tools/configurator/templates/namelist.f90.jinja
+index d95e764d..a9d2b9ec 100644
+--- a/lfric/infrastructure/build/tools/configurator/templates/namelist.f90.jinja
++++ b/lfric/infrastructure/build/tools/configurator/templates/namelist.f90.jinja
+@@ -17,6 +17,7 @@ module {{listname}}_config_mod
+ 
+   use namelist_mod,      only: namelist_type
+   use namelist_item_mod, only: namelist_item_type
++  use namelist_collection_mod, only: namelist_collection_type
+ 
+ {%- for module, symbols in use_from | dictsort %}
+ {%-   if loop.first %}{{'\n'}}{% endif %}
+@@ -34,7 +35,7 @@ module {{listname}}_config_mod
+ {{' '*12}}{{listname}}_is_loadable, {{listname}}_is_loaded, &
+ {{' '*12}}{{listname}}_reset_load_status, &
+ {{' '*12}}{{listname}}_multiples_allowed, {{listname}}_final, &
+-{{' '*12}}get_{{listname}}_nml
++{{' '*12}}get_{{listname}}_nml, get_{{listname}}_nml2
+ 
+ {%- for name in enumerations | sort %}
+ {%-   if loop.first %}{{'\n'}}{%- endif %}
+@@ -401,12 +402,21 @@ contains
+     type(namelist_type)      :: namelist_obj
+     type(namelist_item_type) :: members({{parameters|length}})
+ 
++    ! FS#35515
++    allocate(namelist_obj%members({{parameters|length}}))
++{%- for name, parameter in parameters | dictsort %}
++{%-   if loop.first %}{{'\n'}}{% endif %}
++      call namelist_obj%members({{loop.index}})%initialise( &
++                  '{{parameter.name}}', {{parameter.name}} )
++{{- '\n'}}
++{%- endfor %}
+ {%- for name, parameter in parameters | dictsort %}
+ {%-   if loop.first %}{{'\n'}}{% endif %}
+       call members({{loop.index}})%initialise( &
+                   '{{parameter.name}}', {{parameter.name}} )
+ {{- '\n'}}
+ {%- endfor %}
++
+     if (trim(profile_name) /= trim(cmdi) ) then
+       call namelist_obj%initialise( trim(listname), &
+                                     members, &
+@@ -418,6 +428,44 @@ contains
+ 
+   end function get_{{listname}}_nml
+ 
++  !> @brief Returns a <<namelist_type>> object populated with the
++  !>        current contents of this configuration module.
++  !> @return namelist_obj <<namelist_type>> with current namelist contents.
++  subroutine get_{{listname}}_nml2(namelist_obj,nml_bank)
++
++    implicit none
++
++    type(namelist_type)      :: namelist_obj
++    type(namelist_collection_type) :: nml_bank
++    type(namelist_item_type) :: members({{parameters|length}})
++
++    ! FS#35515
++    allocate(namelist_obj%members({{parameters|length}}))
++{%- for name, parameter in parameters | dictsort %}
++{%-   if loop.first %}{{'\n'}}{% endif %}
++      call namelist_obj%members({{loop.index}})%initialise( &
++                  '{{parameter.name}}', {{parameter.name}} )
++{{- '\n'}}
++{%- endfor %}
++{%- for name, parameter in parameters | dictsort %}
++{%-   if loop.first %}{{'\n'}}{% endif %}
++      call members({{loop.index}})%initialise( &
++                  '{{parameter.name}}', {{parameter.name}} )
++{{- '\n'}}
++{%- endfor %}
++
++    if (trim(profile_name) /= trim(cmdi) ) then
++      call namelist_obj%initialise( trim(listname), &
++                                    members, &
++                                    profile_name = profile_name )
++    else
++      call namelist_obj%initialise( trim(listname), &
++                                    members )
++    end if
++    call nml_bank%add_namelist(namelist_obj)
++
++  end subroutine get_{{listname}}_nml2
++
+ 
+   !> Performs any processing to be done once all namelists are loaded
+   !>
+diff --git a/lfric/infrastructure/build/update_mod.sh b/lfric/infrastructure/build/update_mod.sh
+new file mode 100644
+index 00000000..4ed8f129
+--- /dev/null
++++ b/lfric/infrastructure/build/update_mod.sh
+@@ -0,0 +1,9 @@
++#!/bin/bash
++
++for f in $(find $1 -type f); do
++    bname=`basename $f`
++    if [ ! -f $2/$bname ] || [[ `diff <(sed '3d' $1/$bname) <(sed '3d' $2/$bname)` ]]; then
++        echo "Updating $bname"
++        cp $1/$bname $2/$bname
++    fi
++done
+diff --git a/lfric/infrastructure/source/configuration/namelist_collection_mod.f90 b/lfric/infrastructure/source/configuration/namelist_collection_mod.f90
+index c52f67fc..885922fc 100644
+--- a/lfric/infrastructure/source/configuration/namelist_collection_mod.f90
++++ b/lfric/infrastructure/source/configuration/namelist_collection_mod.f90
+@@ -60,6 +60,7 @@ module namelist_collection_mod
+     procedure, public :: get_name
+     procedure, public :: namelist_exists
+     procedure, public :: get_namelist_names
++    procedure, public :: print_namelists
+     procedure, public :: get_namelist_profiles
+     procedure, public :: get_n_namelists
+     procedure, public :: get_table_len
+@@ -69,7 +70,8 @@ module namelist_collection_mod
+ 
+     procedure, public :: clear
+ 
+-    final :: namelist_collection_destructor
++    ! FS#35515
++    ! final :: namelist_collection_destructor
+ 
+   end type namelist_collection_type
+ 
+@@ -402,6 +404,62 @@ function get_name( self ) result( name )
+   name = self%name
+ 
+ end function get_name
++subroutine print_namelists( self)
++
++  implicit none
++
++  class(namelist_collection_type), intent(in) :: self
++
++  character(str_def) :: name_to_add
++
++  type(namelist_type), pointer :: namelist_obj => null()
++
++  ! Pointer to linked list - used for looping through the list
++  type(linked_list_item_type), pointer :: loop => null()
++
++  integer(i_def) :: i
++  integer(i_def) :: n_lists
++
++  n_lists = self%get_n_namelists()
++
++  if (n_lists > 1) then
++
++    do i=0, self%table_len-1
++
++      loop => self%namelist_list(i)%get_head()
++
++      do while ( associated(loop) )
++
++        ! Loop over all items in this list
++        if ( associated(loop) ) then
++
++          select type( payload => loop%payload )
++          type is ( namelist_type )
++            namelist_obj => payload
++
++            name_to_add = trim(namelist_obj%get_full_name())
++            print *, name_to_add
++            call namelist_obj%print()
++          end select
++
++          loop => loop%next
++        else
++          loop => loop%next
++        end if
++      end do
++    end do
++
++  else
++    write( log_scratch_space, '(A)')              &
++        'Namelist collection '//trim(self%name)// &
++        ' is empty.'
++    call log_event( log_scratch_space, LOG_LEVEL_ERROR )
++  end if
++
++  nullify(namelist_obj)
++
++  return
++end subroutine print_namelists
+ 
+ 
+ !> @brief Queries the unique names of the namelists in the collection.
+diff --git a/lfric/infrastructure/source/configuration/namelist_item_mod.f90 b/lfric/infrastructure/source/configuration/namelist_item_mod.f90
+index f1c607e9..03b76ff1 100644
+--- a/lfric/infrastructure/source/configuration/namelist_item_mod.f90
++++ b/lfric/infrastructure/source/configuration/namelist_item_mod.f90
+@@ -62,8 +62,6 @@ module namelist_item_mod
+   !=========================================
+   type :: namelist_item_type
+ 
+-    private
+-
+     class(key_value_type), allocatable :: key_value_pair
+ 
+   contains
+diff --git a/lfric/infrastructure/source/configuration/namelist_mod.f90 b/lfric/infrastructure/source/configuration/namelist_mod.f90
+index 5ddacf53..bad1d81a 100644
+--- a/lfric/infrastructure/source/configuration/namelist_mod.f90
++++ b/lfric/infrastructure/source/configuration/namelist_mod.f90
+@@ -23,6 +23,13 @@ module namelist_mod
+                                   LOG_LEVEL_INFO
+   use constants_mod,        only: imdi, rmdi, cmdi, str_def, i_def
+   use namelist_item_mod,    only: namelist_item_type
++  use key_value_mod,   only: key_value_type,                                   &
++                            int32_key_value_type,     int64_key_value_type,        &
++                            int32_arr_key_value_type, int64_arr_key_value_type,    &
++                            real32_key_value_type,    real64_key_value_type,        &
++                            real32_arr_key_value_type,real64_arr_key_value_type,    &
++                            logical_key_value_type, logical_arr_key_value_type,&
++                            str_key_value_type,     str_arr_key_value_type
+ 
+   implicit none
+ 
+@@ -35,7 +42,8 @@ module namelist_mod
+   !=========================================
+   type, extends(linked_list_data_type) :: namelist_type
+ 
+-    private
++    ! FS#35515
++    ! private
+ 
+     character(:), allocatable :: listname
+     character(str_def) :: profile_name = trim(cmdi)
+@@ -65,6 +73,7 @@ module namelist_mod
+     procedure :: get_listname
+     procedure :: get_profile_name
+     procedure :: get_full_name
++    procedure :: print => namelist_print
+ 
+     generic   :: get_value => get_int32_value,   get_int32_arr_value,   &
+                               get_int64_value,   get_int64_arr_value,   &
+@@ -90,6 +99,52 @@ contains
+   ! 1.0 Constructors
+   !=========================================
+ 
++  subroutine namelist_print(self)
++
++    implicit none
++
++    class(namelist_type),     intent(inout) :: self
++
++    integer(i_def) :: i, j
++    character(:), allocatable :: key
++
++    if ( allocated(self%members) ) then
++      print *, "  ", "number of members", size(self%members)
++      do j=1, size(self%members)
++        key = self%members(j)%get_key()
++        select type( clay => self%members(j)%key_value_pair )
++        class is (int32_key_value_type)
++          print *, trim(key), clay%value
++        class is (int64_key_value_type)
++          print *, trim(key), clay%value
++        class is (real32_key_value_type)
++          print *, trim(key), clay%value
++        class is (real64_key_value_type)
++          print *, trim(key), clay%value
++        class is (logical_key_value_type)
++          print *, trim(key), clay%value
++        class is (str_key_value_type)
++          print *, trim(key), trim(clay%value)
++        class is (int32_arr_key_value_type)
++          print *, trim(key), clay%value
++        class is (int64_arr_key_value_type)
++          print *, trim(key), clay%value
++        class is (real32_arr_key_value_type)
++          print *, trim(key), clay%value
++        class is (real64_arr_key_value_type)
++          print *, trim(key), clay%value
++        class is (logical_arr_key_value_type)
++          print *, trim(key), clay%value
++        class is (str_arr_key_value_type)
++          print *, trim(key), clay%value
++        end select
++
++      end do
++
++    else
++      print *, "  ", "<empty>"
++    end if
++  end subroutine
+   !------------------------------------------
+   ! 1.4 GENERIC namelist constructor
+   !============================================================================
+@@ -118,7 +173,7 @@ contains
+     end if
+ 
+     allocate( self%listname, source=listname )
+-    allocate( self%members,  source=members )
++    !allocate( self%members,  source=members )
+ 
+   end subroutine namelist_initialise
+ 
+diff --git a/lfric/infrastructure/source/field/field_mod.t90 b/lfric/infrastructure/source/field/field_mod.t90
+index 777ec479..0001fc8e 100644
+--- a/lfric/infrastructure/source/field/field_mod.t90
++++ b/lfric/infrastructure/source/field/field_mod.t90
+@@ -86,6 +86,7 @@ module field_{{kind}}_mod
+     !> Function to get a proxy with public pointers to the data in a
+     !! field_type.
+     procedure, public :: get_proxy
++    procedure, private :: get_proxy2
+ 
+     !> Setter for the field write method
+     procedure, public :: set_write_behaviour
+@@ -226,18 +227,39 @@ contains
+   !>
+   !> @return The proxy type with public pointers to the elements of
+   !> field_type
++  subroutine get_proxy2(self, proxy)
++    implicit none
++    class(field_{{kind}}_type), target, intent(in)  :: self
++    type(field_{{kind}}_proxy_type), intent(inout) :: proxy
++    type(field_{{kind}}_type), pointer    :: sself
++
++    ! Call the routine that initialises the proxy for data held in the parent
++    sself => self
++    call sself%field_parent_proxy_initialiser(proxy)
++
++    if (allocated(sself%data))then
++      proxy%data => sself%data
++    else
++      ! Fields can alternatively point to bespoke data
++      proxy%data => sself%override_data
++    end if
++
++  end subroutine get_proxy2
++
+   type(field_{{kind}}_proxy_type) function get_proxy(self)
+     implicit none
+     class(field_{{kind}}_type), target, intent(in)  :: self
++    type(field_{{kind}}_type), pointer    :: sself
+ 
+     ! Call the routine that initialises the proxy for data held in the parent
+-    call self%field_parent_proxy_initialiser(get_proxy)
++    sself => self
++    call sself%field_parent_proxy_initialiser(get_proxy)
+ 
+-    if (allocated(self%data))then
+-      get_proxy%data => self%data
++    if (allocated(sself%data))then
++      get_proxy%data => sself%data
+     else
+       ! Fields can alternatively point to bespoke data
+-      get_proxy%data => self%override_data
++      get_proxy%data => sself%override_data
+     end if
+ 
+   end function get_proxy
+@@ -260,7 +282,7 @@ contains
+ 
+     implicit none
+ 
+-    class(field_{{kind}}_type), intent(inout)        :: self
++    class(field_{{kind}}_type), intent(inout), target        :: self
+     type(function_space_type), pointer, intent(in) :: vector_space
+     character(*), optional, intent(in)             :: name
+     {{type}}({{kind}}), target, optional, intent(in)     :: override_data( : )
+@@ -275,6 +297,8 @@ contains
+     ! Depth of halo to allocate data array
+     integer(i_def) :: field_halo_depth
+ 
++    type(field_{{kind}}_type), pointer    :: sself
++
+     if ( present(name) ) then
+       local_name = name
+     else
+@@ -295,7 +319,8 @@ contains
+     ! re-initialisation
+     call field_{{kind}}_destructor_scalar(self)
+ 
+-    call self%field_parent_initialiser(vector_space, &
++    sself => self
++    call sself%field_parent_initialiser(vector_space, &
+                                        name=local_name, &
+                                        fortran_type={{type}}_type, &
+                                        fortran_kind={{kind}}, &
+@@ -622,18 +647,21 @@ contains
+ 
+     implicit none
+ 
+-    class(field_{{kind}}_type),   intent(in) :: this
++    class(field_{{kind}}_type),   intent(in), target :: this
+     character(len=*), optional, intent(in) :: field_name
++    type(field_{{kind}}_type), pointer :: tthis
+ 
+     character(str_def) :: name_used
++    type(field_{{kind}}_proxy_type) :: proxy
+ 
++    tthis => this
+     if (present(field_name)) then
+ 
+       name_used = field_name
+ 
+     else
+ 
+-      name_used = this%get_name()
++      name_used = tthis%get_name()
+       if (name_used == name_none) &
+         call log_event( 'Attempt to write field with undefined name', LOG_LEVEL_ERROR )
+ 
+@@ -641,7 +669,8 @@ contains
+ 
+     if (associated(this%write_method)) then
+ 
+-      call this%write_method(trim(name_used), this%get_proxy())
++      call tthis%get_proxy2(proxy)
++      call tthis%write_method(name_used, proxy)
+ 
+     else
+ 
+diff --git a/lfric/infrastructure/source/key_value/key_value_collection_mod.f90 b/lfric/infrastructure/source/key_value/key_value_collection_mod.f90
+index 324c466c..c8eec92b 100644
+--- a/lfric/infrastructure/source/key_value/key_value_collection_mod.f90
++++ b/lfric/infrastructure/source/key_value/key_value_collection_mod.f90
+@@ -178,7 +178,7 @@ subroutine create_key_value_object( self, key, value )
+ 
+   instance => create_key_value( key, value )
+   call self%add_key_value_object( instance )
+-  deallocate( instance )
++  !deallocate( instance )
+ 
+ end subroutine create_key_value_object
+ 
+@@ -800,7 +800,7 @@ subroutine get_abstract_value(self, key, value)
+     ! 'cast' to the data type
+     select type(listitem => loop%payload)
+       class is (abstract_key_value_type)
+-      if ( trim(key) == trim(listitem%get_key()) ) then
++      if ( key == listitem%get_key() ) then
+           value => listitem%value
+           exit
+       end if
+@@ -867,7 +867,9 @@ function get_hash(self, key) result(hash)
+   class(key_value_collection_type), intent(in) :: self
+   character(*), intent(in) :: key
+   integer(i_def) :: hash
+-  hash = mod(sum_string(trim(key)),self%get_table_len())
++  character(len=100) :: key2
++  key2 = key
++  hash = mod(sum_string(trim(key2)),self%get_table_len())
+ end function get_hash
+ 
+ !> Clears all items from the collection
+diff --git a/lfric/infrastructure/source/key_value/key_value_mod.f90 b/lfric/infrastructure/source/key_value/key_value_mod.f90
+index 4abfbb8f..0a693548 100644
+--- a/lfric/infrastructure/source/key_value/key_value_mod.f90
++++ b/lfric/infrastructure/source/key_value/key_value_mod.f90
+@@ -48,7 +48,6 @@ module key_value_mod
+   !=========================================
+   type, abstract, extends(linked_list_data_type) :: key_value_type
+ 
+-    private
+     character(:), allocatable :: key
+ 
+   contains
+@@ -271,7 +270,8 @@ subroutine key_value_initialise( self, key )
+         'Type already initialised as ' // trim(self%key)
+     call log_event( log_scratch_space, LOG_LEVEL_ERROR )
+   else
+-    allocate( self%key, source=trim(key) )
++    !allocate( self%key, source=trim(key) )
++    self%key = key
+   end if
+ 
+   return
+@@ -283,11 +283,12 @@ function get_key_value_key( self ) result( key )
+ 
+   implicit none
+ 
+-  class(key_value_type), intent(in) :: self
++  class(key_value_type), target, intent(in) :: self
+ 
+-  character(:), allocatable :: key
++  character(:), pointer :: key
+ 
+-  allocate( key, source=trim(self%key) )
++  !allocate( key, source=trim(self%key) )
++  key => self%key
+ 
+   return
+ end function get_key_value_key
+diff --git a/lfric_apps/applications/gravity_wave/source/driver/gravity_wave_infrastructure_mod.f90 b/lfric_apps/applications/gravity_wave/source/driver/gravity_wave_infrastructure_mod.f90
+index 672a462f..10ccfa13 100644
+--- a/lfric_apps/applications/gravity_wave/source/driver/gravity_wave_infrastructure_mod.f90
++++ b/lfric_apps/applications/gravity_wave/source/driver/gravity_wave_infrastructure_mod.f90
+@@ -204,7 +204,8 @@ contains
+                     extrusion, stencil_depth,     &
+                     apply_partition_check )
+ 
+-    allocate( twod_names, source=base_mesh_names )
++    !allocate( twod_names, source=base_mesh_names )
++    twod_names = base_mesh_names
+     do i=1, size(twod_names)
+       twod_names(i) = trim(twod_names(i))//'_2d'
+     end do
+diff --git a/lfric_apps/applications/lfric_atm/build/fortran/nvfortran.mk b/lfric_apps/applications/lfric_atm/build/fortran/nvfortran.mk
+new file mode 100644
+index 00000000..d4a476c7
+--- /dev/null
++++ b/lfric_apps/applications/lfric_atm/build/fortran/nvfortran.mk
+@@ -0,0 +1,18 @@
++##############################################################################
++# (c) Crown copyright 2017 Met Office. All rights reserved.
++# The file LICENCE, distributed with this code, contains details of the terms
++# under which the code may be used.
++##############################################################################
++# Various things specific to the Portland Fortran compiler.
++##############################################################################
++
++$(info Project specials for Portland compiler)
++
++export FFLAGS_UM_PHYSICS = -r8
++
++# The lfric_atm app defines an extra set of debug flags for
++# fast-debug. For this compiler use the same as the full-debug
++# settings
++FFLAGS_FASTD_INIT         = $(FFLAGS_INIT) 
++FFLAGS_FASTD_RUNTIME      = $(FFLAGS_RUNTIME)
++
+diff --git a/lfric_apps/applications/ngarch/build/fortran/nvfortran.mk b/lfric_apps/applications/ngarch/build/fortran/nvfortran.mk
+new file mode 100644
+index 00000000..e95afff3
+--- /dev/null
++++ b/lfric_apps/applications/ngarch/build/fortran/nvfortran.mk
+@@ -0,0 +1,17 @@
++##############################################################################
++# (c) Crown copyright 2017 Met Office. All rights reserved.
++# The file LICENCE, distributed with this code, contains details of the terms
++# under which the code may be used.
++##############################################################################
++# Various things specific to the Portland Fortran compiler.
++##############################################################################
++
++$(info Project specials for Portland compiler)
++
++export FFLAGS_UM_PHYSICS = -r8
++
++# The lfric_atm app defines an extra set of debug flags for
++# fast-debug. For this compiler use the same as the full-debug
++# settings
++FFLAGS_FASTD_INIT         = $(FFLAGS_INIT) 
++FFLAGS_FASTD_RUNTIME      = $(FFLAGS_RUNTIME)
+diff --git a/lfric_apps/applications/shallow_water/source/driver/shallow_water_model_mod.F90 b/lfric_apps/applications/shallow_water/source/driver/shallow_water_model_mod.F90
+index 670e2225..5c5aaaeb 100644
+--- a/lfric_apps/applications/shallow_water/source/driver/shallow_water_model_mod.F90
++++ b/lfric_apps/applications/shallow_water/source/driver/shallow_water_model_mod.F90
+@@ -189,7 +189,7 @@ module shallow_water_model_mod
+                     stencil_depth, check_partitions )
+ 
+ 
+-    allocate( twod_names, source=base_mesh_names )
++    twod_names = base_mesh_names
+     do i=1, size(twod_names)
+       twod_names(i) = trim(twod_names(i))//'_2d'
+     end do
+diff --git a/lfric_apps/applications/transport/source/driver/transport_driver_mod.f90 b/lfric_apps/applications/transport/source/driver/transport_driver_mod.f90
+index 54507eec..4b3859c6 100644
+--- a/lfric_apps/applications/transport/source/driver/transport_driver_mod.f90
++++ b/lfric_apps/applications/transport/source/driver/transport_driver_mod.f90
+@@ -228,11 +228,12 @@ contains
+ 
+     ! 1.1b Meshes the require a shifted extrusion
+     ! ---------------------------------------------------------
+-    allocate(meshes_to_shift,  source=base_mesh_names)
++    !allocate(meshes_to_shift,  source=base_mesh_names)
++    meshes_to_shift = base_mesh_names
+ 
+     ! 1.1c Meshes that require a double-level extrusion
+     ! ---------------------------------------------------------
+-    allocate(meshes_to_double, source=base_mesh_names)
++    meshes_to_double = base_mesh_names
+ 
+ 
+     !=======================================================================
+@@ -255,7 +256,7 @@ contains
+                                                   number_of_layers, &
+                                                   PRIME_EXTRUSION ) )
+ 
+-    allocate( twod_names, source=base_mesh_names )
++    twod_names = base_mesh_names
+     do i=1, size(twod_names)
+       twod_names(i) = trim(twod_names(i))//'_2d'
+     end do
+@@ -309,7 +310,7 @@ contains
+     if (allocated(meshes_to_shift)) then
+       if (size(meshes_to_shift) > 0) then
+ 
+-        allocate( shifted_names, source=meshes_to_shift )
++        shifted_names = meshes_to_shift
+         do i=1, size(shifted_names)
+           shifted_names(i) = trim(shifted_names(i))//'_shifted'
+         end do
+@@ -326,7 +327,7 @@ contains
+     if (allocated(meshes_to_double)) then
+       if (size(meshes_to_double) > 0) then
+ 
+-        allocate( double_names, source=meshes_to_double )
++        double_names = meshes_to_double
+         do i=1, size(double_names)
+           double_names(i) = trim(double_names(i))//'_double'
+         end do
+diff --git a/lfric_apps/build/extract/target-map.txt b/lfric_apps/build/extract/target-map.txt
+index 1da724e6..036b4cf5 100644
+--- a/lfric_apps/build/extract/target-map.txt
++++ b/lfric_apps/build/extract/target-map.txt
+@@ -1,5 +1,7 @@
+ generic-x86:  gfortran: meto-x86-gcc-ifort
+ generic-x86:  ifort:    meto-x86-gcc-ifort
++generic-x86:  nvfortran:meto-x86-gcc-ifort
++generic-x86:  flang:    meto-x86-gcc-ifort
+ meto-spice:   gfortran: meto-x86-gcc-ifort
+ meto-spice:   ifort:    meto-x86-gcc-ifort
+ meto-xc40:    crayftn:  meto-xc40-cce
+diff --git a/lfric_apps/science/gungho/source/driver/gungho_model_mod.F90 b/lfric_apps/science/gungho/source/driver/gungho_model_mod.F90
+index 0ffb6dde..f136b599 100644
+--- a/lfric_apps/science/gungho/source/driver/gungho_model_mod.F90
++++ b/lfric_apps/science/gungho/source/driver/gungho_model_mod.F90
+@@ -603,7 +603,8 @@ contains
+                     apply_partition_check )
+ 
+ 
+-    allocate( twod_names, source=base_mesh_names )
++    !allocate( twod_names, source=base_mesh_names )
++    twod_names = base_mesh_names
+     do i=1, size(twod_names)
+       twod_names(i) = trim(twod_names(i))//'_2d'
+     end do
+@@ -617,7 +618,8 @@ contains
+     if (allocated(meshes_to_shift)) then
+       if (size(meshes_to_shift) > 0) then
+ 
+-        allocate( shifted_names, source=meshes_to_shift )
++        !allocate( shifted_names, source=meshes_to_shift )
++        shifted_names = meshes_to_shift
+         do i=1, size(shifted_names)
+           shifted_names(i) = trim(shifted_names(i))//'_shifted'
+         end do
+@@ -634,7 +636,8 @@ contains
+     if (allocated(meshes_to_double)) then
+       if (size(meshes_to_double) > 0) then
+ 
+-        allocate( double_names, source=meshes_to_double )
++        !allocate( double_names, source=meshes_to_double )
++        double_names = meshes_to_double
+         do i=1, size(double_names)
+           double_names(i) = trim(double_names(i))//'_double'
+         end do
+diff --git a/lfric_apps/science/um_physics_interface/source/psy/psykal_lite_phys_mod.F90 b/lfric_apps/science/um_physics_interface/source/psy/psykal_lite_phys_mod.F90
+index ef6d8f4b..a8167ff2 100644
+--- a/lfric_apps/science/um_physics_interface/source/psy/psykal_lite_phys_mod.F90
++++ b/lfric_apps/science/um_physics_interface/source/psy/psykal_lite_phys_mod.F90
+@@ -216,7 +216,7 @@ contains
+   ! Add arrays my_coeff_rad & my_phi_stph by hand
+   real(kind=r_def), intent(in), dimension(:,:) :: coeffc_phase
+   real(kind=r_def), intent(in), dimension(:,:) :: coeffs_phase
+-  integer(kind=i_def), parameter :: nranks_array = rank(coeffc_phase)
++  integer(kind=i_def), parameter :: nranks_array = 2
+   integer(kind=i_def), dimension(nranks_array) :: dims_array
+ 
+   ! Get the upper bound for each rank of each scalar array
diff --git a/examples/lfric/scripts/KGOs/lfric_gunho_configuration_4its.nml b/examples/lfric/scripts/KGOs/lfric_gungho_configuration_4its.nml
similarity index 100%
rename from examples/lfric/scripts/KGOs/lfric_gunho_configuration_4its.nml
rename to examples/lfric/scripts/KGOs/lfric_gungho_configuration_4its.nml
diff --git a/examples/lfric/scripts/KGOs/lfric_gunho_configuration_4its_checksums.txt b/examples/lfric/scripts/KGOs/lfric_gungho_configuration_4its_checksums.txt
similarity index 100%
rename from examples/lfric/scripts/KGOs/lfric_gunho_configuration_4its_checksums.txt
rename to examples/lfric/scripts/KGOs/lfric_gungho_configuration_4its_checksums.txt
diff --git a/examples/lfric/scripts/KGOs/nvc++.mk b/examples/lfric/scripts/KGOs/nvc++.mk
new file mode 100644
index 000000000..13b17a108
--- /dev/null
+++ b/examples/lfric/scripts/KGOs/nvc++.mk
@@ -0,0 +1,9 @@
+##############################################################################
+# (c) Crown copyright 2017 Met Office. All rights reserved.
+# The file LICENCE, distributed with this code, contains details of the terms
+# under which the code may be used.
+##############################################################################
+
+$(info ** Chosen NVC++ compiler)
+
+CXX_RUNTIME_LIBRARY=stdc++
diff --git a/examples/lfric/scripts/KGOs/nvfortran_acc.mk b/examples/lfric/scripts/KGOs/nvfortran_acc.mk
new file mode 100644
index 000000000..79df074ed
--- /dev/null
+++ b/examples/lfric/scripts/KGOs/nvfortran_acc.mk
@@ -0,0 +1,39 @@
+##############################################################################
+# Copyright (c) 2017,  Met Office, on behalf of HMSO and Queen's Printer
+# For further details please refer to the file LICENCE.original which you
+# should have received as part of this distribution.
+##############################################################################
+# Various things specific to the NVIDIA Fortran compiler including flags to
+# build with OpenACC offload and OpenMP multicore enabled.
+##############################################################################
+#
+# This macro is evaluated now (:= syntax) so it may be used as many times as
+# desired without wasting time rerunning it.
+#
+F_MOD_DESTINATION_ARG = -module$(SPACE)
+
+FFLAGS_COMPILER           = 
+FFLAGS_NO_OPTIMISATION    = -O0
+FFLAGS_SAFE_OPTIMISATION  = -O2
+FFLAGS_RISKY_OPTIMISATION = -O4
+FFLAGS_DEBUG              = -g -traceback
+FFLAGS_RUNTIME            = -Mchkptr -Mchkstk
+# Option for checking code meets Fortran standard (not available for PGI)
+FFLAGS_FORTRAN_STANDARD   =
+OPENMP_ARG = -acc=gpu -gpu=managed -mp=multicore
+
+LDFLAGS_COMPILER = -g -acc=gpu -gpu=managed -mp=multicore -cuda
+
+FPP = cpp -traditional-cpp
+FPPFLAGS = -P
+FC = mpif90
+
+# nvbug 4648082
+science/src/um/src/atmosphere/large_scale_precipitation/ls_ppnc.o: private FFLAGS_RUNTIME = -Mchkstk
+
+# FS#35751
+mesh/create_mesh_mod.o: private FFLAGS_RUNTIME = -Mchkstk
+
+# 24.3
+science/src/socrates/src/cosp_github/subsample_and_optics_example/optics/quickbeam_optics/optics_lib.o: private FFLAGS_SAFE_OPTIMISATION = -O1
+science/src/socrates/src/cosp_github/subsample_and_optics_example/optics/quickbeam_optics/optics_lib.o: private FFLAGS_RISKY_OPTIMISATION = -O1
diff --git a/examples/lfric/scripts/KGOs/psyclone.mk b/examples/lfric/scripts/KGOs/psyclone.mk
new file mode 100644
index 000000000..7e52e1dac
--- /dev/null
+++ b/examples/lfric/scripts/KGOs/psyclone.mk
@@ -0,0 +1,95 @@
+##############################################################################
+# Copyright (c) 2017,  Met Office, on behalf of HMSO and Queen's Printer
+# For further details please refer to the file LICENCE.original which you
+# should have received as part of this distribution.
+##############################################################################
+#
+# Run this make file to generate PSyKAl source in WORKING_DIR from algorithms
+# and kernels in SOURCE_DIR. Transformation scripts are sought in
+# OPTIMISATION_PATH.
+#
+ALGORITHM_F_FILES := $(patsubst $(SOURCE_DIR)/%.X90, \
+                                $(WORKING_DIR)/%.f90, \
+                                $(shell find $(SOURCE_DIR) -name '*.X90' -print))
+
+ALGORITHM_f_FILES := $(patsubst $(SOURCE_DIR)/%.x90, \
+                                $(WORKING_DIR)/%.f90, \
+                                $(shell find $(SOURCE_DIR) -name '*.x90' -print))
+
+DIRECTORIES := $(patsubst $(SOURCE_DIR)%,$(WORKING_DIR)%, \
+                          $(shell find $(SOURCE_DIR) -type d -printf '%p/\n'))
+PSYCLONE_CONFIG_FILE ?= $(CORE_ROOT_DIR)/etc/psyclone.cfg
+
+.PHONY: psyclone
+psyclone: $(ALGORITHM_F_FILES) $(ALGORITHM_f_FILES)
+
+include $(LFRIC_BUILD)/lfric.mk
+include $(LFRIC_BUILD)/fortran.mk
+
+MACRO_ARGS := $(addprefix -D,$(PRE_PROCESS_MACROS))
+
+# Where an override file exists in the "psy" directory we invoke PSyclone, then
+# delete the resulting PSy source. The override has been copied as part of the
+# rest of the source.
+#
+$(WORKING_DIR)/%.f90: \
+$$(SOURCE_DIR)/psy/$$(notdir $$*)_psy.f90 $(WORKING_DIR)/%_psy.f90
+	$(call MESSAGE,Removing,$*_psy.f90)
+	$Qrm $(WORKING_DIR)/$*_psy.f90
+
+# Where an optimisation script exists for a specific file, use it.
+#
+$(WORKING_DIR)/%.f90 $(WORKING_DIR)/%_psy.f90: \
+$(WORKING_DIR)/%.x90 $$(OPTIMISATION_PATH)/$$*.py | $$(dir $$@)
+	$(call MESSAGE,PSyclone - local optimisation,$(subst $(SOURCE_DIR)/,,$<))
+	$QPYTHONPATH=$(LFRIC_BUILD)/psyclone:$$PYTHONPATH psyclone -api dynamo0.3 \
+	           -l all -d $(WORKING_DIR) \
+	           --config $(PSYCLONE_CONFIG_FILE) \
+	           -s $(OPTIMISATION_PATH)/$*.py \
+                   -okern $(WORKING_DIR) \
+	           -oalg $(WORKING_DIR)/$*.f90 \
+	           -opsy $(WORKING_DIR)/$*_psy.f90 $<
+
+# Where a global optimisation script exists, use it.
+#
+$(WORKING_DIR)/%.f90 $(WORKING_DIR)/%_psy.f90: \
+$(WORKING_DIR)/%.x90 $(OPTIMISATION_PATH)/global.py | $$(dir $$@)
+	$(call MESSAGE,PSyclone - global optimisation,$(subst $(SOURCE_DIR)/,,$<))
+	$QPYTHONPATH=$(LFRIC_BUILD)/psyclone:$$PYTHONPATH psyclone -api dynamo0.3 \
+	           -l all -d $(WORKING_DIR) \
+	           --config $(PSYCLONE_CONFIG_FILE) \
+	           -s $(OPTIMISATION_PATH)/global.py \
+                   -okern $(WORKING_DIR) \
+	           -oalg  $(WORKING_DIR)/$*.f90 \
+	           -opsy $(WORKING_DIR)/$*_psy.f90 $<
+
+# Where no optimisation script exists, don't use it.
+#
+$(WORKING_DIR)/%.f90 $(WORKING_DIR)/%_psy.f90: \
+$(WORKING_DIR)/%.x90 | $$(dir $$@)
+	$(call MESSAGE,PSyclone,$(subst $(SOURCE_DIR)/,,$<))
+	$QPYTHONPATH=$(LFRIC_BUILD)/psyclone:$$PYTHONPATH psyclone -api dynamo0.3 \
+	           -l all -d $(WORKING_DIR) \
+	           --config $(PSYCLONE_CONFIG_FILE) \
+                   -okern $(WORKING_DIR) \
+	           -oalg  $(WORKING_DIR)/$*.f90 \
+	           -opsy $(WORKING_DIR)/$*_psy.f90 $<
+
+.PRECIOUS: $(WORKING_DIR)/%.x90
+# Perform preprocessing for big X90 files.
+#
+$(WORKING_DIR)/%.x90: $(SOURCE_DIR)/%.X90 | $$(dir $$@)
+	$(call MESSAGE,Preprocessing, $(subst $(SOURCE_DIR)/,,$<))
+	$Q$(FPP) $(FPPFLAGS) $(MACRO_ARGS) $< $@
+
+# Little x90 files are just copied to the workspace.
+#
+$(WORKING_DIR)/%.x90: $(SOURCE_DIR)/%.x90 | $$(dir $$@)
+	$(call MESSAGE,Copying, $(subst $(SOURCE_DIR)/,,$<))
+	$Qcp $< $@
+
+# Create directories in the workspace as needed.
+#
+$(DIRECTORIES):
+	$(call MESSAGE,Creating,$@)
+	$Qmkdir -p $@
diff --git a/examples/lfric/scripts/acc_parallel.py b/examples/lfric/scripts/acc_parallel.py
new file mode 100644
index 000000000..198450e91
--- /dev/null
+++ b/examples/lfric/scripts/acc_parallel.py
@@ -0,0 +1,150 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2018-2024, 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: A. R. Porter, STFC Daresbury Lab
+#          R. W. Ford, STFC Daresbury Lab
+#          L. Mosimann, NVIDIA.
+
+'''PSyclone transformation script for the lfric API to apply
+colouring, OpenACC, OpenMP. Also adds redundant computation to the level-1
+halo for setval_* generically.
+
+'''
+from psyclone.domain.lfric import LFRicConstants
+from psyclone.psyir.nodes import ACCDirective, Loop
+from psyclone.psyir.transformations import (
+    ACCKernelsTrans, TransformationError)
+from psyclone.transformations import (
+    Dynamo0p3ColourTrans, Dynamo0p3OMPLoopTrans,
+    Dynamo0p3RedundantComputationTrans, OMPParallelTrans,
+    ACCParallelTrans, ACCLoopTrans, ACCRoutineTrans)
+
+
+# Names of any routines that we won't add any OpenACC to.
+ACC_EXCLUSIONS = [
+]
+
+
+def trans(psy):
+    '''Applies PSyclone colouring and OpenACC transformations. Any kernels that
+    cannot be offloaded to GPU are parallelised using OpenMP on the CPU. Any
+    setval_* kernels are transformed so as to compute into the L1 halos.
+
+    '''
+    rtrans = Dynamo0p3RedundantComputationTrans()
+    ctrans = Dynamo0p3ColourTrans()
+    otrans = Dynamo0p3OMPLoopTrans()
+    const = LFRicConstants()
+    loop_trans = ACCLoopTrans()
+    ktrans = ACCKernelsTrans()
+    parallel_trans = ACCParallelTrans(default_present=False)
+    artrans = ACCRoutineTrans()
+    oregtrans = OMPParallelTrans()
+
+    print(f"PSy name = '{psy.name}'")
+
+    # Loop over all of the Invokes in the PSy object
+    for invoke in psy.invokes.invoke_list:
+
+        print("Transforming invoke '{0}' ...".format(invoke.name))
+        schedule = invoke.schedule
+
+        # Make setval_* compute redundantly to the level 1 halo if it
+        # is in its own loop
+        for loop in schedule.loops():
+            if loop.iteration_space == "dof":
+                if len(loop.kernels()) == 1:
+                    if loop.kernels()[0].name in ["setval_c", "setval_x"]:
+                        rtrans.apply(loop, options={"depth": 1})
+
+        if psy.name.lower() in ACC_EXCLUSIONS:
+            print(f"Not adding ACC to invoke in '{psy.name}'")
+            apply_acc = False
+        else:
+            apply_acc = True
+
+        # Keep a record of any kernels we fail to module inline as we can't
+        # then add ACC ROUTINE to them.
+        failed_inline = set()
+
+        # Colour loops over cells unless they are on discontinuous
+        # spaces or over dofs
+        for loop in schedule.loops():
+            if loop.iteration_space == "cell_column":
+                if apply_acc:
+                    for kern in loop.kernels():
+                        try:
+                            artrans.apply(kern)
+                        except TransformationError as err:
+                            failed_inline.add(kern.name.lower())
+                            print(f"Adding ACC Routine to kernel '{kern.name}'"
+                                  f" failed:\n{err.value}")
+                if (loop.field_space.orig_name not in
+                        const.VALID_DISCONTINUOUS_NAMES):
+                    ctrans.apply(loop)
+
+        # Add OpenACC to loops unless they are over colours or are null.
+        schedule = invoke.schedule
+        for loop in schedule.walk(Loop):
+            if not apply_acc or any(kern.name.lower() in failed_inline for
+                                    kern in loop.kernels()):
+                print(f"Not adding OpenACC for kernels: "
+                      f"{[kern.name for kern in loop.kernels()]}")
+                continue
+            try:
+                if loop.loop_type == "colours":
+                    pass
+                if loop.loop_type == "colour":
+                    loop_trans.apply(loop, options={"independent": True})
+                    parallel_trans.apply(loop.ancestor(ACCDirective))
+                if loop.loop_type == "":
+                    loop_trans.apply(loop, options={"independent": True})
+                    parallel_trans.apply(loop.ancestor(ACCDirective))
+                if loop.loop_type == "dof":
+                    # We use ACC KERNELS for dof loops since they can contain
+                    # reductions.
+                    ktrans.apply(loop)
+            except TransformationError as err:
+                print(str(err))
+                pass
+
+        # Apply OpenMP thread parallelism for any kernels we've not been able
+        # to offload to GPU.
+        for loop in schedule.walk(Loop):
+            if not apply_acc or any(kern.name.lower() in failed_inline for
+                                    kern in loop.kernels()):
+                if loop.loop_type not in ["colours", "null"]:
+                    oregtrans.apply(loop)
+                    otrans.apply(loop, options={"reprod": True})
+
+    return psy
diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py
index 6d7d9d85b..7083af578 100644
--- a/src/psyclone/dynamo0p3.py
+++ b/src/psyclone/dynamo0p3.py
@@ -2109,7 +2109,7 @@ class DynMeshes():
         # required if distributed memory is enabled. We also require a
         # mesh object if any of the kernels require properties of
         # either the reference element or the mesh. (Colourmaps also
-        # require a mesh object but that is handled in _colourmap_init().)
+        # require a mesh object but that is handled in colourmap_init().)
         if not _name_set and Config.get().distributed_memory:
             # We didn't already have a requirement for a mesh so add one now.
             _name_set.add("mesh")
@@ -2161,12 +2161,11 @@ class DynMeshes():
                 self._symbol_table.find_or_create_integer_symbol(
                     var_name, tag=var_name)
 
-    def _colourmap_init(self):
+    def colourmap_init(self):
         '''
-        Sets-up information on any required colourmaps. This cannot be done
-        in the constructor since colouring is applied by Transformations
-        and happens after the Schedule has already been constructed. Therefore,
-        this method is called at code-generation time.
+        Sets-up information on any required colourmaps. Since colouring is
+        applied by Transformations, this method is called as the final step
+        of Dynamo0p3ColourTrans.apply().
 
         '''
         # pylint: disable=too-many-locals
@@ -2254,10 +2253,6 @@ class DynMeshes():
         api_config = Config.get().api_conf("lfric")
         const = LFRicConstants()
 
-        # Since we're now generating code, any transformations must
-        # have been applied so we can set-up colourmap information
-        self._colourmap_init()
-
         # We'll need various typedefs from the mesh module
         mtype = const.MESH_TYPE_MAP["mesh"]["type"]
         mmod = const.MESH_TYPE_MAP["mesh"]["module"]
diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py
index 7d3f9d96a..ca34b7660 100644
--- a/src/psyclone/transformations.py
+++ b/src/psyclone/transformations.py
@@ -378,6 +378,8 @@ class MarkRoutineForGPUMixin:
         :raises TransformationError: if the target is a built-in kernel.
         :raises TransformationError: if it is a kernel but without an
                                      associated PSyIR.
+        :raises TransformationError: if it is a Kernel that has multiple
+                                     implementations (mixed precision).
         :raises TransformationError: if any of the symbols in the kernel are
                                      accessed via a module use statement (and
                                      are not compile-time constants).
@@ -410,6 +412,17 @@ class MarkRoutineForGPUMixin:
                 raise TransformationError(
                     f"Failed to create PSyIR for kernel '{node.name}'. "
                     f"Cannot transform such a kernel.") from error
+
+            # Check that it's not a mixed-precision kernel (which will have
+            # more than one Routine implementing it). We can't transform
+            # these at the moment because we can't correctly manipulate their
+            # metadata - TODO #1946.
+            routines = kernel_schedule.root.walk(Routine)
+            if len(routines) > 1:
+                raise TransformationError(
+                    f"Cannot apply {self.name} to kernel '{node.name}' as "
+                    f"it has multiple implementations - TODO #1946")
+
             k_or_r = "Kernel"
         else:
             # Supplied node is a PSyIR Routine which *is* a Schedule.
@@ -1127,8 +1140,17 @@ class Dynamo0p3ColourTrans(ColourTrans):
             raise TransformationError("Cannot have a loop over colours "
                                       "within an OpenMP parallel region.")
 
+        # Get the ancestor InvokeSchedule as applying the transformation
+        # creates a new Loop node.
+        sched = node.ancestor(LFRicInvokeSchedule)
+
         super().apply(node, options=options)
 
+        # Finally, update the information on the colourmaps required for
+        # the mesh(es) in this invoke.
+        if sched and sched.invoke:
+            sched.invoke.meshes.colourmap_init()
+
     def _create_colours_loop(self, node):
         '''
         Creates a nested loop (colours, and cells of a given colour) which


Test patch:
diff --git a/.github/workflows/lfric_test.yml b/.github/workflows/lfric_test.yml
index 2065cf730..8f6c796f3 100644
--- a/.github/workflows/lfric_test.yml
+++ b/.github/workflows/lfric_test.yml
@@ -75,6 +75,58 @@ jobs:
         pip install .[test]
         pip install jinja2
 
+    # PSyclone, compile and run MetOffice gungho_model on GPU
+    - name: LFRic GungHo with OpenACC offload
+      run: |
+        # Set up environment
+        source /apps/spack/psyclone-spack/spack-repo/share/spack/setup-env.sh
+        spack load lfric-build-environment%nvhpc
+        source .runner_venv/bin/activate
+        export PSYCLONE_LFRIC_DIR=${GITHUB_WORKSPACE}/examples/lfric/scripts
+        export PSYCLONE_CONFIG_FILE=${PSYCLONE_LFRIC_DIR}/KGOs/lfric_psyclone.cfg
+        # The LFRic source must be patched to workaround bugs in the NVIDIA
+        # compiler's namelist handling.
+        rm -rf ${HOME}/LFRic/gpu_build
+        mkdir -p ${HOME}/LFRic/gpu_build
+        cp -r ${HOME}/LFRic/lfric_apps_${LFRIC_APPS_REV} ${HOME}/LFRic/gpu_build/lfric_apps
+        cp -r ${HOME}/LFRic/lfric_core_50869 ${HOME}/LFRic/gpu_build/lfric
+        cd ${HOME}/LFRic/gpu_build
+        patch -p1 < ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_${LFRIC_APPS_REV}_nvidia.patch
+        # Update the compiler definitions to build for GPU
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/nvfortran_acc.mk lfric/infrastructure/build/fortran/nvfortran.mk
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/nvc++.mk lfric/infrastructure/build/cxx/.
+        # Update the PSyclone commands to ensure transformed kernels are written
+        # to working directory.
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/psyclone.mk lfric/infrastructure/build/psyclone/.
+        # Update dependencies.sh to point to our patched lfric core.
+        sed -i -e 's/export lfric_core_sources=.*$/export lfric_core_sources\=\/home\/gh_runner\/LFRic\/gpu_build\/lfric/' lfric_apps/dependencies.sh
+        export LFRIC_DIR=${HOME}/LFRic/gpu_build/lfric_apps
+        export OPT_DIR=${LFRIC_DIR}/applications/gungho_model/optimisation/psyclone-test
+        cd ${LFRIC_DIR}
+        # PSyclone scripts must now be under 'optimisation' and be called 'global.py'
+        mkdir -p ${OPT_DIR}
+        cp ${PSYCLONE_LFRIC_DIR}/acc_parallel.py ${OPT_DIR}/global.py
+        # Clean previous version and compile again
+        rm -rf applications/gungho_model/working
+        ./build/local_build.py -a gungho_model -p psyclone-test
+        cd applications/gungho_model/example
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its.nml configuration.nml
+        mpirun -n 1 ../bin/gungho_model configuration.nml |& tee output.txt
+        python ${PSYCLONE_LFRIC_DIR}/compare_ouput.py ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its_checksums.txt gungho_model-checksums.txt
+        cat timer.txt
+        export VAR_TIME=$(grep "gungho_model" timer.txt | cut -d'|' -f5)
+        export VAR_HALOS=$(grep "gungho_model" halo_calls_counter.txt | cut -d'|' -f5)
+        echo $GITHUB_REF_NAME $GITHUB_SHA $VAR_TIME $VAR_HALOS >> ${HOME}/store_results/lfric_acc_performance_history
+        ${HOME}/mongosh-2.1.1-linux-x64/bin/mongosh \
+          "mongodb+srv://cluster0.x8ncpxi.mongodb.net/PerformanceMonitoring" \
+          --quiet --apiVersion 1 --username ${{ secrets.MONGODB_USERNAME }} \
+          --password ${{ secrets.MONGODB_PASSWORD }} \
+          --eval 'db.GitHub_CI.insertOne({branch_name: "'"$GITHUB_REF_NAME"'", commit: "'"$GITHUB_SHA"'",
+          github_job: "'"$GITHUB_RUN_ID"'"-"'"$GITHUB_RUN_ATTEMPT"'",
+          ci_test: "LFRic OpenACC", lfric_apps_version: '"$LFRIC_APPS_REV"', system: "GlaDos",
+          compiler:"spack-nvhpc-24.5", date: new Date(), elapsed_time: '"$VAR_TIME"',
+          num_of_halo_exchanges: '"$VAR_HALOS"'})'
+
     # PSyclone, compile and run MetOffice LFRic with 6 MPI ranks
     - name: LFRic passthrough (with DistributedMemory)
       run: |
@@ -91,9 +143,9 @@ jobs:
         ./build/local_build.py -a gungho_model -v
         # Run
         cd applications/gungho_model/example
-        cp ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gunho_configuration_4its.nml configuration.nml
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its.nml configuration.nml
         mpirun -n 6 ../bin/gungho_model configuration.nml |& tee output.txt
-        python ${PSYCLONE_LFRIC_DIR}/compare_ouput.py ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gunho_configuration_4its_checksums.txt gungho_model-checksums.txt
+        python ${PSYCLONE_LFRIC_DIR}/compare_ouput.py ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its_checksums.txt gungho_model-checksums.txt
         cat timer.txt
         export VAR_TIME=$(grep "gungho_model" timer.txt | cut -d'|' -f5)
         export VAR_HALOS=$(grep "gungho_model" halo_calls_counter.txt | cut -d'|' -f5)
@@ -108,13 +160,6 @@ jobs:
           compiler:"spack-gfortran-14", date: new Date(), elapsed_time: '"$VAR_TIME"',
           num_of_halo_exchanges: '"$VAR_HALOS"'})'
 
-    - name: Upload LFRic passthrough results
-      uses: exuanbo/actions-deploy-gist@v1
-      with:
-        token: ${{ secrets.GIST_TOKEN }}
-        gist_id: a4049a0fc0a0a11651a5ce6a04d76160
-        file_path: ../../../../store_results/lfric_passthrough_performance_history
-
     # PSyclone, compile and run MetOffice LFRic with all optimisations and 6 OpenMP threads
     - name: LFRic with all transformations
       run: |
@@ -127,7 +172,7 @@ jobs:
         export LFRIC_DIR=${HOME}/LFRic/lfric_apps_${LFRIC_APPS_REV}
         export OPT_DIR=${LFRIC_DIR}/applications/gungho_model/optimisation/psyclone-test
         cd ${LFRIC_DIR}
-        # Psyclone scripts must now be under 'optimisation' and be called 'global.py'
+        # PSyclone scripts must now be under 'optimisation' and be called 'global.py'
         mkdir -p applications/gungho_model/optimisation/psyclone-test
         cp ${PSYCLONE_LFRIC_DIR}/everything_everywhere_all_at_once.py ${OPT_DIR}/global.py
         # Clean previous version and compile again
@@ -135,10 +180,10 @@ jobs:
         ./build/local_build.py -a gungho_model -p psyclone-test -v
         # Run
         cd applications/gungho_model/example
-        cp ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gunho_configuration_4its.nml configuration.nml
+        cp ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its.nml configuration.nml
         export OMP_NUM_THREADS=6
         mpirun -n 1 ../bin/gungho_model configuration.nml |& tee output.txt
-        python ${PSYCLONE_LFRIC_DIR}/compare_ouput.py ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gunho_configuration_4its_checksums.txt gungho_model-checksums.txt
+        python ${PSYCLONE_LFRIC_DIR}/compare_ouput.py ${PSYCLONE_LFRIC_DIR}/KGOs/lfric_gungho_configuration_4its_checksums.txt gungho_model-checksums.txt
         cat timer.txt
         export VAR_TIME=$(grep "gungho_model" timer.txt | cut -d'|' -f5)
         export VAR_HALOS=$(grep "gungho_model" halo_calls_counter.txt | cut -d'|' -f5)
@@ -152,10 +197,3 @@ jobs:
           ci_test: "LFRic all transformations", lfric_version: '"$LFRIC_APPS_REV"', omp_threads: '"$OMP_NUM_THREADS"',
           system: "GlaDos", compiler:"spack-gfortran-14", date: new Date(), elapsed_time: '"$VAR_TIME"',
           num_of_halo_exchanges: '"$VAR_HALOS"'})'
-
-    - name: Upload LFRic optimised results
-      uses: exuanbo/actions-deploy-gist@v1
-      with:
-        token: ${{ secrets.GIST_TOKEN }}
-        gist_id: a4049a0fc0a0a11651a5ce6a04d76160
-        file_path: ../../../../store_results/lfric_optimised_performance_history
diff --git a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
index 563d016d8..344bb683c 100644
--- a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
+++ b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
@@ -57,18 +57,15 @@ from psyclone.psyir.nodes import (colored, Loop, Schedule, Literal, Directive,
 from psyclone.psyir.symbols import (AutomaticInterface, ScalarType, ArrayType,
                                     REAL_TYPE, INTEGER_TYPE)
 from psyclone.psyir.transformations import (
-    ACCKernelsTrans, LoopFuseTrans, LoopTrans, TransformationError)
+    ACCKernelsTrans, LoopFuseTrans, LoopTrans, OMPLoopTrans,
+    TransformationError)
 from psyclone.tests.lfric_build import LFRicBuild
 from psyclone.tests.utilities import get_invoke
-from psyclone.transformations import OMPParallelTrans, \
-    Dynamo0p3ColourTrans, \
-    Dynamo0p3OMPLoopTrans, \
-    DynamoOMPParallelLoopTrans, \
-    MoveTrans, \
-    Dynamo0p3RedundantComputationTrans, \
-    Dynamo0p3AsyncHaloExchangeTrans, \
-    Dynamo0p3KernelConstTrans, \
-    ACCLoopTrans, ACCParallelTrans, ACCEnterDataTrans
+from psyclone.transformations import (
+    OMPParallelTrans, Dynamo0p3ColourTrans, Dynamo0p3OMPLoopTrans,
+    DynamoOMPParallelLoopTrans, MoveTrans, Dynamo0p3RedundantComputationTrans,
+    Dynamo0p3AsyncHaloExchangeTrans, Dynamo0p3KernelConstTrans,
+    ACCLoopTrans, ACCParallelTrans, ACCEnterDataTrans)
 
 
 # The version of the API that the tests in this file
@@ -6306,10 +6303,12 @@ def test_haloex_rc4_colouring(tmpdir, monkeypatch, annexed):
 
         assert LFRicBuild(tmpdir).code_compiles(psy)
 
-        print("OK for iteration ", idx)
-
 
-def test_intergrid_colour(dist_mem):
+@pytest.mark.parametrize("trans_class",
+                         [(None, None, ""),
+                          (ACCParallelTrans, ACCLoopTrans, "acc"),
+                          (OMPParallelTrans, OMPLoopTrans, "omp")])
+def test_intergrid_colour(dist_mem, trans_class, tmpdir):
     ''' Check that we can apply colouring to a loop containing
     an inter-grid kernel. '''
     # Use an example that contains both prolongation and restriction
@@ -6320,10 +6319,17 @@ def test_intergrid_colour(dist_mem):
     # First two kernels are prolongation, last two are restriction
     loops = schedule.walk(Loop)
     ctrans = Dynamo0p3ColourTrans()
+    reg_trans = trans_class[0]() if trans_class[0] else None
+    loop_trans = trans_class[1]() if trans_class[1] else None
     # To a prolong kernel
     ctrans.apply(loops[1])
     # To a restrict kernel
     ctrans.apply(loops[3])
+    if reg_trans and loop_trans:
+        for loop in schedule.walk(Loop):
+            if loop.loop_type == "colour":
+                reg_trans.apply(loop)
+                loop_trans.apply(loop)
     gen = str(psy.gen).lower()
     expected = '''\
       ncolour_fld_m = mesh_fld_m%get_ncolours()
@@ -6335,18 +6341,19 @@ def test_intergrid_colour(dist_mem):
     assert expected in gen
     assert "loop1_stop = ncolour_fld_m" in gen
     assert "loop2_stop" not in gen
+    assert "      do colour = loop1_start, loop1_stop, 1\n" in gen
+    if trans_class[2]:
+        assert f"!${trans_class[2]} " in gen
     if dist_mem:
         assert ("last_halo_cell_all_colours_fld_m = "
                 "mesh_fld_m%get_last_halo_cell_all_colours()" in gen)
         expected = (
-            "      do colour = loop1_start, loop1_stop, 1\n"
             "        do cell = loop2_start, last_halo_cell_all_colours_fld_m"
             "(colour,1), 1\n")
     else:
         assert ("last_edge_cell_all_colours_fld_m = "
                 "mesh_fld_m%get_last_edge_cell_all_colours()" in gen)
         expected = (
-            "      do colour = loop1_start, loop1_stop, 1\n"
             "        do cell = loop2_start, last_edge_cell_all_colours_fld_m"
             "(colour), 1\n")
     assert expected in gen
@@ -6358,6 +6365,8 @@ def test_intergrid_colour(dist_mem):
         "map_w2(:,cmap_fld_m(colour,cell)))\n")
     assert expected in gen
 
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
 
 def test_intergrid_colour_errors(dist_mem, monkeypatch):
     ''' Check that we raise the expected error when colouring is not applied
@@ -6374,15 +6383,7 @@ def test_intergrid_colour_errors(dist_mem, monkeypatch):
     ctrans.apply(loop)
     # Update our list of loops
     loops = schedule.walk(Loop)
-    # Trigger the error by calling the internal method to get the upper
-    # bound before the colourmaps have been set-up
-    with pytest.raises(InternalError) as err:
-        _ = loops[1]._upper_bound_fortran()
-    assert ("All kernels within a loop over colours must have been coloured "
-            "but kernel 'prolong_test_kernel_code' has not" in str(err.value))
-    # Set-up the colourmaps
-    psy.invokes.invoke_list[0].meshes._colourmap_init()
-    # Check that the upper bound is now correct
+    # Check that the upper bound is correct
     upperbound = loops[1]._upper_bound_fortran()
     assert upperbound == "ncolour_fld_m"
     # Manually add an un-coloured kernel to the loop that we coloured
diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py
index c6887947e..cf7658d58 100644
--- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py
+++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py
@@ -282,6 +282,30 @@ def test_gpumixin_validate_wrong_node_type():
             "Routine but got 'FileContainer'" in str(err.value))
 
 
+def test_gpumixin_kernel_interface(kernel_outputdir, monkeypatch,
+                                   fortran_reader, fortran_writer):
+    '''
+    Test that the MarkRoutineForGPUMixin.validate() rejects a kernel that has
+    multiple implementations (i.e. for different precisions).
+
+    TODO this limitation is the subject of #1946.
+
+    '''
+    # Ensure kernel-output directory is uninitialised
+    config = Config.get()
+    monkeypatch.setattr(config, "_kernel_naming", "multiple")
+    psy, invoke = get_invoke("26.8_mixed_precision_args.f90",
+                             api="lfric", idx=0)
+    sched = invoke.schedule
+    kernels = sched.walk(Kern)
+    rtrans = ACCRoutineTrans()
+    # Use force because the kernel contains a WRITE statement.
+    with pytest.raises(TransformationError) as err:
+        rtrans.apply(kernels[0], options={"force": True})
+    assert ("Cannot apply ACCRoutineTrans to kernel 'mixed_code' as it has "
+            "multiple implementations - TODO #1946" in str(err.value))
+
+
 def test_gpumixin_validate_no_schedule(monkeypatch):
     '''
     Test that the MarkRoutineForGPUMixin.validate_it_can_run_on_gpu() method