summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/math/fourier.tcl
diff options
context:
space:
mode:
Diffstat (limited to 'tcllib/modules/math/fourier.tcl')
-rwxr-xr-xtcllib/modules/math/fourier.tcl376
1 files changed, 376 insertions, 0 deletions
diff --git a/tcllib/modules/math/fourier.tcl b/tcllib/modules/math/fourier.tcl
new file mode 100755
index 0000000..bd455ad
--- /dev/null
+++ b/tcllib/modules/math/fourier.tcl
@@ -0,0 +1,376 @@
+# fourier.tcl --
+# Package for discrete (ordinary) and fast fourier transforms
+#
+# Author: Lars Hellstrom (...)
+#
+# The two top-level procedures defined are
+#
+# dft data-list
+# inverse_dft data-list
+#
+# which take a list of complex numbers and apply a Discrete Fourier
+# Transform (DFT) or its inverse respectively to these lists of numbers.
+# A "complex number" in this case is either (i) a pair (two element
+# list) of numbers, interpreted as the real and imaginary parts of the
+# complex number, or (ii) a single number, interpreted as the real
+# part of a complex number whose imaginary part is zero. The return
+# value is always in the first format. (The DFT generally produces
+# complex results even if the input is purely real.) Applying first
+# one and then the other of these procedures to a list of complex
+# numbers will (modulo rounding errors due to floating point
+# arithmetic) return the original list of numbers.
+#
+# If the input length N is a power of two then these procedures will
+# utilize the O(N log N) Fast Fourier Transform algorithm. If input
+# length is not a power of two then the DFT will instead be computed
+# using a the naive quadratic algorithm.
+#
+# Some examples:
+#
+# % dft {1 2 3 4}
+# {10 0.0} {-2.0 2.0} {-2 0.0} {-2.0 -2.0}
+# % inverse_dft {{10 0.0} {-2.0 2.0} {-2 0.0} {-2.0 -2.0}}
+# {1.0 0.0} {2.0 0.0} {3.0 0.0} {4.0 0.0}
+# % dft {1 2 3 4 5}
+# {15.0 0.0} {-2.5 3.44095480118} {-2.5 0.812299240582} {-2.5 -0.812299240582} {-2.5 -3.44095480118}
+# % inverse_dft {{15.0 0.0} {-2.5 3.44095480118} {-2.5 0.812299240582} {-2.5 -0.812299240582} {-2.5 -3.44095480118}}
+# {1.0 0.0} {2.0 8.881784197e-17} {3.0 4.4408920985e-17} {4.0 4.4408920985e-17} {5.0 -8.881784197e-17}
+ #
+# In the last case, the imaginary parts <1e-16 would have been zero in
+# exact arithmetic, but aren't here due to rounding errors.
+#
+# Internally, the procedures use a flat list format where every even
+# index element of a list is a real part and every odd index element is
+# an imaginary part. This is reflected in the variable names by Re_ and
+# Im_ prefixes.
+#
+
+namespace eval ::math::fourier {
+ #::math::constants pi
+
+ namespace export dft inverse_dft lowpass highpass
+}
+
+# dft --
+# Return the discrete fourier transform as a list of complex numbers
+#
+# Arguments:
+# in_data List of data (either real or complex)
+# Returns:
+# List of complex amplitudes for the Fourier components
+# Note:
+# The procedure uses an ordinary DFT if the number of data is
+# not a power of 2, otherwise it uses FFT.
+#
+proc ::math::fourier::dft {in_data} {
+ # First convert to internal format
+ set dataL [list]
+ set n 0
+ foreach datum $in_data {
+ if {[llength $datum] == 1} then {
+ lappend dataL $datum 0.0
+ } else {
+ lappend dataL [lindex $datum 0] [lindex $datum 1]
+ }
+ incr n
+ }
+
+ # Then compute a list of n'th roots of unity (explanation below)
+ set rootL [DFT_make_roots $n -1]
+
+ # Check if the input length is a power of two.
+ set p 1
+ while {$p < $n} {set p [expr {$p << 1}]}
+ # By construction, $p is a power of two. If $n==$p then $n is too.
+
+ # Finally compute the transform using Fast_DFT or Slow_DFT,
+ # and convert back to the input format.
+ set res [list]
+ foreach {Re Im} [
+ if {$p == $n} then {
+ Fast_DFT $dataL $rootL
+ } else {
+ Slow_DFT $dataL $rootL
+ }
+ ] {
+ lappend res [list $Re $Im]
+ }
+ return $res
+}
+
+# inverse_dft --
+# Invert the discrete fourier transform and return the restored data
+# as complex numbers
+#
+# Arguments:
+# in_data List of fourier coefficients (either real or complex)
+# Returns:
+# List of complex amplitudes for the Fourier components
+# Note:
+# The procedure uses an ordinary DFT if the number of data is
+# not a power of 2, otherwise it uses FFT.
+#
+proc ::math::fourier::inverse_dft {in_data} {
+ # First convert to internal format
+ set dataL [list]
+ set n 0
+ foreach datum $in_data {
+ if {[llength $datum] == 1} then {
+ lappend dataL $datum 0.0
+ } else {
+ lappend dataL [lindex $datum 0] [lindex $datum 1]
+ }
+ incr n
+ }
+
+ # Then compute a list of n'th roots of unity (explanation below)
+ set rootL [DFT_make_roots $n 1]
+
+ # Check if the input length is a power of two.
+ set p 1
+ while {$p < $n} {set p [expr {$p << 1}]}
+ # By construction, $p is a power of two. If $n==$p then $n is too.
+
+ # Finally compute the transform using Fast_DFT or Slow_DFT,
+ # divide by input data length to correct the amplitudes,
+ # and convert back to the input format.
+ set res [list]
+ foreach {Re Im} [
+ # $p is power of two. If $n==$p then $n is too.
+ if {$p == $n} then {
+ Fast_DFT $dataL $rootL
+ } else {
+ Slow_DFT $dataL $rootL
+ }
+ ] {
+ lappend res [list [expr {$Re/$n}] [expr {$Im/$n}]]
+ }
+ return $res
+}
+
+# DFT_make_roots --
+# Return a list of the complex roots of unity or of -1
+#
+# Arguments:
+# n Order of the roots
+# sign Whether to use 1 or -1 (for inverse transform)
+# Returns:
+# List of complex roots of unity or -1
+#
+proc ::math::fourier::DFT_make_roots {n sign} {
+ set res [list]
+ for {set k 0} {2*$k < $n} {incr k} {
+ set alpha [expr {2*3.1415926535897931*$sign*$k/$n}]
+ lappend res [expr {cos($alpha)}] [expr {sin($alpha)}]
+ }
+ return $res
+}
+
+# Fast_DFT --
+# Perform the fast Fourier transform
+#
+# Arguments:
+# dataL List of data
+# rootL Roots of unity or -1 to use in the transform
+# Returns:
+# List of complex numbers
+#
+proc ::math::fourier::Fast_DFT {dataL rootL} {
+ if {[llength $dataL] == 8} then {
+ foreach {Re_z0 Im_z0 Re_z1 Im_z1 Re_z2 Im_z2 Re_z3 Im_z3} $dataL {break}
+ if {[lindex $rootL 3] > 0} then {
+ return [list\
+ [expr {$Re_z0 + $Re_z1 + $Re_z2 + $Re_z3}] [expr {$Im_z0 + $Im_z1 + $Im_z2 + $Im_z3}]\
+ [expr {$Re_z0 - $Im_z1 - $Re_z2 + $Im_z3}] [expr {$Im_z0 + $Re_z1 - $Im_z2 - $Re_z3}]\
+ [expr {$Re_z0 - $Re_z1 + $Re_z2 - $Re_z3}] [expr {$Im_z0 - $Im_z1 + $Im_z2 - $Im_z3}]\
+ [expr {$Re_z0 + $Im_z1 - $Re_z2 - $Im_z3}] [expr {$Im_z0 - $Re_z1 - $Im_z2 + $Re_z3}]]
+ } else {
+ return [list\
+ [expr {$Re_z0 + $Re_z1 + $Re_z2 + $Re_z3}] [expr {$Im_z0 + $Im_z1 + $Im_z2 + $Im_z3}]\
+ [expr {$Re_z0 + $Im_z1 - $Re_z2 - $Im_z3}] [expr {$Im_z0 - $Re_z1 - $Im_z2 + $Re_z3}]\
+ [expr {$Re_z0 - $Re_z1 + $Re_z2 - $Re_z3}] [expr {$Im_z0 - $Im_z1 + $Im_z2 - $Im_z3}]\
+ [expr {$Re_z0 - $Im_z1 - $Re_z2 + $Im_z3}] [expr {$Im_z0 + $Re_z1 - $Im_z2 - $Re_z3}]]
+ }
+ } elseif {[llength $dataL] > 8} then {
+ set evenL [list]
+ set oddL [list]
+ foreach {Re_z0 Im_z0 Re_z1 Im_z1} $dataL {
+ lappend evenL $Re_z0 $Im_z0
+ lappend oddL $Re_z1 $Im_z1
+ }
+ set squarerootL [list]
+ foreach {Re_omega0 Im_omega0 Re_omega1 Im_omega1} $rootL {
+ lappend squarerootL $Re_omega0 $Im_omega0
+ }
+ set lowL [list]
+ set highL [list]
+ foreach\
+ {Re_y0 Im_y0} [Fast_DFT $evenL $squarerootL]\
+ {Re_y1 Im_y1} [Fast_DFT $oddL $squarerootL]\
+ {Re_omega Im_omega} $rootL {
+ set Re_y1t [expr {$Re_y1 * $Re_omega - $Im_y1 * $Im_omega}]
+ set Im_y1t [expr {$Im_y1 * $Re_omega + $Re_y1 * $Im_omega}]
+ lappend lowL [expr {$Re_y0 + $Re_y1t}] [expr {$Im_y0 + $Im_y1t}]
+ lappend highL [expr {$Re_y0 - $Re_y1t}] [expr {$Im_y0 - $Im_y1t}]
+ }
+ return [concat $lowL $highL]
+ } elseif {[llength $dataL] == 4} then {
+ foreach {Re_z0 Im_z0 Re_z1 Im_z1} $dataL {break}
+ return [list\
+ [expr {$Re_z0 + $Re_z1}] [expr {$Im_z0 + $Im_z1}]\
+ [expr {$Re_z0 - $Re_z1}] [expr {$Im_z0 - $Im_z1}]]
+ } else {
+ return $dataL
+ }
+}
+
+# Slow_DFT --
+# Perform the ordinary discrete (slow) Fourier transform
+#
+# Arguments:
+# dataL List of data
+# rootL Roots of unity or -1 to use in the transform
+# Returns:
+# List of complex numbers
+#
+proc ::math::fourier::Slow_DFT {dataL rootL} {
+ set n [expr {[llength $dataL] / 2}]
+
+ # The missing roots are computed by complex conjugating the given
+ # roots. If $n is even then -1 is also needed; it is inserted explicitly.
+ set k [llength $rootL]
+ if {$n % 2 == 0} then {
+ lappend rootL -1.0 0.0
+ }
+ for {incr k -2} {$k > 0} {incr k -2} {
+ lappend rootL [lindex $rootL $k]\
+ [expr {-[lindex $rootL [expr {$k+1}]]}]
+ }
+
+ # This is strictly following the naive formula.
+ # The product jk is kept as a separate counter variable.
+ set res [list]
+ for {set k 0} {$k < $n} {incr k} {
+ set Re_sum 0.0
+ set Im_sum 0.0
+ set jk 0
+ foreach {Re_z Im_z} $dataL {
+ set Re_omega [lindex $rootL [expr {2*$jk}]]
+ set Im_omega [lindex $rootL [expr {2*$jk+1}]]
+ set Re_sum [expr {$Re_sum +
+ $Re_z * $Re_omega - $Im_z * $Im_omega}]
+ set Im_sum [expr {$Im_sum +
+ $Im_z * $Re_omega + $Re_z * $Im_omega}]
+ incr jk $k
+ if {$jk >= $n} then {set jk [expr {$jk - $n}]}
+ }
+ lappend res $Re_sum $Im_sum
+ }
+ return $res
+}
+
+# lowpass --
+# Apply a low-pass filter to the Fourier transform
+#
+# Arguments:
+# cutoff Cut-off frequency
+# in_data Input transform (complex data)
+# Returns:
+# Filtered transform
+#
+proc ::math::fourier::lowpass {cutoff in_data} {
+ package require math::complexnumbers
+
+ set res [list]
+ set cutoff [list $cutoff 0.0]
+ set f 0.0
+ foreach a $in_data {
+ set an [::math::complexnumbers::/ $a \
+ [::math::complexnumbers::+ {1.0 0.0} \
+ [::math::complexnumbers::/ [list 0.0 $f] $cutoff]]]
+ lappend res $an
+ set f [expr {$f+1.0}]
+ }
+
+ return $res
+}
+
+# highpass --
+# Apply a high-pass filter to the Fourier transform
+#
+# Arguments:
+# cutoff Cut-off frequency
+# in_data Input transform (complex data)
+# Returns:
+# Filtered transform (high-pass)
+#
+proc ::math::fourier::highpass {cutoff in_data} {
+ package require math::complexnumbers
+
+ set res [list]
+ set cutoff [list $cutoff 0.0]
+ set f 0.0
+ foreach a $in_data {
+ set ff [::math::complexnumbers::/ [list 0.0 $f] $cutoff]
+ set an [::math::complexnumbers::/ $ff \
+ [::math::complexnumbers::+ {1.0 0.0} $ff]]
+ lappend res $an
+ set f [expr {$f+1.0}]
+ }
+
+ return $res
+}
+
+#
+# Announce the package
+#
+package provide math::fourier 1.0.2
+
+# test --
+#
+proc test_dft {points {real 0} {iterations 20}} {
+ set in_dataL [list]
+ for {set k 0} {$k < $points} {incr k} {
+ if {$real} then {
+ lappend in_dataL [expr {2*rand()-1}]
+ } else {
+ lappend in_dataL [list [expr {2*rand()-1}] [expr {2*rand()-1}]]
+ }
+ }
+ set time1 [time {
+ set conv_dataL [::math::fourier::dft $in_dataL]
+ } $iterations]
+ set time2 [time {
+ set out_dataL [::math::fourier::inverse_dft $conv_dataL]
+ } $iterations]
+ set err 0.0
+ foreach iz $in_dataL oz $out_dataL {
+ if {$real} then {
+ foreach {o1 o2} $oz {break}
+ set err [expr {$err + ($i-$o1)*($i-$o1) + $o2*$o2}]
+ } else {
+ foreach i $iz o $oz {
+ set err [expr {$err + ($i-$o)*($i-$o)}]
+ }
+ }
+ }
+ return [format "Forward: %s\nInverse: %s\nAverage error: %g"\
+ $time1 $time2 [expr {sqrt($err/$points)}]]
+}
+
+# Note:
+# Add simple filters
+
+if { 0 } {
+puts [::math::fourier::dft {1 2 3 4}]
+puts [::math::fourier::inverse_dft {{10 0.0} {-2.0 2.0} {-2 0.0} {-2.0 -2.0}}]
+puts [::math::fourier::dft {1 2 3 4 5}]
+puts [::math::fourier::inverse_dft {{15.0 0.0} {-2.5 3.44095480118} {-2.5 0.812299240582} {-2.5 -0.812299240582} {-2.5 -3.44095480118}}]
+puts [test_dft 10]
+puts [test_dft 16]
+puts [test_dft 100]
+puts [test_dft 128]
+
+puts [::math::fourier::dft {1 2 3 4}]
+puts [::math::fourier::lowpass 1.5 [::math::fourier::dft {1 2 3 4}]]
+}