Visualizing the Fourier Syntheisis
Link To GitHub
Introduction
The goal of the project is to generate Visualizations for how the Fourier series creates approximation of curves. The end result is a function that is able to generate two different graphics to show using Circles to represent the trigonometric waves used in the Fourier Series.The Fourier Series in Calculus is the summation of trigonometric waves in order to represent an approximation of a curve. Since the Fourier Series uses trigonometric functions which are created by the rotation of a circle, it is possible to link together multiple rotating circles and recreate the curve.
Development
Understanding the General Idea
First,I had to learn Calculus and trigonometry in order to understand how the Fourier Series works. At first I wanted a proof of concept that I was able to visualize how the addition of sinusoidal could result in an approximation of the curve. So I spent some time to hard code rotating radii of circles and orbiting circles. I created orbiting circles by setting the centers of successive circles as a sinusoidal functions so that they would be following a track built by the previous circle's circumference.
Animate[
Graphics[
{
Line[{{0, 0}, {5*Cos[time], 5*Sin[time]}}],
Circle[{0, 0}, 5],
Circle[{5*Cos[time], 5*Sin[time]}, 3],
Line[{{5*Cos[time], 5*Sin[time]}, {3*Cos[2 time] + 5*Cos[time], 3*Sin[2 time] + 5*Sin[time]}}],
Circle[{3*Cos[2 time] + 5*Cos[time], 3*Sin[2 time] + 5*Sin[time]},2],
Line[{{3*Cos[2 time] + 5*Cos[time], 3*Sin[2 time] + 5*Sin[time]}, {5*Cos[time] + 3*Cos[2 time] + 2 Cos[3 time], 5*Sin[time] + 3*Sin[2 time] + 2 Sin[3 time]}}],
}
],
{time, 0, 2*Pi}, AnimationRunning -> False]
The lines are the rotating arms and connect one center of a circle to the next circle's center. The code above shows the nesting of coordinates to create the moving parts using the "Animate" function. If the x-coordinates use "Cosine" and the y-coordinates use "Sine" functions, then the rotating parts will move counter-clockwise.If you switch the functions, then the arms rotate clockwise. (This information proves to be useful later on in the developing process)
Creating the Functions
I went through many iterations of my code an restarted on a new approach three times. The hardest part of this project was getting the information form the functions to be in a certain format so that it could be displayed in a cohesive manner. This issue rose form creating a generalized function that would work for most if not all scenarios.
Approach One
At first I tried to create multiple helper functions to manipulate the information and create it modular. The code below takes in a radius list, frequency list, and the number of circles needed, and then creates the nested list that is used for the moving objects in the graphics. The function is then called multiple times (I at that time had not optimized by code to run quickly) and used them to create list of Graphics Primitives. This one of the more developed versions of this method where the user physically inputs the amplitude and radii of the waves used to generate the desired function. In earlier versions I had an issue with getting animate to work; when I was not using slots in order to input lists and other information. I later found out that the function "Animate" had a "HoldAll" attribute which meant that it would not evaluate anything that is inputted directly in the Animate. Therefore, I had to adapt my code to evaluate outside the Animate and then be inserted into the Animate function via Slot Machines.
ClearAll[fourierSeriesGraphicsV3]
fourierSeriesCoordinates[numberCircles_,radiusList_List,frequencyList_List]:=
With[
{protoList=MapThread[
{#1*Cos[#2*time],#1*Sin[#2*time]}&,{radiusList,frequencyList}]
},
Plus@@@Table[protoList[[;;n]],
{n,1,Length[protoList]}]//Prepend[{0,0}]
]
fourierSeriesGraphicsV3[numberCircles_,radiusList_List,frequencyList_List]:=
With[
{
coordinateList=fourierSeriesCoordinates[numberCircles,radiusList,frequencyList],
protoList=fourierSeriesCoordinates[numberCircles,radiusList,frequencyList],
lastCoordinate=Drop[Apply[List,
MapThread[
Circle,{
Drop[
fourierSeriesCoordinates[10,Reverse[Range[10]],Range[10]],-1],
Reverse[Range[10]]}
]//Last],-1][[1,2]],
circleList=Evaluate[
MapThread[
Circle,{
Drop[fourierSeriesCoordinates[numberCircles,radiusList,frequencyList],-1],
radiusList}
]
]
},
Animate[
Graphics[{#1,#2,Line[Last[coordinateList],{Total[radiusList]+10,lastCoordinate}]}],
{time,0,2*Pi},
AnimationRunning->False]&@Evaluate[
(Line[coordinateList]),(circleList)
]
]
Approach Two
Although the previous method works, it is hard to know exactly if you are creating the desired function,Which means that you need to know what the radii/amplitudes and frequency where before you used this function. Hence my second approach. This approach was more of a function that would be built as an addition to the previous method. The new addition included a function built around the inbuilt function "FourierSeries" that took in function, variable, and number of terms as inputs and returned the equation of the approximation of the function as complex numbers.I had to use the inbuilt function "ComplexExpand" to get the equation in terms of Sine and Cosine. If the input function was even, then the Sine terms would cancel, and if the function is odd, then the Cosine terms canceled out.
fourierInputV2[inputFunction_,variableUsing_,numberOfTerms_Integer]:=
With[
{protoSeries=FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand,
manipulateSeries=Drop[Apply[List,FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand],1],
protoCoordinateSeries={
{
Select[Drop[Apply[List,FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand],1],!FreeQ[#,Cos]&],
Select[Drop[Apply[List,FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand],1],!FreeQ[#,Cos]&]/.Cos->Sin
},
{
Select[Drop[Apply[List,FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand],1],!FreeQ[#,Sin]&],
Select[Drop[Apply[List,FourierSeries[inputFunction,variableUsing,numberOfTerms]//ComplexExpand],1],!FreeQ[#,Sin]&]/.Sin->Cos
}
}
},
Join[
MapThread[List,{
Select[manipulateSeries,!FreeQ[#,Cos]&],
Select[manipulateSeries,!FreeQ[#,Cos]&]/.Cos->Sin
}],
MapThread[List,
{
Select[manipulateSeries,!FreeQ[#,Sin]&],
Select[manipulateSeries,!FreeQ[#,Sin]&]/.Sin->Cos
}
]
]
]
Final Approach And Why it worked
Creating a New Visual
For my final approach, I used a pair of functions called "FourierCosCoefficient" and "FourierSinCoefficient" in order separately get the radii of the circles. Then the many variables are used to artificially stitch together the coefficients and the frequencies; the code is using a descriete Fourier Transform, thus allowing me to artificially generate the frequencies used. Then the artificially generated Fourier series is used to generate coordinates. In the beginning I had the x coordinates to be in terms of Cosine and y coordinates in terms of Sine which caused the rotating parts to move in the counter clockwise direction. This function then creates individual moving plots which are then combined with the circles through the "Show" function to give the illusion that the circles are creating the plots. This function is named "unblendedCosCircleSmoothie" because this function returns the separate waves that were added to create the approximation, hence the "ingredients" in the "smoothie." I created this function as a demonstration to show how separate waves and circles can create function when combined together; the different steps of the process would be a good visual aid in understanding the concept of what the Fourier Series does. Below there is an example of the function that is used for the even functions. For the odd functions I replaced a few operations and switched wherever it Cos to Sine and vise-versa.
ClearAll[unblendedCosCircleSmoothie]
unblendedCosCircleSmoothie[function_,variable_Symbol,numCir_Integer]:=
Block[
{
fourierCoeff=Table[FourierCosCoefficient[function,variable,p],{p,1,numCir}],
color=Drop[Hue[#]&/@Range[0,1,(1/numCir)],-1],
circleList,basicCoord,manipulatedCoord,maxRadius,lineList,plotList,styledLines,prePlot
},
circleList=Circle[{-maxRadius-5,0},#1]&/@Abs@fourierCoeff;
basicCoord=MapThread[Times,{fourierCoeff,Cos[Range[numCir]*variable]}];
manipulatedCoord=Transpose[{basicCoord-maxRadius-5,basicCoord/.Cos->Sin}];
maxRadius=Max[Abs@fourierCoeff];
lineList=Line[{{-maxRadius-5,0},#1,{0,#1[[2]]}}]&/@manipulatedCoord/.variable->$time;
styledLines=MapThread[Style,{lineList,color}];
prePlot=MapThread[Times,{fourierCoeff,Cos[(Range[numCir]*(variable+$time))-(Pi/2)]}];
plotList=Plot[#1,{variable,0,10},PlotStyle->#2]&@@@(Transpose[{prePlot,color}]);
Animate[
Show[
Graphics[{Sequence@@#1,Sequence@@#2},Axes->True],
Plot[#3,{variable,0,10},PlotStyle->#4,PlotRange->All],PlotRange->All
],
{$time,0,-2Pi},AnimationRunning->False]&@@Evaluate[{circleList,styledLines,prePlot,color}]
Creating the Nested Visual
The following function is structured similar to the previous algorithm. However, the difference is that now the centers of the circles are not static and are changed so that they create a chain. There is an algorithm that takes the set of coordinates and nests them together so that the circles follow an orbit. Another difference in this function is that instead of creating different rotating arms form the origin in order to color them, I was able to insert the entire nested function list into the line which created a line that appeared to be segmented.
ClearAll[circleCosSmoothie2]
circleCosSmoothie2[function_,variable_Symbol,numCir_Integer]:=
Block[{
fourierConstant=FourierCosCoefficient[function,variable,0]//ComplexExpand,
fourierCoeff=Table[FourierCosCoefficient[function,variable,p],{p,1,numCir}],
basicCoord,manipulatedCoord,lastYCoord,totalRadius,combinedCir,cirCenter,
cirArguments,finalPlot,lineSeries,centerOfFirstCir,listOfCoord,lastLinetoYaxis,
lastXCoord,plotCoord,plotAllign
},
totalRadius=Total[Abs@fourierCoeff];
basicCoord=MapThread[Times,{fourierCoeff,Cos[Range[numCir]*$time]}];
manipulatedCoord=Join[
Take[
Transpose[
{(basicCoord-totalRadius-5)/.Cos->Sin,basicCoord}],1],
Drop[Transpose[{basicCoord/.Cos->Sin,basicCoord}],1]];
manipulatedCoord=(Plus@@@Table[
manipulatedCoord[[;;n]],
{n,1,Length[manipulatedCoord]}]//Prepend[{-totalRadius-5,0}]);
lastYCoord={0,Take[Take[manipulatedCoord,-1]//Flatten,-1]};
plotCoord=Total[MapThread[Times,{fourierCoeff,Cos[((Range[numCir])*(variable+$time))]}]];
cirCenter=Drop[manipulatedCoord,-1];
cirArguments=Transpose[{cirCenter,Abs@fourierCoeff}];
lastLinetoYaxis=Prepend[Take[Take[manipulatedCoord,-1]//Flatten,-1],0];
combinedCir={Circle[#1,#2]}&@@@(#1&/@cirArguments);
lineSeries= Line[#1]&@(Append[manipulatedCoord,lastLinetoYaxis]);
Animate[
Show[
Graphics[{Sequence@@#1,#2},Axes->True],
Plot[#3,{variable,0,3*#4},PlotRange->Full],
PlotRange->{{-3*#4,3#4},{-#4,#4}}
],
{$time,0,-2Pi},AnimationRunning->False]&@@Evaluate[{combinedCir,lineSeries,plotCoord,totalRadius}]
]
Making a Funciton that works for all curves
Unlike the other functions, this function uses the FourierSeries function and then extracts data from the equation itself (There are more detailed comment on what each step does in the code ). I turn the equation into a list and then separate the sine and cosine terms into different lists. Then a little bit more modification is needed to extract the coefficients which was done by replacing all the terms in a specific pattern to 1 so that it would multiply out and only leave me with the coefficients. Then I have functions similar to the previous ones which create a list of circle and line Graphics Primitives
ClearAll[circleSmoothietheRest]
circleSmoothietheRest[function_,variable_,numCir_]:=
Block[
{proto=FourierSeries[function,variable,numCir]//ComplexExpand,protoList2,cosList,sinList,cosCoeff,sinCoeff,
basicCosCoord,basicSinCoord,totalRadius,manipulatedCosCoord,manipulatedSinCoord,manipulatedCoord,
lastYCoord,plotCoord,cirCenter,cirArguments,lastLinetoYaxis,combinedCir,lineSeries,range,fourierCoeff},
protoList2=Drop[Apply[List,proto],1];
cosList=Take[protoList2,numCir];
sinList=(Apply[Plus,Partition[Drop[protoList2,numCir],2],{1}]);
cosCoeff=ReplaceAll[#1,Cos[x_]->1]&/@cosList;
sinCoeff=(ReplaceAll[#1,Sin[x_]->1]&/@sinList);
totalRadius=Echo@(Abs@(Total[Abs@cosCoeff]+Total[Abs@sinCoeff]));
fourierCoeff=Abs@Join[cosCoeff,sinCoeff];
manipulatedCosCoord=Join[
Take[
Transpose[
{(cosList-10)/.Cos->Sin,cosList}],1],
Drop[Transpose[{cosList/.Cos->Sin,cosList}],1]];
manipulatedSinCoord=Join[
Take[
Transpose[
{(sinList-10)/.Sin->Cos,sinList}],1],
Drop[Transpose[{sinList/.Sin->Cos,sinList}],1]];
manipulatedCoord=(Join[manipulatedCosCoord,manipulatedSinCoord]/.x->$time);
manipulatedCoord=(Plus@@@Table[
manipulatedCoord[[;;n]],
{n,1,Length[manipulatedCoord]}]//Prepend[{-10,0}]);
lastYCoord={0,Take[Take[manipulatedCoord,-1]//Flatten,-1]};
plotCoord=(Total[cosList]+Total[sinList])/.variable->(variable+$time);
cirCenter=Drop[manipulatedCoord,-1];
(*I need to find a way to extract the radii of the Circles*)
cirArguments=Transpose[{cirCenter,fourierCoeff}];
lastLinetoYaxis=Prepend[Take[Take[manipulatedCoord,-1]//Flatten,-1],0];
combinedCir={Circle[#1,#2]}&@@@(#1&/@cirArguments);
lineSeries= (Line[#1]&@Evaluate[Append[manipulatedCoord,lastLinetoYaxis]]);
range=(2Pi)*totalRadius;
Animate[
Show[
Graphics[{Sequence@@#1,#2},Axes->True],
Plot[#3,{variable,0,30},PlotRange->Full]
],
{$time,0,-2Pi},AnimationRunning->False]&@@Evaluate[{combinedCir,lineSeries,plotCoord}]
]
Putting it All Together
This code was written to separate the odd and even functions and use the corresponding function to them; for even functions I used the cosine functions and for the odd functions I used sine functions. I also added a plot of the original function to show how the approximation compares.
ClearAll[fourierCircleCookBook]
fourierCircleCookBook[function_,variable_Symbol,numCir_Integer]:=
Which[
PossibleZeroQ[function-(function/.variable->(-variable))],
Row[
{unblendedCosCircleSmoothie[function,variable,numCir],
circleCosSmoothie2[function,variable,numCir],
Plot[function,{variable,-30,30},PlotStyle->Black,Frame->True,ImageSize->Medium,Axes->False]}
],
PossibleZeroQ[function+(function/.variable->(-variable))],
Row[
{unblendedSinCircleSmoothie[function,variable,numCir],
circleSinSmoothie2[function,variable,numCir],
Plot[function,{variable,-30,30},PlotStyle->Black,Frame->True,ImageSize->Medium,Axes->False]}
],
True,
circleSmoothietheRest[function,variable,numCir]
]
Further Improvements
In the future, I will create a function that identifies the frequencies itself and provides a visual step by step of the built in function. Currently we need to input a function in order to get an output. In the future I want to implement a function that will take in an audio file and filter out the different frequencies and provide a visual for how it does it.