ftorch_tensor.f90 Source File


This file depends on

sourcefile~~ftorch_tensor.f90~~EfferentGraph sourcefile~ftorch_tensor.f90 ftorch_tensor.f90 sourcefile~ftorch_devices.f90 ftorch_devices.F90 sourcefile~ftorch_tensor.f90->sourcefile~ftorch_devices.f90 sourcefile~ftorch_types.f90 ftorch_types.f90 sourcefile~ftorch_tensor.f90->sourcefile~ftorch_types.f90

Files dependent on this one

sourcefile~~ftorch_tensor.f90~~AfferentGraph sourcefile~ftorch_tensor.f90 ftorch_tensor.f90 sourcefile~ftorch.f90 ftorch.f90 sourcefile~ftorch.f90->sourcefile~ftorch_tensor.f90 sourcefile~ftorch_loss.f90 ftorch_loss.f90 sourcefile~ftorch.f90->sourcefile~ftorch_loss.f90 sourcefile~ftorch_model.f90 ftorch_model.f90 sourcefile~ftorch.f90->sourcefile~ftorch_model.f90 sourcefile~ftorch_optim.f90 ftorch_optim.f90 sourcefile~ftorch.f90->sourcefile~ftorch_optim.f90 sourcefile~ftorch_loss.f90->sourcefile~ftorch_tensor.f90 sourcefile~ftorch_model.f90->sourcefile~ftorch_tensor.f90 sourcefile~ftorch_optim.f90->sourcefile~ftorch_tensor.f90

Source Code

!| Module for the FTorch `torch_tensor` type and associated procedures.
!  Generated from `ftorch_tensor.fypp` using the
!  [fypp Fortran preprocessor](https://fypp.readthedocs.io/en/stable/index.html).
!
!  * License
!    FTorch is released under an MIT license.
!    See the [LICENSE](https://github.com/Cambridge-ICCS/FTorch/blob/main/LICENSE)
!    file for details.

module ftorch_tensor
  use, intrinsic :: iso_c_binding, only: c_associated, c_null_ptr, c_ptr
  use, intrinsic :: iso_fortran_env, only: int32, int64
  use ftorch_devices, only: torch_kCPU, torch_kCUDA, torch_kHIP, torch_kXPU, torch_kMPS
  use ftorch_types, only: torch_kInt8, torch_kInt16, torch_kInt32, torch_kInt64, &
                          torch_kFloat32, torch_kFloat64

  implicit none

  public

  !> Type for holding a Torch tensor.
  type torch_tensor
    type(c_ptr) :: p = c_null_ptr  !! pointer to the tensor in memory
  contains
    procedure :: rank => torch_tensor_get_rank
    procedure :: shape => torch_tensor_get_shape
    procedure :: stride => torch_tensor_get_stride
    procedure :: dtype => torch_tensor_get_dtype
    procedure :: device_type => torch_tensor_get_device_type
    procedure :: device_index => torch_tensor_get_device_index
    procedure :: requires_grad => torch_tensor_requires_grad
    procedure :: zero => torch_tensor_zero
    procedure :: zero_grad => torch_tensor_zero_grad
    procedure :: print => torch_tensor_print
    final :: torch_tensor_delete
  end type torch_tensor

  ! ============================================================================
  ! --- Interfaces for tensor constructors
  ! ============================================================================

  !> Interface for directing `torch_tensor_from_array` to possible input types and ranks
  !> Signature: (tensor, data, device_type, [device_index], [permute_dims], [requires_grad])
  interface torch_tensor_from_array
    module procedure torch_tensor_from_array_int8_1d
    module procedure torch_tensor_from_array_int8_2d
    module procedure torch_tensor_from_array_int8_3d
    module procedure torch_tensor_from_array_int8_4d
    module procedure torch_tensor_from_array_int8_5d
    module procedure torch_tensor_from_array_int16_1d
    module procedure torch_tensor_from_array_int16_2d
    module procedure torch_tensor_from_array_int16_3d
    module procedure torch_tensor_from_array_int16_4d
    module procedure torch_tensor_from_array_int16_5d
    module procedure torch_tensor_from_array_int32_1d
    module procedure torch_tensor_from_array_int32_2d
    module procedure torch_tensor_from_array_int32_3d
    module procedure torch_tensor_from_array_int32_4d
    module procedure torch_tensor_from_array_int32_5d
    module procedure torch_tensor_from_array_int64_1d
    module procedure torch_tensor_from_array_int64_2d
    module procedure torch_tensor_from_array_int64_3d
    module procedure torch_tensor_from_array_int64_4d
    module procedure torch_tensor_from_array_int64_5d
    module procedure torch_tensor_from_array_real32_1d
    module procedure torch_tensor_from_array_real32_2d
    module procedure torch_tensor_from_array_real32_3d
    module procedure torch_tensor_from_array_real32_4d
    module procedure torch_tensor_from_array_real32_5d
    module procedure torch_tensor_from_array_real64_1d
    module procedure torch_tensor_from_array_real64_2d
    module procedure torch_tensor_from_array_real64_3d
    module procedure torch_tensor_from_array_real64_4d
    module procedure torch_tensor_from_array_real64_5d
  end interface

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array:
  !> `(tensor, data, layout, device_type, [device_index], [requires_grad])`.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  interface torch_tensor_from_array_legacy
    module procedure torch_tensor_from_array_int8_1d_legacy
    module procedure torch_tensor_from_array_int8_2d_legacy
    module procedure torch_tensor_from_array_int8_3d_legacy
    module procedure torch_tensor_from_array_int8_4d_legacy
    module procedure torch_tensor_from_array_int8_5d_legacy
    module procedure torch_tensor_from_array_int16_1d_legacy
    module procedure torch_tensor_from_array_int16_2d_legacy
    module procedure torch_tensor_from_array_int16_3d_legacy
    module procedure torch_tensor_from_array_int16_4d_legacy
    module procedure torch_tensor_from_array_int16_5d_legacy
    module procedure torch_tensor_from_array_int32_1d_legacy
    module procedure torch_tensor_from_array_int32_2d_legacy
    module procedure torch_tensor_from_array_int32_3d_legacy
    module procedure torch_tensor_from_array_int32_4d_legacy
    module procedure torch_tensor_from_array_int32_5d_legacy
    module procedure torch_tensor_from_array_int64_1d_legacy
    module procedure torch_tensor_from_array_int64_2d_legacy
    module procedure torch_tensor_from_array_int64_3d_legacy
    module procedure torch_tensor_from_array_int64_4d_legacy
    module procedure torch_tensor_from_array_int64_5d_legacy
    module procedure torch_tensor_from_array_real32_1d_legacy
    module procedure torch_tensor_from_array_real32_2d_legacy
    module procedure torch_tensor_from_array_real32_3d_legacy
    module procedure torch_tensor_from_array_real32_4d_legacy
    module procedure torch_tensor_from_array_real32_5d_legacy
    module procedure torch_tensor_from_array_real64_1d_legacy
    module procedure torch_tensor_from_array_real64_2d_legacy
    module procedure torch_tensor_from_array_real64_3d_legacy
    module procedure torch_tensor_from_array_real64_4d_legacy
    module procedure torch_tensor_from_array_real64_5d_legacy
  end interface

  interface
    function torch_from_blob_c(data, ndims, tensor_shape, strides, dtype, &
                               device_type, device_index, &
                               requires_grad) result(tensor_p) &
                               bind(c, name = "torch_from_blob")
      use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t, c_ptr

      implicit none

      ! Arguments
      type(c_ptr), value, intent(in)    :: data
      integer(c_int), value, intent(in) :: ndims
      integer(c_int64_t), intent(in)    :: tensor_shape(*)
      integer(c_int64_t), intent(in)    :: strides(*)
      integer(c_int), value, intent(in) :: dtype
      integer(c_int), value, intent(in) :: device_type
      integer(c_int), value, intent(in) :: device_index
      logical(c_bool), value, intent(in) :: requires_grad
      type(c_ptr)                       :: tensor_p
    end function torch_from_blob_c
  end interface

  ! ============================================================================
  ! --- Interfaces for overloaded operators acting on tensors
  ! ============================================================================

  interface assignment (=)
    module procedure torch_tensor_assign
  end interface

  interface operator (+)
    module procedure torch_tensor_add
  end interface

  interface operator (-)
    module procedure torch_tensor_negative
    module procedure torch_tensor_subtract
  end interface

  interface operator (*)
    module procedure torch_tensor_multiply
  end interface

  interface
    subroutine torch_tensor_multiply_c(output_c, tensor1_c, tensor2_c) &
        bind(c, name = "torch_tensor_multiply")
      use, intrinsic :: iso_c_binding, only : c_ptr
      implicit none
      type(c_ptr), value, intent(in) :: output_c
      type(c_ptr), value, intent(in) :: tensor1_c
      type(c_ptr), value, intent(in) :: tensor2_c
    end subroutine torch_tensor_multiply_c
  end interface

  interface operator (/)
    module procedure torch_tensor_divide
  end interface

  interface
    subroutine torch_tensor_divide_c(output_c, tensor1_c, tensor2_c) &
        bind(c, name = "torch_tensor_divide")
      use, intrinsic :: iso_c_binding, only : c_ptr
      implicit none
      type(c_ptr), value, intent(in) :: output_c
      type(c_ptr), value, intent(in) :: tensor1_c
      type(c_ptr), value, intent(in) :: tensor2_c
    end subroutine torch_tensor_divide_c
  end interface

  interface operator (**)
    module procedure torch_tensor_power_int8
    module procedure torch_tensor_power_int16
    module procedure torch_tensor_power_int32
    module procedure torch_tensor_power_int64
    module procedure torch_tensor_power_real32
    module procedure torch_tensor_power_real64
  end interface

  ! ============================================================================
  ! --- Interfaces related to automatic differentation functionality for tensors
  ! ============================================================================

  interface torch_tensor_backward
    module procedure torch_tensor_backward_with_external_gradient
    module procedure torch_tensor_backward_without_external_gradient
  end interface

contains

  ! ============================================================================
  ! --- Procedures for constructing tensors
  ! ============================================================================

  !> Returns a tensor with uninitialised values.
  subroutine torch_tensor_empty(tensor, ndims, tensor_shape, dtype, &
                                device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t
    type(torch_tensor), intent(out) :: tensor     !! Returned tensor
    integer(int32), intent(in)      :: ndims      !! Number of dimensions of the tensor
    integer(int64), intent(in)      :: tensor_shape(ndims)   !! Shape of the tensor
    integer(c_int), intent(in)      :: dtype      !! Data type of the tensor
    integer(c_int), intent(in)      :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor
    integer(c_int)                  :: ndims_c_int  !! C-type ndims
    integer(c_int64_t)              :: tensor_shape_c_int(ndims)  !! C-type tensor_shape
    integer(c_int)                  :: device_index_value  !! device index used
    logical(c_bool)                 :: requires_grad_value
        !! Whether gradients need to be computed for the created tensor

    interface
      function torch_empty_c(ndims_c, tensor_shape_c, dtype_c, device_type_c, &
          device_index_c, requires_grad_c) result(tensor_c) &
          bind(c, name = "torch_empty")
        use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t, c_ptr

        implicit none

        integer(c_int), value, intent(in) :: ndims_c
        integer(c_int64_t), intent(in)    :: tensor_shape_c(*)
        integer(c_int), value, intent(in) :: dtype_c
        integer(c_int), value, intent(in) :: device_type_c
        integer(c_int), value, intent(in) :: device_index_c
        logical(c_bool), value, intent(in) :: requires_grad_c
        type(c_ptr)                       :: tensor_c
      end function torch_empty_c
    end interface

    ! Process optional arguments
    if (present(device_index)) then
      device_index_value = device_index
    else if (device_type == torch_kCPU) then
      device_index_value = -1
    else
      device_index_value = 0
    end if

    if (.not. present(requires_grad)) then
      requires_grad_value = logical(.false., c_bool)
    else
      requires_grad_value = requires_grad
    end if

    ! Convert public arguments to C-types
    ndims_c_int = ndims
    tensor_shape_c_int(:) = tensor_shape(:)

    tensor%p = torch_empty_c(ndims_c_int, tensor_shape_c_int, dtype, device_type,   &
                             device_index_value, requires_grad_value)
  end subroutine torch_tensor_empty

  !> Returns a tensor filled with the scalar value 0.
  subroutine torch_tensor_zeros(tensor, ndims, tensor_shape, dtype, &
                                device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t
    type(torch_tensor), intent(out) :: tensor     !! Returned tensor
    integer(int32), intent(in)      :: ndims      !! Number of dimensions of the tensor
    integer(int64), intent(in)      :: tensor_shape(ndims)   !! Shape of the tensor
    integer(c_int), intent(in)      :: dtype      !! Data type of the tensor
    integer(c_int), intent(in)      :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor
    integer(c_int)                  :: ndims_c_int  !! C-type ndims
    integer(c_int64_t)              :: tensor_shape_c_int(ndims)  !! C-type tensor_shape
    integer(c_int)                  :: device_index_value   !! device index used
    logical(c_bool)                 :: requires_grad_value
        !! Whether gradients need to be computed for the created tensor

    interface
      function torch_zeros_c(ndims_c, tensor_shape_c, dtype_c, &
                             device_type_c, device_index_c, requires_grad_c) result(tensor_c) &
          bind(c, name = "torch_zeros")
        use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t, c_ptr

        implicit none

        integer(c_int), value, intent(in) :: ndims_c
        integer(c_int64_t), intent(in)    :: tensor_shape_c(*)
        integer(c_int), value, intent(in) :: dtype_c
        integer(c_int), value, intent(in) :: device_type_c
        integer(c_int), value, intent(in) :: device_index_c
        logical(c_bool), value, intent(in) :: requires_grad_c
        type(c_ptr)                       :: tensor_c
      end function torch_zeros_c
    end interface

    ! Process optional arguments
    if (present(device_index)) then
      device_index_value = device_index
    else if (device_type == torch_kCPU) then
      device_index_value = -1
    else
      device_index_value = 0
    end if

    if (.not. present(requires_grad)) then
      requires_grad_value = logical(.false., c_bool)
    else
      requires_grad_value = requires_grad
    end if

    ! Convert public arguments to C-types
    ndims_c_int = ndims
    tensor_shape_c_int(:) = tensor_shape(:)

    tensor%p = torch_zeros_c(ndims_c_int, tensor_shape_c_int, dtype, device_type,     &
                             device_index_value, requires_grad_value)
  end subroutine torch_tensor_zeros

  !> Returns a tensor filled with the scalar value 1.
  subroutine torch_tensor_ones(tensor, ndims, tensor_shape, dtype, &
                               device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t
    type(torch_tensor), intent(out) :: tensor     !! Returned tensor
    integer(int32), intent(in)      :: ndims      !! Number of dimensions of the tensor
    integer(int64), intent(in)      :: tensor_shape(ndims)   !! Shape of the tensor
    integer(c_int), intent(in)      :: dtype        !! Data type of the tensor
    integer(c_int), intent(in)      :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor
    integer(c_int)                  :: ndims_c_int  !! C-type ndims
    integer(c_int64_t)              :: tensor_shape_c_int(ndims)  !! C-type tensor_shape
    integer(c_int)                  :: device_index_value    !! device index used
    logical(c_bool)                 :: requires_grad_value
        !! Whether gradients need to be computed for the created tensor

    interface
      function torch_ones_c(ndims_c, tensor_shape_c, dtype_c, &
                            device_type_c, device_index_c, requires_grad_c) result(tensor_c) &
          bind(c, name = "torch_ones")
        use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t, c_ptr

        implicit none

        integer(c_int), value, intent(in) :: ndims_c
        integer(c_int64_t), intent(in)    :: tensor_shape_c(*)
        integer(c_int), value, intent(in) :: dtype_c
        integer(c_int), value, intent(in) :: device_type_c
        integer(c_int), value, intent(in) :: device_index_c
        logical(c_bool), value, intent(in) :: requires_grad_c
        type(c_ptr)                       :: tensor_c
      end function torch_ones_c
    end interface

    ! Process optional arguments
    if (present(device_index)) then
      device_index_value = device_index
    else if (device_type == torch_kCPU) then
      device_index_value = -1
    else
      device_index_value = 0
    end if

    if (.not. present(requires_grad)) then
      requires_grad_value = logical(.false., c_bool)
    else
      requires_grad_value = requires_grad
    end if

    ! Convert public arguments to C-types
    ndims_c_int = ndims
    tensor_shape_c_int(:) = tensor_shape(:)

    tensor%p = torch_ones_c(ndims_c_int, tensor_shape_c_int, dtype, device_type,      &
                            device_index_value, requires_grad_value)
  end subroutine torch_tensor_ones

  !| Exposes the given data as a tensor without taking ownership of the original data.
  !  This routine will take an array in memory and return a tensor as specified by
  !  the shape and stride input arguments.
  !
  ! Note that `data` needs to be a pointer to a **contiguous** block of memory!
  ! This is not generally not the case when calling `c_loc` on a Fortran array as
  ! array slicing can lead to non-contiguous memory.
  ! Please consider asserting that the data is contiguous with the `is_contiguous`
  ! implicit procedure before calling this routine.
  subroutine torch_tensor_from_blob(tensor, data, ndims, tensor_shape, tensor_strides, dtype, &
                                    device_type, device_index, &
                                    requires_grad)
    use, intrinsic :: iso_c_binding, only : c_bool, c_int, c_int64_t, c_ptr
    type(torch_tensor), intent(out) :: tensor     !! Returned tensor
    type(c_ptr), intent(in)         :: data       !! Pointer to data
    integer(int32), intent(in)      :: ndims      !! Number of dimensions of the tensor
    integer(int64), intent(in)      :: tensor_shape(ndims)  !! Shape of the returned tensor
    integer(int64), intent(in)      :: tensor_strides(ndims)
        !! Strides for accessing data in the returned tensor. Note that these are integers
        !! representing the number of items of type `dtype` and NOT bytes/memory.
    integer(c_int), intent(in)      :: dtype      !! Data type of the input data and tensor
    integer(c_int), intent(in)      :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(c_int)                  :: ndims_c            !! C-type ndims
    integer(c_int64_t)              :: tensor_shape_c(ndims)  !! C-type tensor_shape
    integer(c_int64_t)              :: tensor_strides_c(ndims)  !! C-type strides
    integer(c_int)                  :: device_index_value   !! device index used
    logical(c_bool)                 :: requires_grad_value
        !! Whether gradients need to be computed for the created tensor

    ! Process optional arguments
    if (.not. present(requires_grad)) then
      requires_grad_value = logical(.false., c_bool)
    else
      requires_grad_value = requires_grad
    end if

    if (present(device_index)) then
      device_index_value = device_index
    else if (device_type == torch_kCPU) then
      device_index_value = -1
    else
      device_index_value = 0
    end if

    ! Convert public arguments to C-types
    ndims_c = ndims
    tensor_shape_c(:) = tensor_shape(:)
    tensor_strides_c(:) = tensor_strides(:)

    tensor%p = torch_from_blob_c(data, ndims_c, tensor_shape_c, tensor_strides_c, dtype, &
                                 device_type, device_index_value,              &
                                 requires_grad_value)
  end subroutine torch_tensor_from_blob

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `int8`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int8_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt8  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int8_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `int8`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int8_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt8  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int8_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `int8`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int8_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt8  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int8_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `int8`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int8_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt8  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int8_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `int8`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int8_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt8  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int8_5d

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `int16`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int16_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt16  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int16_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `int16`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int16_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt16  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int16_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `int16`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int16_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt16  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int16_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `int16`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int16_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt16  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int16_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `int16`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int16_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt16  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int16_5d

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `int32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int32_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt32  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int32_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `int32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int32_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt32  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int32_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `int32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int32_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt32  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int32_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `int32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int32_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt32  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int32_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `int32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int32_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt32  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int32_5d

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `int64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int64_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt64  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int64_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `int64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int64_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt64  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int64_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `int64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int64_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt64  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int64_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `int64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int64_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt64  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int64_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `int64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_int64_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kInt64  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_int64_5d

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `real32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real32_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat32  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real32_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `real32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real32_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat32  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real32_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `real32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real32_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat32  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real32_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `real32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real32_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat32  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real32_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `real32`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real32_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat32  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real32_5d

  !> Return a Torch tensor pointing to data_in array of rank 1 containing data of type `real64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real64_1d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(1)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(1)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(1)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(1)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(1)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat64  !! Data type
    integer(int32), parameter :: ndims = 1
        !! Number of dimension of input data
    logical                   :: permute_valid(1)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 1 and contain numbers 1 to 1.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 1]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real64_1d

  !> Return a Torch tensor pointing to data_in array of rank 2 containing data of type `real64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real64_2d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(2)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(2)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(2)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(2)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(2)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat64  !! Data type
    integer(int32), parameter :: ndims = 2
        !! Number of dimension of input data
    logical                   :: permute_valid(2)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 2 and contain numbers 1 to 2.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 2]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real64_2d

  !> Return a Torch tensor pointing to data_in array of rank 3 containing data of type `real64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real64_3d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(3)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(3)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(3)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(3)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(3)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat64  !! Data type
    integer(int32), parameter :: ndims = 3
        !! Number of dimension of input data
    logical                   :: permute_valid(3)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 3 and contain numbers 1 to 3.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 3]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real64_3d

  !> Return a Torch tensor pointing to data_in array of rank 4 containing data of type `real64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real64_4d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(4)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(4)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(4)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(4)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(4)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat64  !! Data type
    integer(int32), parameter :: ndims = 4
        !! Number of dimension of input data
    logical                   :: permute_valid(4)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 4 and contain numbers 1 to 4.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 4]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real64_4d

  !> Return a Torch tensor pointing to data_in array of rank 5 containing data of type `real64`
  !> This subroutine is part of an interface and should be accessed through
  !> `torch_tensor_from_array`.
  subroutine torch_tensor_from_array_real64_5d(tensor, data_in, &
                                                         device_type, device_index, permute_dims, &
                                                         requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer, optional, intent(in) :: device_index
        !! Device index for GPU devices
    integer(int32), optional, intent(in) :: permute_dims(5)
        !! Permutation of dimensions to be applied to Fortran data in the resulting tensor.
        !! Takes the form of an array of length `n` with elements `1` to `n`, where `n`
        !! is the `rank`. Element `i` indicates which dimension of the Fortran array
        !! appears as dimension `i` on the Torch tensor.
        !! e.g. a Fortran array of shape [10, 20, 30] permuted by [2, 3, 1] will result
        !! in Torch shape [20, 30, 10]. This matches behaviour of `torch.permute()`,
        !! noting that this is Fortran so we index from 1!
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int64)        :: fortran_shape(5)
        !! Shape of the unpermuted Fortran array
    integer(int64)        :: fortran_strides(5)
        !! Strides for the Fortran array
    integer(int64)        :: torch_shape(5)
        !! Shape of the Torch tensor
    integer(int64)        :: torch_strides(5)
        !! Strides for the Torch tensor
    integer(c_int), parameter :: dtype = torch_kFloat64  !! Data type
    integer(int32), parameter :: ndims = 5
        !! Number of dimension of input data
    logical                   :: permute_valid(5)
        !! Array to check supplied permutation is valid
    integer :: i

    fortran_shape = shape(data_in)

    ! Compute native Fortran strides
    do i = 1, ndims
      if (i == 1) then
        fortran_strides(1) = 1_int64
      else
        fortran_strides(i) = fortran_strides(i - 1) * fortran_shape(i-1)
      end if
    end do

    if (present(permute_dims)) then
      ! Check that the supplied permutation is valid and raise an error if not.
      ! Should be of length 5 and contain numbers 1 to 5.
      permute_valid = .false.
      do i = 1, ndims
        if (permute_dims(i) < 1 .or. permute_dims(i) > ndims) then
          error stop "Invalid permute_dims: element out of range [1, 5]"
        end if
        if (permute_valid(permute_dims(i))) then
          error stop "Invalid permute_dims: duplicate dimension"
        end if
        permute_valid(permute_dims(i)) = .true.
      end do

      ! permute shape of torch tensor to match permutation requested ('transpose')
      do i = 1, ndims
          torch_shape(i) = fortran_shape(permute_dims(i))
          torch_strides(i) = fortran_strides(permute_dims(i))
      end do

    else
      ! Keep Torch shape and strides same as Fortran
      torch_shape = fortran_shape
      torch_strides = fortran_strides
    end if

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, torch_shape, &
                                torch_strides, dtype, device_type, device_index, &
                                requires_grad)

   end subroutine torch_tensor_from_array_real64_5d


  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int8_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt8
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int8_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int8_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt8
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int8_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int8_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt8
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int8_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int8_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt8
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int8_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int8_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int8

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int8), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt8
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int8_5d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int16_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt16
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int16_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int16_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt16
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int16_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int16_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt16
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int16_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int16_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt16
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int16_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int16_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int16

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int16), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt16
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int16_5d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int32_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt32
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int32_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int32_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt32
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int32_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int32_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt32
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int32_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int32_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt32
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int32_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int32_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int32), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt32
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int32_5d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int64_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt64
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int64_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int64_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt64
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int64_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int64_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt64
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int64_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int64_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt64
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int64_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_int64_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : int64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    integer(kind=int64), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kInt64
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_int64_5d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real32_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat32
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real32_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real32_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat32
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real32_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real32_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat32
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real32_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real32_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat32
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real32_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real32_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real32

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real32), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat32
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real32_5d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real64_1d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(1)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 1
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat64
        !! Data type
    integer(int64)                :: tensor_shape(1)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(1)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real64_1d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real64_2d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(2)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 2
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat64
        !! Data type
    integer(int64)                :: tensor_shape(2)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(2)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real64_2d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real64_3d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(3)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 3
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat64
        !! Data type
    integer(int64)                :: tensor_shape(3)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(3)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real64_3d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real64_4d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(4)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 4
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat64
        !! Data type
    integer(int64)                :: tensor_shape(4)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(4)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real64_4d_legacy

  !> deprecated: true
  !> This is the old layout-required signature for torch_tensor_from_array.
  !> Use `torch_tensor_from_array` instead. This will be removed in a future release.
  subroutine torch_tensor_from_array_real64_5d_legacy(tensor, data_in, layout, &
                                                    device_type, device_index, requires_grad)
    use, intrinsic :: iso_c_binding, only : c_int, c_loc
    use, intrinsic :: iso_fortran_env, only : real64

    ! output tensor
    type(torch_tensor), intent(out) :: tensor  !! Returned tensor

    ! inputs
    real(kind=real64), intent(in), pointer, contiguous :: data_in(:,:,:,:,:)
        !! Input data that tensor will point at
    integer(c_int), intent(in)    :: device_type
        !! Device type the tensor will live on (`torch_kCPU` or a GPU device type)
    integer(int32), intent(in) :: layout(5)  !! Control order of indices
    integer, optional, intent(in) :: device_index   !! Device index for GPU devices
    logical, optional, intent(in) :: requires_grad
        !! Whether gradients need to be computed for the created tensor

    ! local data
    integer(int32), parameter     :: ndims = 5
        !! Number of dimension of input data
    integer(c_int), parameter     :: dtype = torch_kFloat64
        !! Data type
    integer(int64)                :: tensor_shape(5)
        !! Shape of the input tensor
    integer(int64)                :: tensor_strides(5)
        !! Strides for accessing data appropriately
    integer :: i

    write(*,*) "Warning: torch_tensor_from_array_legacy is deprecated and will be "
    write(*,*) "removed in a future version of FTorch. Please use torch_tensor_from_array, "
    write(*,*) "passing permute_dims as an optional argument after device_type, where possible."

    tensor_shape = shape(data_in)

    tensor_strides(:) = 0
    do i = 1, ndims
      if (i == 1) then
        tensor_strides(layout(i)) = 1_int64
      else
        tensor_strides(layout(i)) = tensor_strides(layout(i - 1)) * tensor_shape(layout(i - 1))
      end if
    end do

    call torch_tensor_from_blob(tensor, c_loc(data_in), ndims, tensor_shape, &
                                tensor_strides, dtype, device_type, device_index, &
                                requires_grad)

  end subroutine torch_tensor_from_array_real64_5d_legacy


  ! ============================================================================
  ! --- Procedures for interrogating tensors
  ! ============================================================================

  !> Prints the contents of a tensor.
  subroutine torch_tensor_print(self)
    class(torch_tensor), intent(in) :: self  !! Tensor to print the contents of

    interface
      subroutine torch_tensor_print_c(tensor_c) &
          bind(c, name = "torch_tensor_print")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_print_c
    end interface

    call torch_tensor_print_c(self%p)
  end subroutine torch_tensor_print

  !> Determines the rank of a tensor.
  function torch_tensor_get_rank(self) result(rank)
    class(torch_tensor), intent(in) :: self  !! Tensor to get the rank of
    integer(kind=int32) :: rank              !! Rank of tensor

    interface
      function torch_tensor_get_rank_c(tensor_c) result(rank_c) &
          bind(c, name = "torch_tensor_get_rank")
        use, intrinsic :: iso_c_binding, only : c_int, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        integer(c_int) :: rank_c
      end function torch_tensor_get_rank_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its rank is unset"
      stop 1
    end if
    rank = torch_tensor_get_rank_c(self%p)
  end function torch_tensor_get_rank

  !> Determines the shape of a tensor.
  function torch_tensor_get_shape(self) result(sizes)
    use, intrinsic :: iso_c_binding, only : c_f_pointer, c_int64_t, c_ptr
    class(torch_tensor), intent(in) :: self         !! Tensor to get the shape of
    integer(kind=int64), allocatable :: sizes(:)    !! Array holding the shape of the tensor

    ! Local data
    integer(kind=int32) :: ndims(1)
    integer(kind=c_int64_t), pointer :: sizes_c_int64_ptr(:)
        !! Temporary pointer to Torch-owned memory containing c_int64
    type(c_ptr) :: cptr

    interface
      function torch_tensor_get_sizes_c(tensor_c) result(sizes_c) &
          bind(c, name = "torch_tensor_get_sizes")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr) :: sizes_c
      end function torch_tensor_get_sizes_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its shape is unset"
      stop 1
    end if
    ndims(1) = self%rank()
    cptr = torch_tensor_get_sizes_c(self%p)
    call c_f_pointer(cptr, sizes_c_int64_ptr, ndims)

    ! Copy out of the Torch-owned memory so the result remains valid even if
    ! the tensor is subsequently deleted
    allocate(sizes(ndims(1)))
    sizes(:) = sizes_c_int64_ptr(:)
  end function torch_tensor_get_shape

  !> Return the strides of the tensor
  function torch_tensor_get_stride(self) result(strides)
    use, intrinsic :: iso_c_binding, only : c_f_pointer, c_int64_t, c_ptr
    class(torch_tensor), intent(in) :: self         !! Tensor to get the strides of
    integer(kind=int64), allocatable :: strides(:)  !! Array holding the strides of the tensor

    ! Local data
    integer(kind=int32) :: ndims(1)
    integer(kind=c_int64_t), pointer :: strides_c_int64_ptr(:)
        !! Temporary pointer to Torch-owned memory containing c_int64
    type(c_ptr) :: cptr

    interface
      function torch_tensor_get_stride_c(tensor_c) result(strides_c) &
          bind(c, name = "torch_tensor_get_stride")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr) :: strides_c
      end function torch_tensor_get_stride_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its strides are unset"
      stop 1
    end if

    ndims(1) = self%rank()
    cptr = torch_tensor_get_stride_c(self%p)
    call c_f_pointer(cptr, strides_c_int64_ptr, ndims)

    ! Copy out of the Torch-owned memory so the result remains valid even if
    ! the tensor is subsequently deleted
    allocate(strides(ndims(1)))
    strides(:) = strides_c_int64_ptr(:)

  end function torch_tensor_get_stride

  !> Returns the data type of a tensor.
  function torch_tensor_get_dtype(self) result(dtype)
    use, intrinsic :: iso_c_binding, only : c_int
    class(torch_tensor), intent(in) :: self  !! Tensor to get the data type of
    integer(c_int) :: dtype                  !! Data type of tensor

    interface
      function torch_tensor_get_dtype_c(tensor_c) result(dtype_c) &
          bind(c, name = "torch_tensor_get_dtype")
        use, intrinsic :: iso_c_binding, only : c_int, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        integer(c_int) :: dtype_c
      end function torch_tensor_get_dtype_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its data type is unset"
      stop 1
    end if
    dtype = torch_tensor_get_dtype_c(self%p)
  end function torch_tensor_get_dtype

  !> Returns the device type of a tensor.
  function torch_tensor_get_device_type(self) result(device_type)
    use, intrinsic :: iso_c_binding, only : c_int
    class(torch_tensor), intent(in) :: self  !! Tensor to get the device type of
    integer(c_int) :: device_type            !! Device type of tensor

    interface
      function torch_tensor_get_device_type_c(tensor_c) result(device_type_c) &
          bind(c, name = "torch_tensor_get_device_type")
        use, intrinsic :: iso_c_binding, only : c_int, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        integer(c_int) :: device_type_c
      end function torch_tensor_get_device_type_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its device type is unset"
      stop 1
    end if
    device_type = torch_tensor_get_device_type_c(self%p)
  end function torch_tensor_get_device_type

  !> Determines the device index of a tensor.
  function torch_tensor_get_device_index(self) result(device_index)
    use, intrinsic :: iso_c_binding, only : c_int
    class(torch_tensor), intent(in) :: self  !! Tensor to get the device index of
    integer(c_int) :: device_index           !! Device index of tensor

    interface
      function torch_tensor_get_device_index_c(tensor_c) result(device_index_c) &
          bind(c, name = "torch_tensor_get_device_index")
        use, intrinsic :: iso_c_binding, only : c_int, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        integer(c_int) :: device_index_c
      end function torch_tensor_get_device_index_c
    end interface

    if (.not. c_associated(self%p)) then
      write(*,*) "Error :: tensor has not been constructed so its device index is unset"
      stop 1
    end if
    device_index = torch_tensor_get_device_index_c(self%p)
  end function torch_tensor_get_device_index

  !> Determines whether a tensor requires the autograd module.
  function torch_tensor_requires_grad(self) result(requires_grad)
    class(torch_tensor), intent(in) :: self  !! Tensor to query
    logical :: requires_grad                 !! Whether the tensor requires autograd

    interface
      function torch_tensor_requires_grad_c(tensor_c) result(requires_grad_c) &
          bind(c, name = "torch_tensor_requires_grad")
        use, intrinsic :: iso_c_binding, only : c_bool, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        logical(c_bool) :: requires_grad_c
      end function torch_tensor_requires_grad_c
    end interface

    requires_grad = torch_tensor_requires_grad_c(self%p)
  end function torch_tensor_requires_grad

  ! ============================================================================
  ! --- Procedures for deallocating tensors
  ! ============================================================================

  !> Deallocates a tensor (elemental).
  !> Note: Marked as impure due to C interoperability, though the operation is
  !> conceptally pure (deletion of a specific C++ Tensor object).
  impure elemental subroutine torch_tensor_delete(tensor)
    use, intrinsic :: iso_c_binding, only : c_associated, c_null_ptr
    type(torch_tensor), intent(inout) :: tensor  !! Tensor to deallocate

    interface
      subroutine torch_tensor_delete_c(tensor_c) &
          bind(c, name = "torch_tensor_delete")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_delete_c
    end interface

    ! Call the destructor, if it hasn't already been called
    if (c_associated(tensor%p)) then
      call torch_tensor_delete_c(tensor%p)
      tensor%p = c_null_ptr
    end if
  end subroutine torch_tensor_delete

  ! ============================================================================
  ! --- Procedures for manipulating tensors
  ! ============================================================================

  !> Fills a tensor with the scalar value 0.
  subroutine torch_tensor_zero(tensor)
    use, intrinsic :: iso_c_binding, only : c_associated
    class(torch_tensor), intent(inout) :: tensor !! Tensor whose values are to be zeroed

    interface
      subroutine torch_tensor_zero_c(tensor_c) bind(c, name = "torch_tensor_zero")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_zero_c
    end interface

    if (.not. c_associated(tensor%p)) then
      write(*,*) "Error :: tensor must be constructed before zeroing values"
      stop 1
    end if
    call torch_tensor_zero_c(tensor%p)
  end subroutine torch_tensor_zero

  !> Moves a source_tensor tensor to a target tensor's device and dtype
  subroutine torch_tensor_to(source_tensor, target_tensor, non_blocking)
    use, intrinsic :: iso_c_binding, only : c_bool, c_int
    type(torch_tensor), intent(in) :: source_tensor      !! Source tensor to be moved
    type(torch_tensor), intent(inout) :: target_tensor
        !! Target tensor with the desired device and dtype
    logical, optional, intent(in) :: non_blocking        !! Whether to perform asynchronous copy
    logical(c_bool) :: non_blocking_value
    integer(c_int) :: source_rank, target_rank, i
    integer(int64), allocatable :: source_shape(:), target_shape(:)

    interface
      subroutine torch_tensor_to_c(source_tensor_c, target_tensor_c, non_blocking_c) &
          bind(c, name = "torch_tensor_to")
        use, intrinsic :: iso_c_binding, only : c_bool, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: source_tensor_c
        type(c_ptr), value, intent(in) :: target_tensor_c
        logical(c_bool), value, intent(in) :: non_blocking_c
      end subroutine torch_tensor_to_c
    end interface

    ! Check for rank and shape consistency between the source and target tensors
    source_rank = source_tensor%rank()
    target_rank = target_tensor%rank()

    if (source_rank /= target_rank) then
      write(*,*) "Error in torch_tensor_to :: Cannot move source_tensor to target_tensor &
                 &because the ranks do not match."
      write(*,*) "Source tensor rank:", source_rank, "Target tensor rank:", target_rank
      stop 1
    end if

    source_shape = source_tensor%shape()
    target_shape = target_tensor%shape()

    do i = 1, source_rank
      if (source_shape(i) /= target_shape(i)) then
        write(*,*) "Error in torch_tensor_to :: Cannot move source_tensor to target_tensor &
                   &because the shapes do not match."
        write(*,*) "Dimension", i, "mismatch: source_tensor =", source_shape(i), &
            "Target =", target_shape(i)
        stop 1
      end if
    end do

    ! Process optional arguments
    if (present(non_blocking)) then
      non_blocking_value = non_blocking
    else
      non_blocking_value = .false.
    end if

    call torch_tensor_to_c(source_tensor%p, target_tensor%p, non_blocking_value)

  end subroutine torch_tensor_to

  ! ============================================================================
  ! --- Overloaded operators acting on tensors
  ! ============================================================================

  !> Overloads assignment operator for tensors.
  subroutine torch_tensor_assign(output, input)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: input      !! Tensor whose values are to be used
    type(torch_tensor), intent(inout) :: output  !! Tensor to assign values to

    interface
      subroutine torch_tensor_assign_c(output_c, input_c) bind(c, name = "torch_tensor_assign")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: input_c
      end subroutine torch_tensor_assign_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, input%rank(), input%shape(), input%dtype(), &
                              input%device_type(), device_index=input%device_index(), &
                              requires_grad=input%requires_grad())
    else
      if (input%device_type() /= output%device_type()) then
        write(*,*) "Error :: cannot assign tensors with different device types"
        stop 1
      end if
      if (input%device_index() /= output%device_index()) then
        write(*,*) "Error :: cannot assign tensors with different device indices"
        stop 1
      end if
    end if
    call torch_tensor_assign_c(output%p, input%p)
  end subroutine torch_tensor_assign

  !> Overloads addition operator for two tensors.
  function torch_tensor_add(tensor1, tensor2) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: tensor1  !! First tensor to be added
    type(torch_tensor), intent(in) :: tensor2  !! Second tensor to be added
    type(torch_tensor) :: output               !! Tensor to hold the sum

    interface
      subroutine torch_tensor_add_c(output_c, tensor1_c, tensor2_c) &
          bind(c, name = "torch_tensor_add")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor1_c
        type(c_ptr), value, intent(in) :: tensor2_c
        type(c_ptr), value, intent(in) :: output_c
      end subroutine torch_tensor_add_c
    end interface

    if (tensor1%device_type() /= tensor2%device_type()) then
      write(*,*) "Error :: cannot add tensors with different device types"
      stop 1
    end if
    if (tensor1%device_index() /= tensor2%device_index()) then
      write(*,*) "Error :: cannot add tensors with different device indices"
      stop 1
    end if

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor1%rank(), tensor1%shape(), &
                              tensor1%dtype(), tensor1%device_type(), &
                              device_index=tensor1%device_index(), &
                              requires_grad=tensor1%requires_grad())
    end if
    call torch_tensor_add_c(output%p,tensor1%p, tensor2%p)
  end function torch_tensor_add

  !> Overloads negative operator for a single tensor.
  function torch_tensor_negative(tensor) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: tensor  !! Tensor to take the negative of
    type(torch_tensor) :: output              !! Tensor to hold the negative values

    interface
      subroutine torch_tensor_negative_c(output_c, tensor_c) bind(c, name = "torch_tensor_negative")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: output_c
      end subroutine torch_tensor_negative_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_negative_c(output%p, tensor%p)
  end function torch_tensor_negative

  !> Overloads subtraction operator for two tensors.
  function torch_tensor_subtract(tensor1, tensor2) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: tensor1  !! First tensor for the subtraction
    type(torch_tensor), intent(in) :: tensor2  !! Second tensor for the subtraction
    type(torch_tensor) :: output               !! Tensor to hold the difference

    interface
      subroutine torch_tensor_subtract_c(output_c, tensor1_c, tensor2_c) &
          bind(c, name = "torch_tensor_subtract")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor1_c
        type(c_ptr), value, intent(in) :: tensor2_c
      end subroutine torch_tensor_subtract_c
    end interface

    if (tensor1%device_type() /= tensor2%device_type()) then
      write(*,*) "Error :: cannot subtract tensors with different device types"
      stop 1
    end if
    if (tensor1%device_index() /= tensor2%device_index()) then
      write(*,*) "Error :: cannot subtract tensors with different device indices"
      stop 1
    end if

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor1%rank(), tensor1%shape(), &
                              tensor1%dtype(), tensor1%device_type(), &
                              device_index=tensor1%device_index(), &
                              requires_grad=tensor1%requires_grad())
    end if
    call torch_tensor_subtract_c(output%p, tensor1%p, tensor2%p)
  end function torch_tensor_subtract

  !> Overloads multiplication operator for two tensors.
  function torch_tensor_multiply(tensor1, tensor2) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: tensor1  !! First tensor to be multiplied
    type(torch_tensor), intent(in) :: tensor2  !! Second tensor to be multiplied
    type(torch_tensor) :: output               !! Tensor to hold the product

    if (tensor1%device_type() /= tensor2%device_type()) then
      write(*,*) "Error :: cannot multiply tensors with different device types"
      stop 1
    end if
    if (tensor1%device_index() /= tensor2%device_index()) then
      write(*,*) "Error :: cannot multiply tensors with different device indices"
      stop 1
    end if

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor1%rank(), tensor1%shape(), &
                              tensor1%dtype(), tensor1%device_type(), &
                              device_index=tensor1%device_index(), &
                              requires_grad=tensor1%requires_grad())
    end if
    call torch_tensor_multiply_c(output%p, tensor1%p, tensor2%p)
  end function torch_tensor_multiply

  !> Overloads division operator for two tensors.
  function torch_tensor_divide(tensor1, tensor2) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(in) :: tensor1  !! First tensor for the division
    type(torch_tensor), intent(in) :: tensor2  !! Second tensor for the division
    type(torch_tensor) :: output               !! Tensor to hold the quotient

    if (tensor1%device_type() /= tensor2%device_type()) then
      write(*,*) "Error :: cannot divide tensors with different device types"
      stop 1
    end if
    if (tensor1%device_index() /= tensor2%device_index()) then
      write(*,*) "Error :: cannot divide tensors with different device indices"
      stop 1
    end if

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor1%rank(), tensor1%shape(), &
                              tensor1%dtype(), tensor1%device_type(), &
                              device_index=tensor1%device_index(), &
                              requires_grad=tensor1%requires_grad())
    end if
    call torch_tensor_divide_c(output%p, tensor1%p, tensor2%p)
  end function torch_tensor_divide

  !> Overloads exponentiation operator for a tensor and a scalar of type `int8`
  function torch_tensor_power_int8(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : int8
    type(torch_tensor), intent(in) :: tensor                  !! Tensor to take the power of
    integer(int8), target, intent(in) :: power   !! Integer exponent
    type(torch_tensor) :: output                              !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_int_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_int")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_int_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_int_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_int8

  !> Overloads exponentiation operator for a tensor and a scalar of type `int16`
  function torch_tensor_power_int16(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : int16
    type(torch_tensor), intent(in) :: tensor                  !! Tensor to take the power of
    integer(int16), target, intent(in) :: power   !! Integer exponent
    type(torch_tensor) :: output                              !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_int_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_int")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_int_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_int_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_int16

  !> Overloads exponentiation operator for a tensor and a scalar of type `int32`
  function torch_tensor_power_int32(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : int32
    type(torch_tensor), intent(in) :: tensor                  !! Tensor to take the power of
    integer(int32), target, intent(in) :: power   !! Integer exponent
    type(torch_tensor) :: output                              !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_int_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_int")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_int_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_int_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_int32

  !> Overloads exponentiation operator for a tensor and a scalar of type `int64`
  function torch_tensor_power_int64(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : int64
    type(torch_tensor), intent(in) :: tensor                  !! Tensor to take the power of
    integer(int64), target, intent(in) :: power   !! Integer exponent
    type(torch_tensor) :: output                              !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_int_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_int")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_int_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_int_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_int64


  !> Overloads exponentiation operator for a tensor and a scalar of type `real32`
  function torch_tensor_power_real32(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : real32
    type(torch_tensor), intent(in) :: tensor                      !! Tensor to take the power of
    real(kind=real32), target, intent(in) :: power  !! Floating point exponent
    type(torch_tensor) :: output  !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_float_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_float")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_float_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_float_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_real32

  !> Overloads exponentiation operator for a tensor and a scalar of type `real64`
  function torch_tensor_power_real64(tensor, power) result(output)
    use, intrinsic :: iso_c_binding, only : c_associated, c_loc
    use, intrinsic :: iso_fortran_env, only : real64
    type(torch_tensor), intent(in) :: tensor                      !! Tensor to take the power of
    real(kind=real64), target, intent(in) :: power  !! Floating point exponent
    type(torch_tensor) :: output  !! Tensor to hold the exponentiation

    interface
      subroutine torch_tensor_power_float_c(output_c, tensor_c, power_c) &
          bind(c, name = "torch_tensor_power_float")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: power_c
      end subroutine torch_tensor_power_float_c
    end interface

    if (.not. c_associated(output%p)) then
      call torch_tensor_empty(output, tensor%rank(), tensor%shape(), tensor%dtype(), &
                              tensor%device_type(), device_index=tensor%device_index(), &
                              requires_grad=tensor%requires_grad())
    end if
    call torch_tensor_power_float_c(output%p, tensor%p, c_loc(power))
  end function torch_tensor_power_real64


  ! ============================================================================
  ! --- Other operators for computations involving tensors
  ! ============================================================================

  !> Overloads summation operator over the values in a tensor.
  subroutine torch_tensor_sum(output, tensor)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(inout) :: output  !! Tensor holding the summed values
    type(torch_tensor), intent(in)    :: tensor  !! Tensor to sum the values of

    interface
      subroutine torch_tensor_sum_c(output_c, tensor_c) &
          bind(c, name = "torch_tensor_sum")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_sum_c
    end interface

    if (.not. c_associated(output%p)) then
      write(*,*) "Error :: output tensor has not been constructed"
      stop 1
    end if
    call torch_tensor_sum_c(output%p, tensor%p)
  end subroutine torch_tensor_sum

  !> Overloads mean operator over the values in a tensor.
  subroutine torch_tensor_mean(output, tensor)
    use, intrinsic :: iso_c_binding, only : c_associated
    type(torch_tensor), intent(inout) :: output  !! Tensor holding the averaged values
    type(torch_tensor), intent(in)    :: tensor  !! Tensor to average the values of

    interface
      subroutine torch_tensor_mean_c(output_c, tensor_c) &
          bind(c, name = "torch_tensor_mean")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: output_c
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_mean_c
    end interface

    if (.not. c_associated(output%p)) then
      write(*,*) "Error :: output tensor has not been constructed"
      stop 1
    end if
    call torch_tensor_mean_c(output%p, tensor%p)
  end subroutine torch_tensor_mean

  ! ============================================================================
  ! --- Procedures related to automatic differentation functionality for tensors
  ! ============================================================================

  !> Resets a tensor's gradient to zero.
  subroutine torch_tensor_zero_grad(tensor)
    class(torch_tensor), intent(inout) :: tensor  !! Tensor to zero the gradient of

    interface
      subroutine torch_tensor_zero_grad_c(tensor_c) bind(c, name = "torch_tensor_zero_grad")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
      end subroutine torch_tensor_zero_grad_c
    end interface

    ! TODO: Call torch_tensor_get_gradient to check it exists?
    call torch_tensor_zero_grad_c(tensor%p)
  end subroutine torch_tensor_zero_grad

  !> Performs back-propagation on a Torch Tensor, given some external gradient.
  subroutine torch_tensor_backward_with_external_gradient(tensor, external_gradient, retain_graph)
    use, intrinsic :: iso_c_binding, only : c_bool
    type(torch_tensor), intent(in) :: tensor             !! Tensor to compute gradients of
    type(torch_tensor), intent(in) :: external_gradient
        !! External tensor used as an initial scaling of the gradient calculation
    logical, optional, intent(in)  :: retain_graph  !! Should the computational graph be retained?

    ! Local arguments
    logical(c_bool) :: retain_graph_value

    interface
      subroutine torch_tensor_backward_with_external_gradient_c(tensor_c, external_gradient_c, &
                                                                retain_graph_c) &
          bind(c, name = "torch_tensor_backward_with_external_gradient")
        use, intrinsic :: iso_c_binding, only : c_bool, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: external_gradient_c
        logical(c_bool), value, intent(in) :: retain_graph_c
      end subroutine torch_tensor_backward_with_external_gradient_c
    end interface

    ! Do not retain the graph by default
    if (present(retain_graph)) then
      retain_graph_value = retain_graph
    else
      retain_graph_value = .false.
    end if

    ! Call back-propagation with the provided external gradient
    call torch_tensor_backward_with_external_gradient_c(tensor%p, external_gradient%p, &
                                                        retain_graph_value)
  end subroutine torch_tensor_backward_with_external_gradient

  !> Performs back-propagation on a Torch Tensor, with an assumed external_gradient of ones.
  subroutine torch_tensor_backward_without_external_gradient(tensor, retain_graph)
    use, intrinsic :: iso_c_binding, only : c_bool
    type(torch_tensor), intent(in) :: tensor       !! Tensor to compute gradients of
    logical, optional, intent(in)  :: retain_graph !! Should the computational graph be retained?

    ! Local arguments
    logical(c_bool) :: retain_graph_value
    integer(int64) :: sizes(1)

    interface
      subroutine torch_tensor_backward_without_external_gradient_c(tensor_c, retain_graph_c) &
          bind(c, name = "torch_tensor_backward_without_external_gradient")
        use, intrinsic :: iso_c_binding, only : c_bool, c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        logical(c_bool), value, intent(in) :: retain_graph_c
      end subroutine torch_tensor_backward_without_external_gradient_c
    end interface

    if (tensor%rank() == 1) then
      ! Accept rank-1 tensors so long as they only have a single entry
      sizes(:) = tensor%shape()
      if (sizes(1) /= 1) then
        write(*,*) "Error :: external gradient can only be implicitly created for scalar fields"
        stop 1
      end if
    else if (tensor%rank() /= 0) then
      ! Disallow anything else except rank-0 tensors (i.e., 0-dim PyTorch scalars)
      write(*,*) "Error :: external gradient can only be implicitly created for scalar fields"
      stop 1
    end if

    ! Do not retain the graph by default
    if (present(retain_graph)) then
      retain_graph_value = retain_graph
    else
      retain_graph_value = .false.
    end if

    ! Call back-propagation
    call torch_tensor_backward_without_external_gradient_c(tensor%p, retain_graph_value)
  end subroutine torch_tensor_backward_without_external_gradient

  !> Retrieves the gradient with respect to a Torch Tensor.
  subroutine torch_tensor_get_gradient(gradient, tensor)
    type(torch_tensor), intent(inout) :: gradient  !! Tensor holding the gradient
    type(torch_tensor), intent(in) :: tensor       !! Tensor to compute the gradient with respect to

    interface
      subroutine torch_tensor_get_gradient_c(tensor_c, gradient_c) &
          bind(c, name = "torch_tensor_get_gradient")
        use, intrinsic :: iso_c_binding, only : c_ptr
        implicit none
        type(c_ptr), value, intent(in) :: tensor_c
        type(c_ptr), value, intent(in) :: gradient_c
      end subroutine torch_tensor_get_gradient_c
    end interface

    if (.not. c_associated(gradient%p)) then
      write(*,*) "Error :: tensors for holding gradients must be constructed before &
                 &retrieving values"
      stop 1
    end if
    call torch_tensor_get_gradient_c(tensor%p, gradient%p)
  end subroutine torch_tensor_get_gradient

end module ftorch_tensor