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 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