Real view on complex array using c_pointer

First of all: Thank you for a great forum!

I want to use a FFT routine which takes a real array as input and performs an in-place transform. The real array can be seen as a real representation of a complex array as [real(x(1)), aimag(1),real(x(2)),…]. In the calling procedure I would really like to work with a complex array that I pass to the FFT routine. I can think of three ways to achieve this:

  1. Call the FFT routine without interface checking with complex actual argument and real dummy argument.
  2. Copy data from complex array to real array before calling
  3. Creating a “real view” into the complex array using a C pointer.

I would like to try out approach 3. Below is a module with a real_view subroutine that creates a real pointer to the complex array. Because Fortran does not allow pointers with a different type than its target I have to use a “back door” via C pointers.

Is this a good idea? Any pitfalls? Will there be a memory leakage?

module real_view_on_complex
    implicit none
    integer, parameter :: wp = kind(0.d0)
    
contains

    subroutine real_view(c, r)
        use ISO_C_binding, only: c_f_pointer, c_loc
        complex(wp), intent(in), target :: c(:)
        real(wp), intent(inout), pointer :: r(:)
    
        call c_f_pointer(c_loc(c), r, shape=[2*size(c)])

    end subroutine real_view
    
end module real_view_on_complex


program test_real_view
    use real_view_on_complex, only : real_view, wp
    implicit none

    complex(wp), allocatable :: c(:)
    real(wp), pointer :: r(:) => null()
    
    ! Create complex 2-element array
    allocate(c(2))
    c(1) = cmplx(1_wp, 2_wp, wp)
    c(2) = cmplx(3_wp, 4_wp, wp)

    ! Create a 4-element real "view" into the same memeory area
    call real_view(c, r)

    ! Print both views
    print*,"complex view:"
    print*,c
    print*,"real view:"
    print*,r

    ! Modify complex number and print
    c(1) = cmplx(1.1_wp, 2.1_wp, wp)

    print*,"Modify:"
    print*,"complex view after modification:"
    print*,c
    print*,"real view after modification:"
    print*,r


end program test_real_view
2 Likes

Can you use the Fortran 2008 syntax c%re to get a real view of c? More information here.

1 Like

I don’t think this is what the OP asks for. The c%re is just a view of the real component of the array of complex values.

The “real view” is supposed to be a pointer to the original complex memory with type real. The idea is more along the lines of the following snippet:

complex(dp), target :: c(5)
real(dp), pointer :: r(:) => null()

r(1:2*size(c)) => c   ! Error: different types in pointer assignment 

With respect to point 1. you could still define an interface in your calling program:

$ cat print_complex_values.f90
subroutine print_complex_values(n,c)
  implicit none
  integer, intent(in) :: n
  real(kind(1.0d0)) :: c(2*n)
  integer :: i
  do i = 1, n
    print *, "re = ", c(2*i-1), ", im = ", c(2*i)
  end do
end subroutine
$ cat fft_interface.f90
program fft_interface

  implicit none

  integer, parameter :: wp = kind(1.0d0)
  integer, parameter :: n = 3
  complex(wp) :: c(n)

  interface
    subroutine print_complex_values(n,c)
      import wp
      integer, intent(in) :: n
      complex(wp), intent(in) :: c(n)  ! argument-mismatch
    end subroutine
  end interface

  call random_number(c%re)
  call random_number(c%im)

  print *, c

  call print_complex_values(n,c)

end program

If you put the subroutine and main program in the same source file, the compiler raises an argument mismatch warning. But if they are in separate files, I have no problem to compile this:

$ gfortran -Wall print_complex_values.f90 fft_interface.f90 -o fft_interface
$ ./fft_interface
             (0.88224078191975730,0.37246157692415127)             (0.87187689541179403,0.65375024314802599)             (0.53851115396661997,0.37454896798923121)
 re =   0.88224078191975730      , im =   0.37246157692415127
 re =   0.87187689541179403      , im =   0.65375024314802599
 re =   0.53851115396661997      , im =   0.37454896798923121

I don’t think this a standard-conforming solution however.

Edit 1: the best solution would be to modify the original FFT routine to have the desired interface (if implemented in Fortran of course). If the FFT routine is actually a C procedure that expects a double * array, then I think solution 3 passing c_loc(c) would be the way to go (without the extra layer of casting to a Fortran real array).

Edit 2: perhaps someone can shed some light on why complex designators (%re, %im) can’t be used in pointer assignment statements as shown below:

  real(wp), target :: r(2*n)
  complex(wp), pointer :: c_p(:) => null()

  c_p%re => r(1::2)   ! Error: Non-POINTER in pointer association context
  c_p%im => r(2::2)   ! Error: Non-POINTER in pointer association context

One of the problems I see is that this could only make sense when array r has an even number of elements.

Edit 3: A real view into part of a complex array is also not allowed apparently…

$ cat real_view_into_complex.f90
implicit none
integer, parameter :: wp = kind(1.0d0)
integer, parameter :: n = 3
complex(wp), target :: c(n)
real(wp), pointer :: re(:) => null(), im(:) => null()

call random_number(c%re)
call random_number(c%im)

re => c%re
im => c%im
end
$ gfortran real_view_into_complex.f90
real_view_into_complex.f90:11:0:

   11 | re => c%re
      |
internal compiler error: in gfc_get_dataptr_offset, at fortran/trans-array.c:6949
Please submit a full bug report,
with preprocessed source if appropriate.
See <file:///usr/share/doc/gcc-9/README.Bugs> for instructions.
2 Likes

I believe this is conforming. You should include a test in real_view that IS_CONTIGUOUS is true for the target and also make sure that the target does not get deallocated from under you. I have no idea if your FFT will come out as you expect!

2 Likes

With option 3, the standard conformance of the code you show requires the actual argument (c) to have the TARGET attribute as well. This is something the calling programs often forget to include with their variables. Without the TARGET, the association status of r with c is undefined upon completion of real_view subprogram.

I would be inclined instead to have the dummy argument to have the POINTER attribute and INTENT(IN) which will then require the actual argument to either have the TARGET or the POINTER attributes. This will be better on the client side and perhaps provide added flexibility there. It will then be better to include if ( associated(c) ) check in the real_view routine.

3 Likes

Thats a good point. Making c rank 2 and sending in a row like this, real_view(c(1,:), r), gives me the wrong result!

I want to make an interface to Ooura FFT routines, which come in both Fortran and C implementations. These routines should be reasonably fast, easy to integrate and have a permissive license. For the complex distrete Fourier transform (complex-to-complex) I can in principle do as you suggest - modify the original code. But there is also real discrete Fourier transform (real-to-complex and inversely) which are also in-place transforms. Then it will make sense to have both a real and complex pointer to the same data, although it will be a bit confusing…
Perhaps I can make two variants of the interface 1) Functions called as y = rfft(x, plan) and x = irfft(y, plan) that makes a copy, leaves the input untouched and avoids pointers. And alternative 2) in-place subroutines with pointers: call rfft_inplace(x, y, plan) and call irfft_inplace(x, y, plan). plan is an object of a type that contains data re-used between transforms of the same size. This type can even be extended from a abstract type, meaning that I can put different fft implementations behind a common interface.

Thanks for good advice!

Do you mean like this?

    subroutine real_view(cp, rp, ierr)
        use ISO_C_binding, only: c_f_pointer, c_loc
        complex(wp), intent(in), pointer :: cp(:)
        real(wp), intent(inout), pointer :: rp(:)
        integer, intent(out) :: ierr
        
        if (.not. is_contiguous(cp)) then
            ierr = -1
            return
        end if
        if (.not. associated(cp)) then
            ierr = -2
            return
        end if

        call c_f_pointer(c_loc(cp), rp, shape=[2*size(cp)])

    end subroutine real_view

called like this?

    complex(wp), target, allocatable :: c(:)
    complex(wp), pointer :: cp(:) => null()
    real(wp), pointer :: rp(:) => null()
    integer :: ierr
    
    ! Create complex 2-element array
    allocate(c(2))
    c(1) = cmplx(1_wp, 2_wp, wp)
    c(2) = cmplx(3_wp, 4_wp, wp)

    ! Create a 4-element real "view" into the same memory area
    cp => c
    call real_view(cp, rp, ierr)

Just a minor nitpick, I think ierr should have a specified output value (say 0) for the case of no error.

1 Like

@oyvind ,

The use of an error code is entirely up to you and based on your (and your team’s) adopted programming practice(s). You can also consider INTENT(OUT) for the second dummy argument toward the real pointer that can just clear up the usage.

Putting that aside, my suggestion will be to strive for as much compilation time diagnostics as possible which you can get here by using the CONTINUOUS attribute.

   subroutine real_view(cp, rp)
      use ISO_C_binding, only: c_f_pointer, c_loc
      complex(wp), contiguous, pointer, intent(in) :: cp(:)
      real(wp), intent(out), pointer :: rp(:)
      
      if ( associated(cp) ) then
         call c_f_pointer(c_loc(cp), rp, shape=[2*size(cp)])
      end if

   end subroutine real_view

Now consider the following:

   subroutine sub( pc )
      complex(wp), pointer :: pc(:)
      real(wp), pointer :: r(:)
      ! Elided is the explicit interface
      call real_view( pc, r )   
   end subroutine 

C:\Temp>gfortran -c c.f90
c.f90:19:22:

19 | call real_view( pc, r )
| 1
Error: Actual argument to contiguous pointer dummy ‘cp’ at (1) must be simply contiguous

Also, you do not need a temporary pointer object on the caller side, the Fortran standard now permits automatic targetting so you can do

   complex(wp), target, allocatable :: c(:)
   real(wp), pointer :: rp(:) => null() !<-- Be mindful of this initialization!
   
   ! Create complex 2-element array
   allocate(c(2))
   c(1) = cmplx(1_wp, 2_wp, wp)
   c(2) = cmplx(3_wp, 4_wp, wp)

   ! Create a 4-element real "view" into the same memory area
   call real_view(c, rp)

By the way, be mindful the initialization of the real pointer in the declaration real(wp), pointer :: rp(:) => null() will impart the SAVE attribute to that rp object, something you may not want!

2 Likes

If the complex array c is deallocated elsewhere in the program, would good programming practice dictate nullifying the real pointer?

That’s a good thought.

When it comes to working with data objects with the POINTER attribute, it’s much safer of course to follow some programming practices such as

  1. Try to avoid “entities” in MODULEs or components with PUBLIC attribute in derived types or variables in the main program from having the POINTER attribute This then mostly leaves only local variables in subprograms or private components of derived types as the instances where such an attribute may get used,
  2. Try to avoid an object with a POINTER attribute from appearing in an ALLOCATE statement, thereby employing the data pointers only as aliases to named data targets,
  3. As mentioned above, have a program workflow such that the data pointer gets explicitly nullified as soon as its need gets exhausted,
3 Likes

Below is my wrapper for the ooura real discrete Fourier transform. The data to be transformed, including real and complex pointers to the same data, are components of rfft_type from which rfft_ooura_type extends. According to gfortran I am not allowed to have allocatable targets as type components so I had to allocate the cview pointer (against FortranFan’s advice…). Note that size(cview) equals size(rview)/2+1. The reason for the extra element is that the imaginary part of the zero and Nyquist frequencies are always zero. The rdft routine of Ooura puts the real part of the Nyquist component in the position of the imaginary part of the zero frequency. My wrapper moves the Nyquist component to the end where it belongs, thereby behaving like the rfft of Python-numpy.

I could have made rview and cview private components with getter and setter methods, but that will probably lead to copying in and out(?) which is what I want to avoid in the first place…

    subroutine test_rfft_ooura()

        type(rfft_ooura_type) :: fobj
        integer nt, i, ierr

        nt = 4

        call fobj%initialize(nt, ierr)

        ! Create time series
        fobj%rview = [ (real(i, wp) , i=1,nt)]
        print*,'Time series with size ',size(fobj%rview)
        print*,'rview: ',fobj%rview

        ! Transform to frequency domain
        call fobj%rfft()
        print*,'Fourier transform with size ',size(fobj%cview)
        print*,'cview: ',fobj%cview

        ! Transform back to time domain
        call fobj%irfft()
        print*,'Time series (recreated) '
        print*,'rview: ',fobj%rview

        call fobj%destroy()
    
    end subroutine test_rfft_ooura

Output

 Time series with size            4
 rview:    1.0000000000000000        2.0000000000000000        3.0000000000000000        4.0000000000000000
 Fourier transform with size            3
 cview:                (10.000000000000000,0.0000000000000000)              (-2.0000000000000000,2.0000000000000000)              (-2.0000000000000000,0.0000000000000000)
 Time series (recreated)
 rview:    1.0000000000000000        2.0000000000000000        3.0000000000000000        4.0000000000000000

module rfft_ooura
    use fftsg_modified_mod, only : rdft
    use rfft, only : rfft_type
    implicit none

    private
    public :: rfft_ooura_type , initialize, destroy, is_initialized, rfft, irfft

    integer, parameter :: wp=kind(0.d0) !Move this to subroutines

    type, extends(rfft_type) :: rfft_ooura_type
        private
        logical :: initialized = .false.
        integer :: nr, nc
        integer,  allocatable :: ip(:)
        real(wp), allocatable :: cstab(:)
        ! From parent: complex(wp), public, pointer, contiguous :: cview(:)
        ! From parent: real(wp),    public, pointer :: rview(:)
    contains
        procedure :: initialize, destroy, is_initialized, rfft, irfft
    end type rfft_ooura_type

    contains

        subroutine initialize(this, fftsize, ierr)
            use ISO_C_binding, only: c_f_pointer, c_loc
            use fft_utils_mod, only: next_power_of_2
            class(rfft_ooura_type), intent(inout) :: this
            integer, intent(in) :: fftsize
            integer, intent(out) :: ierr

            if (fftsize /= next_power_of_2(fftsize)) then
                ierr = -1
                return
            else
                ierr = 0
            end if

            ! Set status
            this%initialized = .true.
            this%nr = fftsize
            this%nc = fftsize/2 + 1
            
            ! Allocate work arrays
            allocate(this%ip(2+2**(int(log(fftsize/2+0.5)/log(2.0))/2)))
            allocate(this%cstab(fftsize/2))
            this%ip(1) = 0 !Initialization flag
            
            ! Allocate data (to be transformed) with both a real and a complex "view"
            ! (rview and cview is pointers to the same data)
            allocate(this%cview(this%nc))
            call c_f_pointer(c_loc(this%cview), this%rview, shape=[fftsize])
            this%cview = 0

        end subroutine

        !*****************************************************************************

        subroutine destroy(this)

            class(rfft_ooura_type), intent(inout) :: this

            this%initialized = .false.
            deallocate(this%ip, this%cstab, this%cview)
            nullify(this%cview, this%rview)

        end subroutine

        !*****************************************************************************

        function is_initialized(this)
            class(rfft_ooura_type), intent(in) :: this
            logical :: is_initialized
            is_initialized = this%initialized
        end function

        !*****************************************************************************

        subroutine rfft(this)
            class(rfft_ooura_type), intent(inout) :: this

            call rdft(this%nr, 1, this%rview, this%ip, this%cstab)
            ! Move the Nyquist component where it should be:
            this%cview(this%nc)%re = this%cview(1)%im
            this%cview(this%nc)%im = 0
            this%cview(1)%im = 0
            ! Flip sign of imaginary part
            this%cview(2:(this%nc-1))%im = -this%cview(2:(this%nc-1))%im

        end subroutine

        !*****************************************************************************

        subroutine irfft(this)
            class(rfft_ooura_type), intent(inout) :: this
            
            ! Flip sign of imaginary part
            this%cview(2:(this%nc-1))%im = -this%cview(2:(this%nc-1))%im
            ! Move the Nyquist component where rdft expects it to be:
            this%cview(1)%im = this%cview(this%nc)%re
            call rdft(this%nr, -1, this%rview, this%ip, this%cstab)
            this%rview = this%rview*(2.0_wp/this%nr) !scaling
            this%cview(this%nc) = 0 ! Set extra element to zero by definition
        end subroutine

        !*****************************************************************************

end module rfft_ooura

Here is the abstract type:

module rfft
    implicit none

    private
    public :: rfft_type

    integer, parameter :: wp=kind(0.d0) !Move this to subroutines

    type, abstract :: rfft_type
        complex(wp), public, pointer, contiguous :: cview(:)
        real(wp),    public, pointer :: rview(:)
    contains
        private
        ! Initialization may vary between subtypes and is therefore not here
        procedure(destroy_abstract), deferred :: destroy
        procedure(is_initialized_abstract), deferred :: is_initialized
        procedure(rfft_abstract), deferred :: rfft
        procedure(irfft_abstract), deferred :: irfft
    end type rfft_type

    interface
        subroutine destroy_abstract(this)
            import :: rfft_type
            class(rfft_type), intent(inout) :: this
        end subroutine
    end interface
    interface
        function is_initialized_abstract(this) result(is_initialized)
            import :: rfft_type
            class(rfft_type), intent(in) :: this
            logical :: is_initialized
        end function
    end interface
    interface
        subroutine rfft_abstract(this)
            import :: rfft_type
            class(rfft_type), intent(inout) :: this
        end subroutine
    end interface
    interface
        subroutine irfft_abstract(this)
            import :: rfft_type
            class(rfft_type), intent(inout) :: this
        end subroutine
    end interface

end module rfft

@oyvind , see the comments by @certik here and here.

Unless you are completely enamored with object-oriented programming (OOP) approach, a somewhat better option will be to follow object-based model as suggested above. Note there is never complete safety but with object-based you can gain some simplicity here: you can hone in on a single data allocation and avoid data duplication and in addition, a lot of the OOP data (and state) management code can be avoided.

Here’s a simple illustration, you can try it with the free Intel oneAPI IFORT compiler.

With this, you can get most of the safety in working with pointers.

module c_m
   use, intrinsic :: iso_c_binding, only : c_loc, c_f_pointer
   private
   type, public :: c_t
      private
      complex, allocatable :: m_c(:)
      complex, pointer :: m_cview(:) => null()
      real, pointer :: m_rview(:) => null()
   end type
   public :: init_c, rview, cview 
contains
   subroutine init_c( this, size_c )
      type(c_t), intent(inout), target :: this
      integer, intent(in)              :: size_c
      ! Elided are checks toward initialization and allocation status, etc.
      allocate( this%m_c(size_c) )
      this%m_cview => this%m_c 
      call c_f_pointer( c_loc(this%m_c), this%m_rview, shape=[ 2*size_c ] )
      this%m_rview = 0.0
   end subroutine 
   function rview( this ) result(r)
      type(c_t), intent(in), pointer :: this
      real, pointer :: r(:)
      r => this%m_rview
   end function 
   function cview( this ) result(c)
      type(c_t), intent(in), pointer :: this
      complex, pointer :: c(:)
      c => this%m_cview
   end function 
end module 
   use c_m
   type(c_t), target :: foo
   integer, parameter :: rsize=4
   call init_c( foo, size_c=rsize/2 )
   rview( foo ) = [( real(i), integer :: i = 1, rsize )]  !<-- Fortran 2008 feature, inadequately supported by gfortran
   print *, "cview() = ", cview( foo ) 
   print *, "rview() = ", rview( foo ) 
end

C:\Temp>ifort /standard-semantics c.f90
Intel(R) Fortran Intel(R) 64 Compiler Classic for applications running on Intel(R) 64, Version 2021.2.0 Build 20210228_000000
Copyright (C) 1985-2021 Intel Corporation. All rights reserved.

Microsoft (R) Incremental Linker Version 14.28.29337.0
Copyright (C) Microsoft Corporation. All rights reserved.

-out:c.exe
-subsystem:console
c.obj

C:\Temp>c.exe
cview() = (1.000000,2.000000) (3.000000,4.000000)
rview() = 1.000000 2.000000 3.000000 4.000000

C:\Temp>

2 Likes

Thanks for good advice! I didn’t know that I could have pointers pointing to members (without target attribute) if the type have the target attribute. Does having cview as an allocatable mean that I can drop the destroy() routine? I guess all allocatable members will be automatically deallocated then the object goes out of scope.

I agree that OOP and inheritance can overcomplicate things… What I want to achieve here is to have a common interface to several fft backends. Only the initialization (object creation) of the different backends should differ, the rest of the code should not have to be changed if we implement a new backend. In this case a new object will only need to be created when the transform size change. The same object (with its internal work arrays) can be reused between transforms of the same size. So typically the initialization is in one place whereas rfft and irfft is used many places (most often reusing the same object) so avoiding if then else in all these places makes things cleaner. But the price to pay for a clean outside is a more complicated inside…