lib/fft.ex

defmodule Chi2fit.FFT do

  # Copyright 2016-2017 Pieter Rijken
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.

  @moduledoc """
  Provides Fast Fourier Transform.
  """

  require Integer

  import Kernel, except: [*: 2, /: 2,+: 2, -: 2]

  @typedoc "A real number."
  @opaque real :: number
  
  @typedoc "A complex number with a real part and an imaginary part."
  @opaque complex :: {real,real}
  
  defp {x1,x2} * {y1,y2}, do: {x1*y1-x2*y2,x1*y2+x2*y1}
  defp x * {y1,y2}, do: {x*y1,x*y2}
  defp x * y, do: Kernel.*(x,y)

  defp {x1,x2} / y, do: {x1/y,x2/y}
  defp x / y, do: Kernel./(x,y)

  defp {x1,x2} + {y1,y2}, do: {x1+y1,x2+y2}
  defp x + {y1,y2}, do: {x+y1,y2}
  defp {x1,x2} + y, do: {x1+y,x2}
  defp x + y, do: Kernel.+(x,y)

  defp {x1,x2} - {y1,y2}, do: {x1-y1,x2-y2}
  defp x - {y1,y2}, do: {x-y1,-y2}
  defp x - y, do: Kernel.-(x,y)

  @doc """
  Calculates the discrete Fast Fourier Transform of a list of numbers.
  
  Provides a parallel version (see options below). See [1] for details of the algorithm implemented.
  
  ## Options
      `:phase` - Correction factor to use in the weights of the FFT algorithm. Defaults to 1.
      `:nproc` - Parellel version. Number of processes to use. See [2]. Defaults to 1.

  ## References
  
      [1] Zie: https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm
      [2] Parallel version of FFT; see http://www.webabode.com/articles/Parallel%20FFT%20implementations.pdf
  
  ## Examples
    
      iex> fft [4]
      [{4.0, 0.0}]
    
      iex> fft [1,2,3,4,5,6]
      [{21.0, 0.0}, {-3.0000000000000053, 5.19615242270663},
                {-3.0000000000000036, 1.7320508075688736}, {-3.0, 0.0},
                {-2.9999999999999982, -1.7320508075688799},
                {-2.999999999999991, -5.196152422706634}]

  """
  @spec fft([real],opts :: Keyword.t) :: [complex]
  def fft(list,opts \\ [])

  def fft([],_opts), do: []
  def fft([x,y],opts) do
    fac = opts[:phase] || 1
    [x*weight(fac*0,0,2)+y*weight(fac*0,1,2),x*weight(fac*1,0,2)+y*weight(fac*1,1,2)]
  end
  def fft(list=[_|_],opts) do
    fac = opts[:phase] || 1
    nproc = opts[:nproc] || 1

    nn = length(list)
    cond do
      Integer.is_even(length(list)) ->
        zipped = cond do
          nproc == 2 or nproc == 4 ->
            list
            |> split_evenodds
            |> Enum.map(fn x-> Task.async(fn -> fft(x,Keyword.merge(opts,[nproc: nproc/2])) end) end)
            |> Task.yield_many(3_600_000)
            |> Enum.map(fn ({_task,{:ok,result}})->result end)
            |> (&(apply(fn x,y->Stream.zip(x,y) end,&1))).()
        
          nproc == 1 ->
            list
            |> split_evenodds
            |> Enum.map(fn arg->fft(arg,opts) end)
            |> (&(apply(fn x,y->Stream.zip(x,y) end,&1))).()
        end

        n = nn/2
        zipped
        |> Stream.concat(zipped)
        |> Stream.with_index(0)
        |> Stream.map(
          fn
            ({{x,y},m}) when m<n -> x + (weight(fac*1,m,2*n)*y)
            ({{x,y},m}) when m>=n -> x - (weight(fac*1,m-n,2*n)*y)
          end)
        |> Enum.to_list
        
      true ->
        0..nn-1 |> Enum.map(
          fn m ->
            list |> Stream.with_index(0) |> Stream.map(fn ({item,k})-> item*weight(fac*m,k,nn) end) |> Enum.reduce(0,fn (x,acc)->x+acc end)
          end)
    end
  end
  
  @doc """
  Calculates the inverse FFT.
  
  For available options see `fft/2`.

  ## Examples
  
      iex> ifft [4.0]
      [{4.0, 0.0}]
  
      iex> ifft [1.0,2.0,3.0]
      [{2.0, 0.0}, {-0.5000000000000003, -0.2886751345948125},
                  {-0.4999999999999995, 0.28867513459481353}]

      iex> [1.0,5.0] |> fft |> ifft
      [{1.0, -3.061616997868383e-16}, {5.0, 6.123233995736767e-17}]

  """
  @spec ifft([real],Keyword.t) :: [complex]
  def ifft(list,opts \\ [nproc: 1]) do
    n = length(list)
    list |> fft(Keyword.merge(opts,[phase: -1])) |> Enum.map(&(&1/n))
  end

  @doc """
  Calculates the norm of a complex number or list of complex numbers.
  
  ## Examples
  
      iex> normv []
      []
      
      iex> normv {2,3}
      13
      
      iex> normv [{2,3},{1,2}]
      [13,5]

  """
  @spec normv([complex]|complex) :: real
  def normv({x,y}), do: x*x+y*y
  def normv(list) when is_list(list), do: list |> Enum.map(&normv/1)

  defp weight(r,m,n), do: weight(r*m,n)
  defp weight(rm,n), do: weight(rm/n)
  defp weight(x), do: {:math.cos(2*:math.pi()*x),-:math.sin(2*:math.pi()*x)}

  defp split_evenodds(list) when Integer.is_even(length(list)) do
    list
    |> List.foldr({[[],[]],false},
      fn
        (item,{[e,o],true}) -> {[[item|e],o],false}
        (item,{[e,o],false}) -> {[e,[item|o]],true}
      end)
    |> elem(0)
  end

end