solution_153.py

#!/usr/bin/python3
# ===================================================================
# Fast Fourier Transform
#
# URL: dsp.stackexchange.com/questions/74748/
#          fft-of-square-wave-what-does-output-represent
# URL: byjus.com/maths/fourier-series/
# URL: lpsa.swarthmore.edu/Fourier/Series/ExFS.html
#
# As you add sine waves of increasingly
# higher frequency, the approximation improves.
# ===================================================================

import math
import draw_xy_axes as ax
import coordinate_conversion as cc
import user_interface as ui
from graphics import *

# -------------------------------------------------------------------
# ---- draw a point
# -------------------------------------------------------------------

def draw_point(win,cx,cy):

    wx,wy = cc.center_to_win_coords(cx,cy,win.width,win.height)

    p = Point(wx,wy)
    p.draw(win)

    return p

# -------------------------------------------------------------------
# ---- draw a circle
# -------------------------------------------------------------------

def draw_circle(win,cx,cy,raidus=2):

    wx,wy = cc.center_to_win_coords(cx,cy,win.width,win.height)
    
    c = Circle(Point(wx,wy),raidus)
    c.setFill('red')
    c.setOutline('red')
    c.draw(win)

    return c

# -------------------------------------------------------------------
# ----- draw lines
# -------------------------------------------------------------------

def draw_line(win,line):

    p0 = line[0]
    p1 = line[1]
    
    l = Line(Point(p0[0],p0[1]),Point(p1[0],p1[1]))
    l.setFill('black')
    l.draw(win)

    return l 

# -------------------------------------------------------------------
# ---- display square wave
# ----
# ---- sub - number of sub-harmonic to add
# -------------------------------------------------------------------

def display_square_wave(win,sub):

    scale    = 100              # y scale factor
    gobj     = []               # list of plotted points

    for deg in range(-180,180,2):

        y = 0                   # accmulated y values
        
        for i in range(1,2*(sub+1),2):  # odd numbers only
            rad = math.radians(i*deg)
            y += (4/math.pi)*((1/i)*math.sin(rad))

        y = scale * y    

        gobj.append(draw_point(win,deg,y))

    return gobj

# -------------------------------------------------------------------
# ---- display sawtooth wave
# ----
# ---- sub - number of sub-harmonic to add
# -------------------------------------------------------------------

def display_sawtooth_wave(win,sub):

    scale    = 100              # y scale factor
    gobj     = []               # list of plotted points

    for deg in range(-180,180,2):

        y = 0                   # accmulated y values
        
        for i in range(1,sub+1):
            rad = math.radians(i*deg)
            y += - (1/math.pi)*(1/i)*math.sin(rad)

        y = scale * y    

        gobj.append(draw_point(win,deg,y))

    return gobj

# -------------------------------------------------------------------
# ---- display triangle wave
# ----
# ---- sub - number of sub-harmonic to add
# -------------------------------------------------------------------

def display_triangle_wave(win,sub):

    scale    = 100              # y scale factor
    gobj     = []               # list of plotted points

    for deg in range(-180,180,2):

        y = 0                   # accmulated y values
        
        for i in range(1,2*(sub+1),2):  # odd values
            rad = math.radians(i*deg)
            y += (8/(math.pi**2))*(pow(-1,(i-1)/2)/i**2)*math.sin(rad)

        y = scale * y    

        gobj.append(draw_point(win,deg,y))

    return gobj

# -------------------------------------------------------------------
# ---- get the number of sub-harmonics to use
# -------------------------------------------------------------------

def sub_harmonics():    

    while(True):

        i = 0
        
        print()
        s = ui.get_user_input(f'Enter sub-harmonics count: ')
        if not s: break
        
        tf,i = ui.is_integer(s)
        if not tf:
            print()
            print(f'Non-integer entered - try again')
            continue   
        if i < 1:
            print()
            print(f'Bad sub-harmonics count ({i}) - try again')
            continue

        break

    return i
            
# -------------------------------------------------------------------
# ---- square wave option
# -------------------------------------------------------------------

def square_wave_option(win):

    pobj = []

    sub = sub_harmonics()
    if sub < 1:
        return pobj

    print()
    print(f'sub-harmonics count is {sub}')

    gobj = display_square_wave(win,sub)

    return gobj
           
# -------------------------------------------------------------------
# ---- saw tooth wave option
# -------------------------------------------------------------------

def sawtooth_option(win):

    pobj = []

    sub = sub_harmonics()
    if sub < 1:
        return pobj

    print()
    print(f'sub-harmonics count is {sub}')

    gobj = display_sawtooth_wave(win,sub)

    return gobj
          
# -------------------------------------------------------------------
# ---- trangle wave option
# -------------------------------------------------------------------

def triangle_option(win):

    pobj = []

    sub = sub_harmonics()
    if sub < 1:
        return pobj

    print()
    print(f'sub-harmonics count is {sub}')

    gobj = display_triangle_wave(win,sub)

    return gobj

# -------------------------------------------------------------------
# ---- main
# -------------------------------------------------------------------

# ---- create graphics window

win = GraphWin("Fourier Transform", 801, 801)
win.setBackground("white")
ax.draw_xy_axes(win)

menu = '''\
+----------------------------------------------------------------+
| Demonstrate How A Fourier Series Work                          |
|                                                                |
| As you add sine/cosine waves of increasingly higher frequency, | 
| the approximation improves.                                    |
|                                                                |
| Program Environment                                            |
| a. Range used -180 degrees to +180 degrees                     |
| b. Vertical scale factor is 100                                |
+----------------------------------------------------------------+

 Option  Description
 ------  ---------------------------
   1     square wave
   2     sawtooth wave
   3     triangle wave
'''

gobj = []

while True:

    print()    
    print(menu)

    s = ui.get_user_input('Enter selection: ')
    if not s: break
    tf,i = ui.is_integer(s)
    if not tf:
        print()
        print(f' Illegal option entered ({s}) - Try again')
        continue

    if i == 1:
        if len(gobj) > 0:
            for obj in gobj:
                obj.undraw()
            gobj = []
        gobj = square_wave_option(win)
        continue

    if i == 2:
        if len(gobj) > 0:
            for obj in gobj:
                obj.undraw()
            gobj = []
        gobj = sawtooth_option(win)
        continue

    if i == 3:
        if len(gobj) > 0:
            for obj in gobj:
                obj.undraw()
            gobj = []
        gobj = triangle_option(win)
        continue        

    print()
    print(f'Unknown Option ({i}) - Try again')

# ---- end program

win.close()