diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2720f9a..35dfe1d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -871,6 +871,12 @@ jobs: apt-get update apt-get install git -y git --version + - name: Install numpy + run: pip3 install numpy + - name: Install casper-r2sdf-fft + run: pip3 install casper-r2sdf-fft + - name: Install pytest + run: pip3 install pytest # Checks-out your repository (and submodules) under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 with: @@ -923,6 +929,8 @@ jobs: git --version - name: Install numpy run: pip3 install numpy + - name: Install casper-r2sdf-fft + run: pip3 install casper-r2sdf-fft - name: Install pytest run: pip3 install pytest # Checks-out your repository (and submodules) under $GITHUB_WORKSPACE, so your job can access it @@ -977,8 +985,10 @@ jobs: git --version - name: Install numpy run: pip3 install numpy + - name: Install casper-r2sdf-fft + run: pip3 install casper-r2sdf-fft - name: Install pytest - run: pip3 install pytest + run: pip3 install pytest # Checks-out your repository (and submodules) under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 with: @@ -1031,8 +1041,10 @@ jobs: git --version - name: Install numpy run: pip3 install numpy + - name: Install casper-r2sdf-fft + run: pip3 install casper-r2sdf-fft - name: Install pytest - run: pip3 install pytest + run: pip3 install pytest # Checks-out your repository (and submodules) under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 @@ -1070,6 +1082,53 @@ jobs: path: ./casper_wb_fft/wide_fft_report.xml if-no-files-found: 'warn' # ['warn', 'error', 'ignore'] + # all_fft: + # # The type of runner that the job will run on + # runs-on: ubuntu-latest + # needs: [files_changed] + # if: needs.files_changed.outputs.all_fft == 'true' + + # # Steps represent a sequence of tasks that will be executed as part of the job + # steps: + # # Checks-out your repository (and submodules) under $GITHUB_WORKSPACE, so your job can access it + # - uses: actions/checkout@v2 + # with: + # submodules: 'recursive' + # token: ${{ secrets.TOKEN }} + # - id: casper_all_fft_test + # uses: VUnit/vunit_action@master + # with: + # image: ghdl/vunit:llvm + # cmd: python3 casper_wb_fft/run.py --xunit-xml ./casper_wb_fft/wide_fft_report.xml + + # - name: Note casper_all_fft_test Failure + # if: ${{ failure() }} + # uses: action-badges/core@0.2.2 + # with: + # label: casper_all_fft + # message: failing + # message-color: "C74D1F" + # file-name: casper_all_fft_test.svg + # badge-branch: badges + # style: flat + # github-token: "${{ secrets.GITHUB_TOKEN }}" + # - name: Note casper_all_fft_test Success + # if: ${{ steps.casper_wide_fft_test.outcome == 'success' }} + # uses: action-badges/core@0.2.2 + # with: + # label: casper_all_fft + # message: passing + # message-color: "4DC71F" + # file-name: casper_all_fft_test.svg + # badge-branch: badges + # style: flat + # github-token: "${{ secrets.GITHUB_TOKEN }}" + # - uses: actions/upload-artifact@v4 + # with: + # name: wide_fft_report + # path: ./casper_wb_fft/wide_all_report.xml + # if-no-files-found: 'warn' # ['warn', 'error', 'ignore'] + single_filter: # The type of runner that the job will run on runs-on: ubuntu-latest diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e59f146..74b3ae9a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,7 @@ casper_accumulators_test: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -32,6 +33,7 @@ casper_adders_test: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -50,6 +52,7 @@ counter: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -68,6 +71,7 @@ delay: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -87,6 +91,7 @@ filter: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -105,6 +110,7 @@ flow_control: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -123,6 +129,7 @@ misc: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -141,6 +148,7 @@ multiplexer: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -159,6 +167,7 @@ multiplier: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -177,6 +186,7 @@ fifo: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -195,6 +205,7 @@ ram: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -213,6 +224,7 @@ rtwosdf_fft: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -231,6 +243,7 @@ rtwosdf_fft_bitaccurate_twid: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -250,6 +263,7 @@ pipe_fft: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -269,6 +283,7 @@ par_fft: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -287,6 +302,7 @@ wide_fft: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -306,6 +322,7 @@ single_filter: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -324,6 +341,7 @@ wide_filter: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH @@ -342,6 +360,7 @@ wbpfb: # This job runs in the test stage. - ghdl script: - python3 -m pip install pytest --progress-bar off + - python3 -m pip install casper-r2sdf-fft --progress-bar off - python3 -m pip install numpy --progress-bar off - echo Attempting to start Vunit Tests - cd $GIT_CLONE_PATH diff --git a/casper_wb_fft/run.py b/casper_wb_fft/run.py index a4ef8b4b..9cd16c2a 100644 --- a/casper_wb_fft/run.py +++ b/casper_wb_fft/run.py @@ -2,10 +2,245 @@ from vunit.sim_if.factory import SIMULATOR_FACTORY from os.path import join, abspath, split,realpath,dirname from importlib.machinery import SourceFileLoader -# load the r2sdf fix point accurate model from r2sdf module. -r2sdf_fft_py = SourceFileLoader("r2sdf_fft_py",f"{realpath(dirname(__file__))}/../r2sdf_fft/r2sdf_fft_py/__init__.py").load_module() +from casper_r2sdf_fft import twiddle_gen,pfft,roundsat +from pathlib import Path import numpy as np + +def make_twiddle_post_check(fftsize, g_twiddle_width,use_vhdl_magic_file): + """ + Return a check function to verify test case output + """ + + def post_check(output_path): + # generate the expected twiddles for this case + # Note if you put a magic file into revision control for a twiddle size, it will then trust + # that size is correct, if you change the twiddle generation you'll need to delete the old magic files! + twiddles=(2**(g_twiddle_width-1))*twiddle_gen(fftsize,g_twiddle_width,1,1,use_vhdl_magic_file,Path(output_path)) + + output_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{fftsize}.txt" + data = np.loadtxt(output_file,dtype="int") + print("Post check: %s" % str(output_file)) + cdata = data[0:data.size:2]+1j*data[1:data.size:2] + + if np.array_equal(cdata,twiddles): + print('Twiddles are exactly the same!') + return True + else: + diffreal=np.abs(np.real(twiddles)-np.real(cdata)) + diffimag=np.abs(np.imag(twiddles)-np.imag(cdata)) + if np.max(diffreal)>1: + print("Twiddle Real Values are more than 1 different!"); + return False + if np.max(diffimag)>1: + print("Twiddle Imag Values are more than 1 different!"); + return False + print("Twiddle Values were +/- 1 from expected!") + # these line can help create the magic files if left uncommented but shouldn't be uncommented normally + #import shutil + #shutil.copy2(output_file,os.path.realpath(os.path.dirname(__file__))) + return True + + return post_check +def tb_twiddle_package_setup(ui): + + testbench=ui.test_bench("tb_vu_twiddlepkg") + for fftsizelog2 in range(1,16): # this was originally 1,21 and passed on March 24, 2023, but reduced to make execution faster + for bidx in range(16,19): #this was originally 12,26, but to save time was converted to16:19 + fftsize=2**fftsizelog2 + testbench.add_config( + name=f"TwiddlePython_w{bidx}b_{fftsize}", + generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + post_check=make_twiddle_post_check(fftsize,bidx,0)) + #testbench.add_config( + # name=f"TwiddleMagic_w{bidx}b_{fftsize}", + # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + # post_check=make_twiddle_post_check(fftsize,bidx,1)) + + + +def make_fft_preconfig(g_fftsize_log2, g_in_dat_w,scale_sched,data): + """ + Return a precheck function that will generate input data. + """ + + def pre_config(output_path): + output_file = Path(output_path) / f"input_data.txt" + f = open(output_file,'w') + header = np.zeros(8,dtype=np.uint32) + header[0] = 2**g_fftsize_log2 + header[1] = g_in_dat_w + header[2] = data.size + header[3] = scale_sched + header[7] = 2122219905 + np.savetxt(f,header,fmt='%u') + data_to_write = np.zeros(2*data.size,dtype=np.int32) + data_to_write[0::2] = np.real(data) + data_to_write[1::2] = np.imag(data) + + np.savetxt(f,data_to_write,fmt='%d') + f.close() + return True + return pre_config + +def make_fft_postcheck(g_use_reorder,g_in_dat_w,g_out_dat_w,g_stage_dat_w,g_guard_w,g_twiddle_width,g_fftsize_log2,g_do_rounding,g_do_saturation,scale_sched): + """ + Return a precheck function that will generate input data. + """ + + def post_check(output_path): + # Read the data created by the pre_config script + input_file = Path(output_path) / f"input_data.txt" + input_data = np.loadtxt(input_file,dtype="int") + header = input_data + input_cdata = input_data[8:input_data.size:2]+1j*input_data[9:input_data.size:2] + if header[0] != (2**g_fftsize_log2): + print("Bad Header in input data") + return False + if header[1] != (g_in_dat_w): + print("Input Data width mismatch") + return False + if header[2] != input_cdata.size: + print("Input Data size mismatch") + return False + if header[3] != scale_sched: + print("Input Data Scale Mismatch") + return False + if header[7] != 2122219905: + print("Input Data Magic Word Mismatch") + return False + + # Read the stage data files (if they exist) + #stage_data = np.zeros((input_cdata.size,g_fftsize_log2+1),dtype=np.complex128) + #for stageidx in range(0,g_fftsize_log2+1): + #stage_file = Path(output_path) / f"stage_data{stageidx}.txt" + #data = np.loadtxt(stage_file,dtype="int32") + #stage_cdata = data[0:data.size:2]+1j*data[1:data.size:2] + #stage_data[:,stageidx] = stage_cdata + + + output_file = Path(output_path) / f"output_data.txt" + data = np.loadtxt(output_file,dtype="int32") + print("Post check: %s" % str(output_file)) + vhdl_cdata = data[0:data.size:2]+1j*data[1:data.size:2] + if input_cdata.shape != vhdl_cdata.shape: + print("Fft Post check: Unexpected Data length") + return False + import shutil + # Copy the download twiddle lookup tables into the script directory so they get used. + #for twididx in range(0,g_fftsize_log2): + # twid_size = 2**twididx + # twid_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{twid_size}.txt" + # shutil.copy2(twid_file,os.path.realpath(os.path.dirname(__file__))) + + # VHDL only support DIF, and is configured to do bitrev + if g_use_reorder==True: + do_output_bit_rev = 1 + else: + do_output_bit_rev = 0 + g_bits_to_round_off = np.zeros(g_fftsize_log2) + g_output_width = g_out_dat_w * np.ones(g_fftsize_log2) + for bit_idx in range(0,g_fftsize_log2): + bit = (scale_sched >> bit_idx) & 1 + if bit==1: + g_bits_to_round_off[bit_idx]=1 + else: + g_bits_to_round_off[bit_idx]=0 + + expected_cdata,stagedebug=pfft(input_cdata,g_fftsize_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,1,0,do_output_bit_rev,Path(output_path)) + + file_path = Path(output_path) / f"matdata_debug.mat" + matdict = {} + matdict['expected_cdata'] = expected_cdata + matdict['stagedebug'] = stagedebug + matdict['vhdl_cdata'] = vhdl_cdata + #matdict['stage_data'] = stage_data + matdict['input_cdata'] = input_cdata + #io.savemat(file_path, matdict) + + + if np.array_equal(expected_cdata[:,0],vhdl_cdata): + print("VHDL Matched Python!") + print("Test Passed!") + return True + else: + print("Data Did not match!") + return False + + + return post_check + +def tb_vu_trwosdf_vfmodel_setup(ui): + + testbench=ui.test_bench("tb_vu_trwosdf_vfmodel") + use_reorder = True + in_dat_w = 18 + out_dat_w = 18 + stage_dat_w = 18 + guard_w = 0 + twiddle_width = 18 + fftsize_log2 = 13 + + do_rounding = 1 + do_saturation = 1 + enable_pattern = 2 #every other clock + # Decode some of those for VHDL + if do_rounding==1: + use_round = "ROUND" + use_mult_round = "ROUND" + if do_saturation==1: + ovflw_behav = "SATURATE" + + scale_sched = 0 + for stage in range(0,fftsize_log2): + if (stage % 2)==1: + scale_sched = scale_sched + 2**stage + # scale at Stage 0 + if stage==0: + scale_sched = scale_sched + 2**stage + if stage==2: + scale_sched = scale_sched + 2**stage + + + d_indices = np.arange(0,2*(2**fftsize_log2)) + # Generate a full scale cw with 12-bits + data = 2047*np.exp(1.0j * 2*np.pi * d_indices*(-2e9/7e9)) + noise = np.random.normal(0, 5.5, size=(data.shape[0])) + data = data + noise + data = roundsat(data,1,in_dat_w,0,1,1,1) + + enable_pattern = 0 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E0_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 1 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_Erandom_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 2 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E10Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 3 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E100Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + + + + # name=f"TwiddleMagic_w{bidx}b_{fftsize}", + # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + # post_check=make_twiddle_post_check(fftsize,bidx,1)) + + def tb_vu_wb_fft_vfmodel_setup(ui): testbench=ui.test_bench("tb_vu_wb_fft_vfmodel") @@ -43,30 +278,30 @@ def tb_vu_wb_fft_vfmodel_setup(ui): data = 2047*np.exp(1.0j * 2*np.pi * d_indices*(-2e9/7e9)) #noise = np.random.normal(0, 5.5, size=(data.shape[0])) #data = data + noise - data = r2sdf_fft_py.roundsat(data,1,in_dat_w,0,1,1,1) + data = roundsat(data,1,in_dat_w,0,1,1,1) enable_pattern = 0 testbench.add_config( - pre_config=r2sdf_fft_py.make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=r2sdf_fft_py.make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), name=f"FFTWIDE_E0_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) enable_pattern = 1 testbench.add_config( - pre_config=r2sdf_fft_py.make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=r2sdf_fft_py.make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), name=f"FFTWIDE_Erandom_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) enable_pattern = 2 testbench.add_config( - pre_config=r2sdf_fft_py.make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=r2sdf_fft_py.make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), name=f"FFTWIDE_E10Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) enable_pattern = 3 testbench.add_config( - pre_config=r2sdf_fft_py.make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=r2sdf_fft_py.make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), name=f"FFTWIDE_E100Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) diff --git a/r2sdf_fft/casper_r2sdf_fft_py/LICENSE b/r2sdf_fft/casper_r2sdf_fft_py/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/r2sdf_fft/casper_r2sdf_fft_py/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/r2sdf_fft/casper_r2sdf_fft_py/README.md b/r2sdf_fft/casper_r2sdf_fft_py/README.md new file mode 100644 index 00000000..16cfdc1a --- /dev/null +++ b/r2sdf_fft/casper_r2sdf_fft_py/README.md @@ -0,0 +1,2 @@ +# CASPER R2SDF FFT Model +This is a fixed point model of the CASPERHDL FFT block diff --git a/r2sdf_fft/casper_r2sdf_fft_py/pyproject.toml b/r2sdf_fft/casper_r2sdf_fft_py/pyproject.toml new file mode 100644 index 00000000..7b93943c --- /dev/null +++ b/r2sdf_fft/casper_r2sdf_fft_py/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "casper_r2sdf_fft" +version = "0.0.3" +authors = [ + { name="Matthew Schiller", email="mschille@nrao.edu" }, +] +description = "A model for the CASPER FFT" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/talonmyburgh/casper_dspdevelt" +Issues = "https://github.com/talonmyburgh/casper_dspdevel/issues" + +[build-system] +requires = ["hatchling","numpy","pathlib"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/casper_r2sdf_fft"] + diff --git a/r2sdf_fft/casper_r2sdf_fft_py/src/casper_r2sdf_fft/__init__.py b/r2sdf_fft/casper_r2sdf_fft_py/src/casper_r2sdf_fft/__init__.py new file mode 100644 index 00000000..21edb53a --- /dev/null +++ b/r2sdf_fft/casper_r2sdf_fft_py/src/casper_r2sdf_fft/__init__.py @@ -0,0 +1,229 @@ +#------------------------------------- +#-- Fixed point model of the FFT + testbench generation +#-- Able to be imported as a module in other designs. +#------------------------------------- +#--Author : M. Schiller (NRAO) +#--Date : 23-March-2023 +# +#-------------------------------------------------------------------------------- +#-- Copyright NRAO March 23, 2023 +#-------------------------------------------------------------------------------- +#-- License +#-- 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. +#-- +import numpy as np +#import matplotlib.pyplot as plt +from vunit import VUnit +from pathlib import Path +import os +#from scipy import io + +def roundsat(data,signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation): + if (integer_bits+fractional_bits)==0: + # don't bother rounding. + return data + if np.iscomplexobj(data): + # it's complex call ourselves with the real and imag part + realround = roundsat(np.real(data),signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation) + imaground = roundsat(np.imag(data),signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation) + return (realround + 1j*imaground) + if signednum==1: + maxpos = ((pow(2,integer_bits+fractional_bits))-1)/(2**fractional_bits) + maxneg = (0-(pow(2,integer_bits+fractional_bits)))/(2**fractional_bits) + else: + maxpos = ((pow(2,integer_bits+fractional_bits))-1)//(2**fractional_bits) + maxneg = 0 + # by default around is convergent round2even, not "bankers" round away from 0. + if g_do_rounding==1: + dataout = np.divide(np.around(np.multiply(data,pow(2,fractional_bits))),pow(2,fractional_bits)) + else: + dataout = np.divide(np.floor(np.multiply(data,pow(2,fractional_bits))),pow(2,fractional_bits)) + + sathighcount = np.count_nonzero(np.greater(dataout,maxpos)) + if print_saturation==1: + if sathighcount>0: + print("Saturating values to Max positive") + satlowcount = np.count_nonzero(np.less(dataout,maxneg)) + if satlowcount>0: + print("Saturating Values to Max negative") + if g_do_saturation==1: + dataout = np.where(dataoutmaxpos,maxpos,dataout) + return dataout + +def twiddle_gen(fftsize,g_twiddle_width,g_do_rounding,g_do_saturation,g_use_vhdl,coef_base): + coefpath = Path(f"{coef_base}/twiddlepkg_twidth{g_twiddle_width}_fftsize{fftsize}.txt") + if (g_use_vhdl and Path(coefpath).is_file()) : + # the VHDL twiddle generator uses VHDL SIN/COS which aren't exactly the same + # as the python SIN/COS + # This causes +/- 1 errors in the twiddles + # to get a perfect FFT simulation we have the option to use a lookup table generated + # by VHDL, but it might not exist yet + # if your size does not exist execute these steps: + # 1) Add a test below in tb_twiddle_package_setup + # 2) Execute the test using your simulator of choice + # 3) find the output file Vunit creates eg: + # /export/home/creon/mschiller_ngvla_project/casper_dspdevel/r2sdf_fft/vunit_out/test_output/r2sdf_fft_lib.tb_vu_twiddlepkg.Twiddle_w18b_8192_a070bdfd1c276c4964716de8a2ae80049f2966b7/fortwidddlepkg_twidth18_fftsize8192.txt + # Add the file to Source control inside the directory that contains this script + print("Using Prestored VHDL coefficients for this size") + data = np.loadtxt(coefpath,dtype="int") + print("Loading Twiddles from: %s" % str(coefpath)) + coeffs = data[0:data.size:2]+1j*data[1:data.size:2] + coeffs = coeffs / (2**(g_twiddle_width-1)) + return coeffs + else: + print("Using Python Coefficient Generation") + coeff_indices = np.arange(0,fftsize) + coeffs = np.exp(np.multiply(coeff_indices,1.0j * -2*np.pi / (2*fftsize))) + coeffs = roundsat(coeffs,1,0,g_twiddle_width-1,g_do_rounding,g_do_saturation,0) # coeffs will still be floating point, but will have the precision indicated by g_twiddle_width + return coeffs + +def fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif): + # xa/xb are assumeed to be integers between stages, any fraction will be rounded off on outputs + if g_do_dif==1: + ya = xa+xb + yb = np.multiply(twiddle,(xa-xb)) + # this isn't really best practice to round here, but it's what the VHDL does + yb = roundsat(yb,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) + ya = np.multiply(ya,pow(2,(-g_bits_to_round_off))) + yb = np.multiply(yb,pow(2,(-g_bits_to_round_off))) + else: + temp = np.multiply(xb,twiddle) + # this isn't really best practice to round here, but it's what the VHDL does + temp = roundsat(temp,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) + ya = xa + temp + yb = xa - temp + ya = np.multiply(ya,pow(2,(-g_bits_to_round_off))) + yb = np.multiply(yb,pow(2,(-g_bits_to_round_off))) + ya = roundsat(ya,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) # no fraction bit on output, integer only! + yb = roundsat(yb,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) + return ya,yb + +def fft_stage(stage_in,fft_size_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif,coef_base): + # Make sure input is the length + if fft_size_log2==0: + stage_out=stage_in + return stage_out + if np.mod(stage_in.shape[0],2**fft_size_log2)>0: + stage_in = stage_in[1:(2**fft_size_log2)*(stage_in.shape[0]//2**fft_size_log2)] + if g_do_dif==1: + #in DIF FFT we need to split into two halves the stage_in data based on + #the current FFTsize + #First reshape stage_in into fftsize X N + data = np.transpose(np.reshape(stage_in,((np.shape(stage_in)[0]//(2**fft_size_log2)),(2**fft_size_log2)))) + xa = data[0:2**(fft_size_log2-1),:] + xb = data[2**(fft_size_log2-1):,:] + # Twiddle values are always rounded and saturated. + twiddle = twiddle_gen(2**(fft_size_log2-1),g_twiddle_width,1,1,1,coef_base) + twiddle = np.tile(np.transpose(np.atleast_2d(twiddle)),(1,np.shape(xa)[1])) + ya,yb = fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif) + stage_out = np.zeros(data.shape,np.complex128) + stage_out[0:2**(fft_size_log2-1),:] = ya + stage_out[2**(fft_size_log2-1):2**(fft_size_log2),:] = yb + else: + if fft_size_log2==0: + stage_out=stage_in + return stage_out + data = np.reshape(stage_in,(pow(2,fft_size_log2),np.shape(stage_in)[0]/(pow(2,fft_size_log2)))); + xa = data[0:2^(fft_size_log2-1),:] + xb = data[2^(fft_size_log2-1):,:] + twiddle = twiddle_gen(pow(2,(fft_size_log2-1)),g_twiddle_width,1,1,1,coef_base) + twiddle = np.tile(twiddle,(1,np.shape(xa)[1])) + ya,yb = fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif) + stage_out = np.zeros(data.shape,np.complex128) + stage_out[0:2**(fft_size_log2-1),:] = ya + stage_out[2**(fft_size_log2-1):2**(fft_size_log2),:] = yb + + + stage_out = np.reshape(np.transpose(stage_out),(stage_out.shape[0]*stage_out.shape[1],1)) + return stage_out + +def bit_reverse_traverse_no_generator(a): + n = a.shape[0] + assert(not n&(n-1)) + + if n == 1: + return a + else: + even_indicies = np.arange(n/2,dtype=np.int32)*2 + odd_indicies = np.arange(n/2,dtype=np.int32)*2 + 1 + + evens = bit_reverse_traverse_no_generator(a[even_indicies]) + odds = bit_reverse_traverse_no_generator(a[odd_indicies]) + + return np.concatenate([evens, odds]) + +def get_bit_reversed_list_no_generator(l): + n = len(l) + + indexs = np.arange(n,dtype=np.int32) + b = [] + for i in bit_reverse_traverse_no_generator(indexs): + b.append(l[i]) + + return b + +def bitrevorder(a): + return get_bit_reversed_list_no_generator(a) + + +def pfft(data,fftsize_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif,g_do_bit_rev_input,g_do_bit_rev_output,coef_base): + # enforce that input data is a multiple of the FFTsize + if np.mod(data.shape[0],pow(2,fftsize_log2))>0: + data = data[1:(pow(2,fftsize_log2)*np.floor(data.shape[1]/pow(2,fftsize_log2)))] + + + if g_do_dif==1: + idxlog2range = np.arange(fftsize_log2,0,-1) + else: + idxlog2range = np.arange(1,fftsize_log2+1) + if g_do_bit_rev_input==1: + # Bitrev won't accept long arrays properly + data_bit_rev_in = np.reshape(data,(2**fftsize_log2,(data.shape[0]//(2**fftsize_log2)))) + for n in range(0,data_bit_rev_in.shape[1]): + data[n*(2**fftsize_log2):(n+1)*(2**fftsize_log2)]=bitrevorder(data_bit_rev_in[:,n]) + + + stageout = data + stage_num = 0 + if g_output_width.size != idxlog2range.size: + raise ValueError('g_output_width not long enough') + return 1 + if len(g_bits_to_round_off) != len(idxlog2range): + raise ValueError('g_bits_to_round_off not long enough') + return 1 + stagedebug =np.zeros((data.size,idxlog2range.size),dtype=np.complex128) + for idxlog2 in idxlog2range: + print("Processing Stage %d of %d\n"%(stage_num,len(idxlog2range))) + stageout = fft_stage(stageout,int(idxlog2),g_twiddle_width,g_do_rounding,g_do_saturation,int(g_output_width[idxlog2-1]),int(g_bits_to_round_off[idxlog2-1]),g_do_dif,coef_base) + stagedebug[:,stage_num] = stageout[:,0] + stage_num = stage_num + 1 + + if g_do_bit_rev_output==1: + # Bitrev won't accept long arrays properly that exceed the FFTsize. + # convert to an array of fftsize x n so we can operate on single length blocks at a time. + data_bit_rev_in = np.transpose(np.reshape(stageout,((np.shape(stageout)[0]//(2**fftsize_log2)),(2**fftsize_log2)))) + + for n in range(0,data_bit_rev_in.shape[1]): + temp_rev = np.asarray(bitrevorder(data_bit_rev_in[:,n])) + stageout[n*(2**fftsize_log2):(n+1)*(2**fftsize_log2)]=np.transpose(np.atleast_2d(temp_rev)) + return stageout,stagedebug + + + +def main(): + print("There is no main..") + + +if __name__=="__main__": + main() \ No newline at end of file diff --git a/r2sdf_fft/r2sdf_fft_py/__init__.py b/r2sdf_fft/r2sdf_fft_py/__init__.py deleted file mode 100644 index a15d8fa1..00000000 --- a/r2sdf_fft/r2sdf_fft_py/__init__.py +++ /dev/null @@ -1,493 +0,0 @@ -#------------------------------------- -#-- Fixed point model of the FFT + testbench generation -#-- Able to be imported as a module in other designs. -#------------------------------------- -#--Author : M. Schiller (NRAO) -#--Date : 23-March-2023 -# -#-------------------------------------------------------------------------------- -#-- Copyright NRAO March 23, 2023 -#-------------------------------------------------------------------------------- -#-- License -#-- 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. -#-- -import numpy as np -#import matplotlib.pyplot as plt -from vunit import VUnit -from pathlib import Path -import os -#from scipy import io - -def roundsat(data,signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation): - if (integer_bits+fractional_bits)==0: - # don't bother rounding. - return data - if np.iscomplexobj(data): - # it's complex call ourselves with the real and imag part - realround = roundsat(np.real(data),signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation) - imaground = roundsat(np.imag(data),signednum,integer_bits,fractional_bits,g_do_rounding,g_do_saturation,print_saturation) - return (realround + 1j*imaground) - if signednum==1: - maxpos = ((pow(2,integer_bits+fractional_bits))-1)/(2**fractional_bits) - maxneg = (0-(pow(2,integer_bits+fractional_bits)))/(2**fractional_bits) - else: - maxpos = ((pow(2,integer_bits+fractional_bits))-1)//(2**fractional_bits) - maxneg = 0 - # by default around is convergent round2even, not "bankers" round away from 0. - if g_do_rounding==1: - dataout = np.divide(np.around(np.multiply(data,pow(2,fractional_bits))),pow(2,fractional_bits)) - else: - dataout = np.divide(np.floor(np.multiply(data,pow(2,fractional_bits))),pow(2,fractional_bits)) - - sathighcount = np.count_nonzero(np.greater(dataout,maxpos)) - if print_saturation==1: - if sathighcount>0: - print("Saturating values to Max positive") - satlowcount = np.count_nonzero(np.less(dataout,maxneg)) - if satlowcount>0: - print("Saturating Values to Max negative") - if g_do_saturation==1: - dataout = np.where(dataoutmaxpos,maxpos,dataout) - return dataout - -def twiddle_gen(fftsize,g_twiddle_width,g_do_rounding,g_do_saturation,g_use_vhdl): - coefpath = Path(f"{os.path.realpath(os.path.dirname(__file__))}/twiddlepkg_twidth{g_twiddle_width}_fftsize{fftsize}.txt") - if (g_use_vhdl and Path(coefpath).is_file()) : - # the VHDL twiddle generator uses VHDL SIN/COS which aren't exactly the same - # as the python SIN/COS - # This causes +/- 1 errors in the twiddles - # to get a perfect FFT simulation we have the option to use a lookup table generated - # by VHDL, but it might not exist yet - # if your size does not exist execute these steps: - # 1) Add a test below in tb_twiddle_package_setup - # 2) Execute the test using your simulator of choice - # 3) find the output file Vunit creates eg: - # /export/home/creon/mschiller_ngvla_project/casper_dspdevel/r2sdf_fft/vunit_out/test_output/r2sdf_fft_lib.tb_vu_twiddlepkg.Twiddle_w18b_8192_a070bdfd1c276c4964716de8a2ae80049f2966b7/fortwidddlepkg_twidth18_fftsize8192.txt - # Add the file to Source control inside the directory that contains this script - print("Using Prestored VHDL coefficients for this size") - data = np.loadtxt(coefpath,dtype="int") - print("Loading Twiddles from: %s" % str(coefpath)) - coeffs = data[0:data.size:2]+1j*data[1:data.size:2] - coeffs = coeffs / (2**(g_twiddle_width-1)) - return coeffs - else: - print("Using Python Coefficient Generation") - coeff_indices = np.arange(0,fftsize) - coeffs = np.exp(np.multiply(coeff_indices,1.0j * -2*np.pi / (2*fftsize))) - coeffs = roundsat(coeffs,1,0,g_twiddle_width-1,g_do_rounding,g_do_saturation,0) # coeffs will still be floating point, but will have the precision indicated by g_twiddle_width - return coeffs - -def fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif): - # xa/xb are assumeed to be integers between stages, any fraction will be rounded off on outputs - if g_do_dif==1: - ya = xa+xb - yb = np.multiply(twiddle,(xa-xb)) - # this isn't really best practice to round here, but it's what the VHDL does - yb = roundsat(yb,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) - ya = np.multiply(ya,pow(2,(-g_bits_to_round_off))) - yb = np.multiply(yb,pow(2,(-g_bits_to_round_off))) - else: - temp = np.multiply(xb,twiddle) - # this isn't really best practice to round here, but it's what the VHDL does - temp = roundsat(temp,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) - ya = xa + temp - yb = xa - temp - ya = np.multiply(ya,pow(2,(-g_bits_to_round_off))) - yb = np.multiply(yb,pow(2,(-g_bits_to_round_off))) - ya = roundsat(ya,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) # no fraction bit on output, integer only! - yb = roundsat(yb,1,g_output_width-1,0,g_do_rounding,g_do_saturation,1) - return ya,yb - -def fft_stage(stage_in,fft_size_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif): - # Make sure input is the length - if fft_size_log2==0: - stage_out=stage_in - return stage_out - if np.mod(stage_in.shape[0],2**fft_size_log2)>0: - stage_in = stage_in[1:(2**fft_size_log2)*(stage_in.shape[0]//2**fft_size_log2)] - if g_do_dif==1: - #in DIF FFT we need to split into two halves the stage_in data based on - #the current FFTsize - #First reshape stage_in into fftsize X N - data = np.transpose(np.reshape(stage_in,((np.shape(stage_in)[0]//(2**fft_size_log2)),(2**fft_size_log2)))) - xa = data[0:2**(fft_size_log2-1),:] - xb = data[2**(fft_size_log2-1):,:] - # Twiddle values are always rounded and saturated. - twiddle = twiddle_gen(2**(fft_size_log2-1),g_twiddle_width,1,1,1) - twiddle = np.tile(np.transpose(np.atleast_2d(twiddle)),(1,np.shape(xa)[1])) - ya,yb = fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif) - stage_out = np.zeros(data.shape,np.complex128) - stage_out[0:2**(fft_size_log2-1),:] = ya - stage_out[2**(fft_size_log2-1):2**(fft_size_log2),:] = yb - else: - if fft_size_log2==0: - stage_out=stage_in - return stage_out - data = np.reshape(stage_in,(pow(2,fft_size_log2),np.shape(stage_in)[0]/(pow(2,fft_size_log2)))); - xa = data[0:2^(fft_size_log2-1),:] - xb = data[2^(fft_size_log2-1):,:] - twiddle = twiddle_gen(pow(2,(fft_size_log2-1)),g_twiddle_width,1,1,1) - twiddle = np.tile(twiddle,(1,np.shape(xa)[1])) - ya,yb = fft_butterfly(xa,xb,twiddle,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif) - stage_out = np.zeros(data.shape,np.complex128) - stage_out[0:2**(fft_size_log2-1),:] = ya - stage_out[2**(fft_size_log2-1):2**(fft_size_log2),:] = yb - - - stage_out = np.reshape(np.transpose(stage_out),(stage_out.shape[0]*stage_out.shape[1],1)) - return stage_out - -def bit_reverse_traverse_no_generator(a): - n = a.shape[0] - assert(not n&(n-1)) - - if n == 1: - return a - else: - even_indicies = np.arange(n/2,dtype=np.int32)*2 - odd_indicies = np.arange(n/2,dtype=np.int32)*2 + 1 - - evens = bit_reverse_traverse_no_generator(a[even_indicies]) - odds = bit_reverse_traverse_no_generator(a[odd_indicies]) - - return np.concatenate([evens, odds]) - -def get_bit_reversed_list_no_generator(l): - n = len(l) - - indexs = np.arange(n,dtype=np.int32) - b = [] - for i in bit_reverse_traverse_no_generator(indexs): - b.append(l[i]) - - return b - -def bitrevorder(a): - return get_bit_reversed_list_no_generator(a) - - -def pfft(data,fftsize_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif,g_do_bit_rev_input,g_do_bit_rev_output): - # enforce that input data is a multiple of the FFTsize - if np.mod(data.shape[0],pow(2,fftsize_log2))>0: - data = data[1:(pow(2,fftsize_log2)*np.floor(data.shape[1]/pow(2,fftsize_log2)))] - - - if g_do_dif==1: - idxlog2range = np.arange(fftsize_log2,0,-1) - else: - idxlog2range = np.arange(1,fftsize_log2+1) - if g_do_bit_rev_input==1: - # Bitrev won't accept long arrays properly - data_bit_rev_in = np.reshape(data,(2**fftsize_log2,(data.shape[0]//(2**fftsize_log2)))) - for n in range(0,data_bit_rev_in.shape[1]): - data[n*(2**fftsize_log2):(n+1)*(2**fftsize_log2)]=bitrevorder(data_bit_rev_in[:,n]) - - - stageout = data - stage_num = 0 - if g_output_width.size != idxlog2range.size: - raise ValueError('g_output_width not long enough') - return 1 - if len(g_bits_to_round_off) != len(idxlog2range): - raise ValueError('g_bits_to_round_off not long enough') - return 1 - stagedebug =np.zeros((data.size,idxlog2range.size),dtype=np.complex128) - for idxlog2 in idxlog2range: - print("Processing Stage %d of %d\n"%(stage_num,len(idxlog2range))) - stageout = fft_stage(stageout,int(idxlog2),g_twiddle_width,g_do_rounding,g_do_saturation,int(g_output_width[idxlog2-1]),int(g_bits_to_round_off[idxlog2-1]),g_do_dif) - stagedebug[:,stage_num] = stageout[:,0] - stage_num = stage_num + 1 - - if g_do_bit_rev_output==1: - # Bitrev won't accept long arrays properly that exceed the FFTsize. - # convert to an array of fftsize x n so we can operate on single length blocks at a time. - data_bit_rev_in = np.transpose(np.reshape(stageout,((np.shape(stageout)[0]//(2**fftsize_log2)),(2**fftsize_log2)))) - - for n in range(0,data_bit_rev_in.shape[1]): - temp_rev = np.asarray(bitrevorder(data_bit_rev_in[:,n])) - stageout[n*(2**fftsize_log2):(n+1)*(2**fftsize_log2)]=np.transpose(np.atleast_2d(temp_rev)) - return stageout,stagedebug - -def make_twiddle_post_check(fftsize, g_twiddle_width,use_vhdl_magic_file): - """ - Return a check function to verify test case output - """ - - def post_check(output_path): - # generate the expected twiddles for this case - # Note if you put a magic file into revision control for a twiddle size, it will then trust - # that size is correct, if you change the twiddle generation you'll need to delete the old magic files! - twiddles=(2**(g_twiddle_width-1))*twiddle_gen(fftsize,g_twiddle_width,1,1,use_vhdl_magic_file) - - output_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{fftsize}.txt" - data = np.loadtxt(output_file,dtype="int") - print("Post check: %s" % str(output_file)) - cdata = data[0:data.size:2]+1j*data[1:data.size:2] - - if np.array_equal(cdata,twiddles): - print('Twiddles are exactly the same!') - return True - else: - diffreal=np.abs(np.real(twiddles)-np.real(cdata)) - diffimag=np.abs(np.imag(twiddles)-np.imag(cdata)) - if np.max(diffreal)>1: - print("Twiddle Real Values are more than 1 different!"); - return False - if np.max(diffimag)>1: - print("Twiddle Imag Values are more than 1 different!"); - return False - print("Twiddle Values were +/- 1 from expected!") - # these line can help create the magic files if left uncommented but shouldn't be uncommented normally - #import shutil - #shutil.copy2(output_file,os.path.realpath(os.path.dirname(__file__))) - return True - - return post_check -def tb_twiddle_package_setup(ui): - - testbench=ui.test_bench("tb_vu_twiddlepkg") - for fftsizelog2 in range(1,16): # this was originally 1,21 and passed on March 24, 2023, but reduced to make execution faster - for bidx in range(16,19): #this was originally 12,26, but to save time was converted to16:19 - fftsize=2**fftsizelog2 - testbench.add_config( - name=f"TwiddlePython_w{bidx}b_{fftsize}", - generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), - post_check=make_twiddle_post_check(fftsize,bidx,0)) - #testbench.add_config( - # name=f"TwiddleMagic_w{bidx}b_{fftsize}", - # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), - # post_check=make_twiddle_post_check(fftsize,bidx,1)) - - - -def make_fft_preconfig(g_fftsize_log2, g_in_dat_w,scale_sched,data): - """ - Return a precheck function that will generate input data. - """ - - def pre_config(output_path): - output_file = Path(output_path) / f"input_data.txt" - f = open(output_file,'w') - header = np.zeros(8,dtype=np.uint32) - header[0] = 2**g_fftsize_log2 - header[1] = g_in_dat_w - header[2] = data.size - header[3] = scale_sched - header[7] = 2122219905 - np.savetxt(f,header,fmt='%u') - data_to_write = np.zeros(2*data.size,dtype=np.int32) - data_to_write[0::2] = np.real(data) - data_to_write[1::2] = np.imag(data) - - np.savetxt(f,data_to_write,fmt='%d') - f.close() - return True - return pre_config - -def make_fft_postcheck(g_use_reorder,g_in_dat_w,g_out_dat_w,g_stage_dat_w,g_guard_w,g_twiddle_width,g_fftsize_log2,g_do_rounding,g_do_saturation,scale_sched): - """ - Return a precheck function that will generate input data. - """ - - def post_check(output_path): - # Read the data created by the pre_config script - input_file = Path(output_path) / f"input_data.txt" - input_data = np.loadtxt(input_file,dtype="int") - header = input_data - input_cdata = input_data[8:input_data.size:2]+1j*input_data[9:input_data.size:2] - if header[0] != (2**g_fftsize_log2): - print("Bad Header in input data") - return False - if header[1] != (g_in_dat_w): - print("Input Data width mismatch") - return False - if header[2] != input_cdata.size: - print("Input Data size mismatch") - return False - if header[3] != scale_sched: - print("Input Data Scale Mismatch") - return False - if header[7] != 2122219905: - print("Input Data Magic Word Mismatch") - return False - - # Read the stage data files (if they exist) - #stage_data = np.zeros((input_cdata.size,g_fftsize_log2+1),dtype=np.complex128) - #for stageidx in range(0,g_fftsize_log2+1): - #stage_file = Path(output_path) / f"stage_data{stageidx}.txt" - #data = np.loadtxt(stage_file,dtype="int32") - #stage_cdata = data[0:data.size:2]+1j*data[1:data.size:2] - #stage_data[:,stageidx] = stage_cdata - - - output_file = Path(output_path) / f"output_data.txt" - data = np.loadtxt(output_file,dtype="int32") - print("Post check: %s" % str(output_file)) - vhdl_cdata = data[0:data.size:2]+1j*data[1:data.size:2] - if input_cdata.shape != vhdl_cdata.shape: - print("Fft Post check: Unexpected Data length") - return False - import shutil - # Copy the download twiddle lookup tables into the script directory so they get used. - for twididx in range(0,g_fftsize_log2): - twid_size = 2**twididx - twid_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{twid_size}.txt" - shutil.copy2(twid_file,os.path.realpath(os.path.dirname(__file__))) - - # VHDL only support DIF, and is configured to do bitrev - if g_use_reorder==True: - do_output_bit_rev = 1 - else: - do_output_bit_rev = 0 - g_bits_to_round_off = np.zeros(g_fftsize_log2) - g_output_width = g_out_dat_w * np.ones(g_fftsize_log2) - for bit_idx in range(0,g_fftsize_log2): - bit = (scale_sched >> bit_idx) & 1 - if bit==1: - g_bits_to_round_off[bit_idx]=1 - else: - g_bits_to_round_off[bit_idx]=0 - - expected_cdata,stagedebug=pfft(input_cdata,g_fftsize_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,1,0,do_output_bit_rev) - - file_path = Path(output_path) / f"matdata_debug.mat" - matdict = {} - matdict['expected_cdata'] = expected_cdata - matdict['stagedebug'] = stagedebug - matdict['vhdl_cdata'] = vhdl_cdata - #matdict['stage_data'] = stage_data - matdict['input_cdata'] = input_cdata - #io.savemat(file_path, matdict) - - - if np.array_equal(expected_cdata[:,0],vhdl_cdata): - print("VHDL Matched Python!") - print("Test Passed!") - return True - else: - print("Data Did not match!") - return False - - - return post_check - -def tb_vu_trwosdf_vfmodel_setup(ui): - - testbench=ui.test_bench("tb_vu_trwosdf_vfmodel") - use_reorder = True - in_dat_w = 18 - out_dat_w = 18 - stage_dat_w = 18 - guard_w = 0 - twiddle_width = 18 - fftsize_log2 = 13 - - do_rounding = 1 - do_saturation = 1 - enable_pattern = 2 #every other clock - # Decode some of those for VHDL - if do_rounding==1: - use_round = "ROUND" - use_mult_round = "ROUND" - if do_saturation==1: - ovflw_behav = "SATURATE" - - scale_sched = 0 - for stage in range(0,fftsize_log2): - if (stage % 2)==1: - scale_sched = scale_sched + 2**stage - # scale at Stage 0 - if stage==0: - scale_sched = scale_sched + 2**stage - if stage==2: - scale_sched = scale_sched + 2**stage - - - d_indices = np.arange(0,2*(2**fftsize_log2)) - # Generate a full scale cw with 12-bits - data = 2047*np.exp(1.0j * 2*np.pi * d_indices*(-2e9/7e9)) - noise = np.random.normal(0, 5.5, size=(data.shape[0])) - data = data + noise - data = roundsat(data,1,in_dat_w,0,1,1,1) - - enable_pattern = 0 - testbench.add_config( - pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), - name=f"FFTR2SDF_E0_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", - generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) - enable_pattern = 1 - testbench.add_config( - pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), - name=f"FFTR2SDF_Erandom_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", - generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) - enable_pattern = 2 - testbench.add_config( - pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), - name=f"FFTR2SDF_E10Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", - generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) - enable_pattern = 3 - testbench.add_config( - pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), - post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), - name=f"FFTR2SDF_E100Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", - generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) - - - - # name=f"TwiddleMagic_w{bidx}b_{fftsize}", - # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), - # post_check=make_twiddle_post_check(fftsize,bidx,1)) - -def main(): - print("There is no main..") - #fftsize = 8192 - #g_twiddle_width = 18 - #g_do_rounding = 1 - #g_do_saturation = 1 - #g_output_width = np.asarray([18,18,18,18,18,18,27,27,27,27,27,27,27]) - #g_bits_to_round_off = np.asarray([0,0,0,0,0,0,0,0,0,0,0,0,0]) - #g_do_dif = 1 - #g_do_bit_rev_input = 0 - #g_do_bit_rev_output = 1 - #d_indices = np.arange(0,fftsize) - #data = 2048*np.exp(1.0j * 2*np.pi * d_indices*(-2e9/7e9)) - #noise = np.random.normal(0, 2.5, size=(data.shape[0])) - #data = data + noise - #plt.ion() - #plt.figure(0) - #plt.plot(np.real(data)) - #plt.title("Input Data (time domain)") - #plt.show - #data = roundsat(data,1,17,0,1,1,1) - #pfft_data = pfft(data,int(np.log2(fftsize)),g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,g_do_dif,g_do_bit_rev_input,g_do_bit_rev_output) - #plt.figure(1) - #plt.plot(20*np.log10(np.fft.fftshift(np.abs(pfft_data)))) - #plt.title("FFT model") - #plt.show - - #plt.figure(2) - #plt.plot(20*np.log10(np.fft.fftshift(np.abs(np.fft.fft(data))))) - #plt.title("Python FFT") - #plt.show - - # debug the python check function thing. - #testfunc = make_fft_postcheck(True,18,18,18,0,18,13,1,1,2735) - #testfunc("/export/home/creon/mschiller_ngvla_project/casper_dspdevel/r2sdf_fft/vunit_out/test_output/r2sdf_fft_lib.tb_vu_trwosdf_vfmodel.FFTR2SDF_s13_reorderTrue_din18_dout18_stagew18_guardw0_doround1_dosaturation1_scale2735_aaafd13ba5b88b5fe244b522b98c37de917678fb") - - -if __name__=="__main__": - main() \ No newline at end of file diff --git a/r2sdf_fft/run.py b/r2sdf_fft/run.py index 7346d790..53c4a80f 100644 --- a/r2sdf_fft/run.py +++ b/r2sdf_fft/run.py @@ -1,8 +1,244 @@ from vunit import VUnit, VUnitCLI from os.path import join, abspath, split from vunit.sim_if.factory import SIMULATOR_FACTORY +from casper_r2sdf_fft import twiddle_gen,pfft,roundsat +from pathlib import Path +import numpy as np # Create VUnit instance by parsing command line arguments +def make_twiddle_post_check(fftsize, g_twiddle_width,use_vhdl_magic_file): + """ + Return a check function to verify test case output + """ + + def post_check(output_path): + # generate the expected twiddles for this case + # Note if you put a magic file into revision control for a twiddle size, it will then trust + # that size is correct, if you change the twiddle generation you'll need to delete the old magic files! + twiddles=(2**(g_twiddle_width-1))*twiddle_gen(fftsize,g_twiddle_width,1,1,use_vhdl_magic_file,Path(output_path)) + + output_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{fftsize}.txt" + data = np.loadtxt(output_file,dtype="int") + print("Post check: %s" % str(output_file)) + cdata = data[0:data.size:2]+1j*data[1:data.size:2] + + if np.array_equal(cdata,twiddles): + print('Twiddles are exactly the same!') + return True + else: + diffreal=np.abs(np.real(twiddles)-np.real(cdata)) + diffimag=np.abs(np.imag(twiddles)-np.imag(cdata)) + if np.max(diffreal)>1: + print("Twiddle Real Values are more than 1 different!"); + return False + if np.max(diffimag)>1: + print("Twiddle Imag Values are more than 1 different!"); + return False + print("Twiddle Values were +/- 1 from expected!") + # these line can help create the magic files if left uncommented but shouldn't be uncommented normally + #import shutil + #shutil.copy2(output_file,os.path.realpath(os.path.dirname(__file__))) + return True + + return post_check +def tb_twiddle_package_setup(ui): + + testbench=ui.test_bench("tb_vu_twiddlepkg") + for fftsizelog2 in range(1,16): # this was originally 1,21 and passed on March 24, 2023, but reduced to make execution faster + for bidx in range(16,19): #this was originally 12,26, but to save time was converted to16:19 + fftsize=2**fftsizelog2 + testbench.add_config( + name=f"TwiddlePython_w{bidx}b_{fftsize}", + generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + post_check=make_twiddle_post_check(fftsize,bidx,0)) + #testbench.add_config( + # name=f"TwiddleMagic_w{bidx}b_{fftsize}", + # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + # post_check=make_twiddle_post_check(fftsize,bidx,1)) + + + +def make_fft_preconfig(g_fftsize_log2, g_in_dat_w,scale_sched,data): + """ + Return a precheck function that will generate input data. + """ + + def pre_config(output_path): + output_file = Path(output_path) / f"input_data.txt" + f = open(output_file,'w') + header = np.zeros(8,dtype=np.uint32) + header[0] = 2**g_fftsize_log2 + header[1] = g_in_dat_w + header[2] = data.size + header[3] = scale_sched + header[7] = 2122219905 + np.savetxt(f,header,fmt='%u') + data_to_write = np.zeros(2*data.size,dtype=np.int32) + data_to_write[0::2] = np.real(data) + data_to_write[1::2] = np.imag(data) + + np.savetxt(f,data_to_write,fmt='%d') + f.close() + return True + return pre_config + +def make_fft_postcheck(g_use_reorder,g_in_dat_w,g_out_dat_w,g_stage_dat_w,g_guard_w,g_twiddle_width,g_fftsize_log2,g_do_rounding,g_do_saturation,scale_sched): + """ + Return a precheck function that will generate input data. + """ + + def post_check(output_path): + # Read the data created by the pre_config script + input_file = Path(output_path) / f"input_data.txt" + input_data = np.loadtxt(input_file,dtype="int") + header = input_data + input_cdata = input_data[8:input_data.size:2]+1j*input_data[9:input_data.size:2] + if header[0] != (2**g_fftsize_log2): + print("Bad Header in input data") + return False + if header[1] != (g_in_dat_w): + print("Input Data width mismatch") + return False + if header[2] != input_cdata.size: + print("Input Data size mismatch") + return False + if header[3] != scale_sched: + print("Input Data Scale Mismatch") + return False + if header[7] != 2122219905: + print("Input Data Magic Word Mismatch") + return False + + # Read the stage data files (if they exist) + #stage_data = np.zeros((input_cdata.size,g_fftsize_log2+1),dtype=np.complex128) + #for stageidx in range(0,g_fftsize_log2+1): + #stage_file = Path(output_path) / f"stage_data{stageidx}.txt" + #data = np.loadtxt(stage_file,dtype="int32") + #stage_cdata = data[0:data.size:2]+1j*data[1:data.size:2] + #stage_data[:,stageidx] = stage_cdata + + + output_file = Path(output_path) / f"output_data.txt" + data = np.loadtxt(output_file,dtype="int32") + print("Post check: %s" % str(output_file)) + vhdl_cdata = data[0:data.size:2]+1j*data[1:data.size:2] + if input_cdata.shape != vhdl_cdata.shape: + print("Fft Post check: Unexpected Data length") + return False + import shutil + # Copy the download twiddle lookup tables into the script directory so they get used. + #for twididx in range(0,g_fftsize_log2): + # twid_size = 2**twididx + # twid_file = Path(output_path) / f"twiddlepkg_twidth{g_twiddle_width}_fftsize{twid_size}.txt" + # shutil.copy2(twid_file,os.path.realpath(os.path.dirname(__file__))) + + # VHDL only support DIF, and is configured to do bitrev + if g_use_reorder==True: + do_output_bit_rev = 1 + else: + do_output_bit_rev = 0 + g_bits_to_round_off = np.zeros(g_fftsize_log2) + g_output_width = g_out_dat_w * np.ones(g_fftsize_log2) + for bit_idx in range(0,g_fftsize_log2): + bit = (scale_sched >> bit_idx) & 1 + if bit==1: + g_bits_to_round_off[bit_idx]=1 + else: + g_bits_to_round_off[bit_idx]=0 + + expected_cdata,stagedebug=pfft(input_cdata,g_fftsize_log2,g_twiddle_width,g_do_rounding,g_do_saturation,g_output_width,g_bits_to_round_off,1,0,do_output_bit_rev,Path(output_path)) + + file_path = Path(output_path) / f"matdata_debug.mat" + matdict = {} + matdict['expected_cdata'] = expected_cdata + matdict['stagedebug'] = stagedebug + matdict['vhdl_cdata'] = vhdl_cdata + #matdict['stage_data'] = stage_data + matdict['input_cdata'] = input_cdata + #io.savemat(file_path, matdict) + + + if np.array_equal(expected_cdata[:,0],vhdl_cdata): + print("VHDL Matched Python!") + print("Test Passed!") + return True + else: + print("Data Did not match!") + return False + + + return post_check + +def tb_vu_trwosdf_vfmodel_setup(ui): + + testbench=ui.test_bench("tb_vu_trwosdf_vfmodel") + use_reorder = True + in_dat_w = 18 + out_dat_w = 18 + stage_dat_w = 18 + guard_w = 0 + twiddle_width = 18 + fftsize_log2 = 13 + + do_rounding = 1 + do_saturation = 1 + enable_pattern = 2 #every other clock + # Decode some of those for VHDL + if do_rounding==1: + use_round = "ROUND" + use_mult_round = "ROUND" + if do_saturation==1: + ovflw_behav = "SATURATE" + + scale_sched = 0 + for stage in range(0,fftsize_log2): + if (stage % 2)==1: + scale_sched = scale_sched + 2**stage + # scale at Stage 0 + if stage==0: + scale_sched = scale_sched + 2**stage + if stage==2: + scale_sched = scale_sched + 2**stage + + + d_indices = np.arange(0,2*(2**fftsize_log2)) + # Generate a full scale cw with 12-bits + data = 2047*np.exp(1.0j * 2*np.pi * d_indices*(-2e9/7e9)) + noise = np.random.normal(0, 5.5, size=(data.shape[0])) + data = data + noise + data = roundsat(data,1,in_dat_w,0,1,1,1) + + enable_pattern = 0 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E0_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 1 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_Erandom_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 2 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E10Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + enable_pattern = 3 + testbench.add_config( + pre_config=make_fft_preconfig(fftsize_log2,in_dat_w,scale_sched,data), + post_check=make_fft_postcheck(use_reorder,in_dat_w,out_dat_w,stage_dat_w,guard_w,twiddle_width,fftsize_log2,do_rounding,do_saturation,scale_sched), + name=f"FFTR2SDF_E100Clocks_s{fftsize_log2}_reorder{use_reorder}_din{in_dat_w}_dout{out_dat_w}_stagew{stage_dat_w}_guardw{guard_w}_doround{do_rounding}_dosaturation{do_saturation}_scale{scale_sched}", + generics=dict(g_use_reorder=use_reorder,g_in_dat_w=in_dat_w,g_out_dat_w=out_dat_w,g_stage_dat_w=stage_dat_w,g_guard_w=guard_w,g_twiddle_width=twiddle_width,g_fftsize_log2=fftsize_log2,g_ovflw_behav=ovflw_behav,g_use_round=use_round,g_use_mult_round=use_mult_round,g_enable_pattern=enable_pattern)) + + + + # name=f"TwiddleMagic_w{bidx}b_{fftsize}", + # generics=dict(g_twiddle_width=bidx,g_fftsize_log2=fftsizelog2), + # post_check=make_twiddle_post_check(fftsize,bidx,1)) + cli = VUnitCLI() cli.parser.add_argument('--twid',action = 'store_true',help = 'Run the Twiddle Tests') @@ -153,11 +389,11 @@ # Setup the Twiddle Testbench by calling it's python function if args.twid: r2sdf_fft_lib.add_source_file(join(script_dir,"tb_vu_twiddlepkg.vhd")) - from r2sdf_fft_py import tb_twiddle_package_setup + #from r2sdf_fft_py import tb_twiddle_package_setup tb_twiddle_package_setup(r2sdf_fft_lib) if args.bitaccurate: r2sdf_fft_lib.add_source_file(join(script_dir,"tb_vu_rtwosdf_vfmodel.vhd")) - from r2sdf_fft_py import tb_vu_trwosdf_vfmodel_setup + #from r2sdf_fft_py import tb_vu_trwosdf_vfmodel_setup tb_vu_trwosdf_vfmodel_setup(r2sdf_fft_lib) TB_GENERATED = r2sdf_fft_lib.test_bench("tb_tb_vu_rTwoSDF") diff --git a/xilinx/xpm_vhdl b/xilinx/xpm_vhdl index e541ac5a..d3015343 160000 --- a/xilinx/xpm_vhdl +++ b/xilinx/xpm_vhdl @@ -1 +1 @@ -Subproject commit e541ac5a6abb3512e06a7196ea63ba5ec312f814 +Subproject commit d3015343daa07dc4e002e7351c9f48babf9cf1a0