diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 77a4ea8ce181979f6f77d01a7a0b503bbfbdf86d..59649197810c2f3472929ce78d115b08da21b25d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,3 +24,4 @@ make_test: - source setup.sh - source /media/data_hdd/Xilinx/Vivado/2021.1/settings64.sh - source tests/startup.sh -i 210308B0B4F5 -k 192.168.0.12 -p 192.168.2.3:ch2 + - source tests/software_emulator.sh diff --git a/README.md b/README.md index b74fcffecabe65d2bf662714bb4ed9ccd08561a1..0fe370fe68b00d153d2aa90e1b7d54d0837be3dd 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,52 @@ pip install --editable . To install IPbus please see the [IPbus user guide](https://ipbus.web.cern.ch/doc/user/html/software/installation.html). +The software emulator also runs without ipbus installed. +To use a container with preinstalled dependencies, refer to [this section](https://gitlab.cern.ch/cms-etl-electronics/module_test_sw#using-docker) (needs docker installed). + ## Running the code To properly set all paths run `source setup.sh`. -A minimal example of usage of this package is given in `test_tamalero.py`, which can be run as: +### Software only + +A software emulator of ETROC2 has been implemented, and examples of its usage are implemented in `test_ETROC.py`. +The simplest example requests a handful of data words by sending L1As at different threshold values. + +``` bash +ipython3 -i test_ETROC.py +``` + +Which should return something like + +``` bash +Running without uhal (ipbus not installed with correct python bindings) +Sending 10 L1As and reading back data, for the following thresholds: +[203.0, 202.7, 202.4, 202.1, 201.8, 201.5, 201.2, 200.9, 200.6, 200.3] +Threshold at th=203.0mV +Vth set to 203.000000. +('header', {'elink': 0, 'sof': 0, 'eof': 0, 'full': 0, 'any_full': 0, 'global_full': 0, 'l1counter': 1, 'type': 0, 'bcid': 0}) +('trailer', {'elink': 0, 'sof': 0, 'eof': 0, 'full': 0, 'any_full': 0, 'global_full': 0, 'chipid': 25152, 'status': 0, 'hits': 0, 'crc': 0}) +Threshold at th=202.7mV +Vth set to 202.700000. +('header', {'elink': 0, 'sof': 0, 'eof': 0, 'full': 0, 'any_full': 0, 'global_full': 0, 'l1counter': 2, 'type': 0, 'bcid': 0}) +('trailer', {'elink': 0, 'sof': 0, 'eof': 0, 'full': 0, 'any_full': 0, 'global_full': 0, 'chipid': 25152, 'status': 0, 'hits': 0, 'crc': 0}) +Threshold at th=202.4mV +... +``` + +A threshold scan can be run with + +``` bash +ipython3 -i test_ETROC.py -- --vth --fitplots +``` + +The threshold scans will produce S-curves for each pixel. +![](output/pixel_1.png) + +### With a physical Readout Board + +A minimal example of usage of this package with a physical readout board (v1 or v2) is given in `test_tamalero.py`, which can be run as: `ipython3 -i test_tamalero.py` The code is organized similar to the physical objects. @@ -72,6 +113,7 @@ rb_0.connect_KCU(kcu) ``` **Note:** Control hub is now required for using the KCU, as shown in the default `ipb_path` of the KCU (i.e. `"chtcp-2.0://localhost:10203?target=192.168.0.11:50001"` instead of `"ipbusudp-2.0://192.168.0.11:50001"`). `tamalero` won't run otherwise. +Control hub is part of the IPbus package and can be started with e.g. `/opt/cactus/bin/controlhub_start`. We can then configure the RB and get a status of the lpGBT: ``` diff --git a/output/pixel_1.png b/output/pixel_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e88142cf762f3b5fc6a4fb624ac27c54d6175cb0 Binary files /dev/null and b/output/pixel_1.png differ diff --git a/requirements.txt b/requirements.txt index e939ad3ca7c2e43dd998ef014adb24fca2230962..e594c68b6e4a4ac1f58fc856570d7a525cdfe8ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ PyYAML==5.3.1 requests==2.22.0 numpy==1.24.2 +scipy==1.8.0 yahist==1.14.0 mplhep==0.3.26 tqdm==4.64.1 diff --git a/tamalero/KCU.py b/tamalero/KCU.py index 44566a277b80af25c6912501dc5b3c4eee7e2ec7..e65230b26378adc9bb791552595338ee17b51e4c 100644 --- a/tamalero/KCU.py +++ b/tamalero/KCU.py @@ -1,7 +1,10 @@ """ Control board class (KCU105). Depends on uhal. """ -import uhal +try: + import uhal +except ModuleNotFoundError: + print("Running without uhal (ipbus not installed with correct python bindings)") from tamalero.colors import red, green diff --git a/test_ETROC.py b/test_ETROC.py index cd36216ca6a7538b9f4be3912e66e2099d508169..5aaf6bd2703b64d2b96f1589a8a2c6ff057a67c1 100644 --- a/test_ETROC.py +++ b/test_ETROC.py @@ -7,6 +7,7 @@ from scipy.optimize import curve_fit from matplotlib import pyplot as plt import os +import sys import json from yaml import load, dump try: @@ -14,71 +15,6 @@ try: except ImportError: from yaml import Loader, Dumper - -# initiate -ETROC2 = ETROC(usefake=True) # currently using Software ETROC2 (fake) -DF = DataFrame('ETROC2') - -# argsparser -import argparse -argParser = argparse.ArgumentParser(description = "Argument parser") -argParser.add_argument('--test_readwrite', action='store_true', default=False, help="Test simple read/write functionality?") -argParser.add_argument('--vth', action='store_true', default=False, help="Parse Vth scan plots?") -argParser.add_argument('--rerun', action='store_true', default=False, help="Rerun Vth scan and overwrite data?") -argParser.add_argument('--fitplots', action='store_true', default=False, help="Create individual vth fit plots for all pixels?") -args = argParser.parse_args() - - -# ============================== -# === Test simple read/write === -# ============================== -if args.test_readwrite: - print("<--- Test simple read/write --->") - print("Testing read/write to addresses...") - for r in range(16): - for c in range(16): - for n in range(32): - regadr = 'PixR%dC%dCfg%d'%(r,c,n) - ETROC2.wr_adr(regadr, 1) - readval = ETROC2.rd_adr(regadr) - if not(readval == 1): - raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) - for n in range(8): - regadr = 'PixR%dC%dSta%d'%(r,c,n) - ETROC2.wr_adr(regadr, 1) - readval = ETROC2.rd_adr(regadr) - if not(readval == 1): - raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) - print("Test passed.\n") - - print("Testing read/write for shared pixels...") - for n in range(32): - regadr = 'PixR%dC%dCfg%d'%(1,1,n) - ETROC2.wr_adr(regadr, 1) - for r in range(16): - for c in range(16): - readval = ETROC2.rd_adr(regadr) - if not(readval == 1): - raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) - print("Test passed.\n") - - print("Testing read/write with register names...") - with open(os.path.expandvars('$TAMALERO_BASE/address_table/ETROC2.yaml'), 'r') as f: - regnames = load(f, Loader=Loader) - for regname in list(regnames.keys()): - for pix in range(256): - ETROC2.wr_reg(regname, pix, 1) - readval = ETROC2.rd_reg(regname, pix) - if not(readval == 1): - raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) - print("Test passed.\n") - - -# ============================== -# ======= Test Vth scan ======== -# ============================== - - # ====== HELPER FUNCTIONS ====== # run N L1A's and return packaged ETROC2 dataformat @@ -118,13 +54,13 @@ def sigmoid_fit(x_axis, y_axis): def parse_data(data, N_pix): results = np.zeros(N_pix) pix_w = int(round(np.sqrt(N_pix))) - + for word in data: datatype, res = DF.read(word) if datatype == 'data': pix = toPixNum(res['row_id'], res['col_id'], pix_w) results[pix] += 1 - + return results @@ -135,7 +71,7 @@ def vth_scan(ETROC2): vth_step = .25 # step size N_steps = int((vth_max-vth_min)/vth_step)+1 # number of steps N_pix = 16*16 # total number of pixels - + vth_axis = np.linspace(vth_min, vth_max, N_steps) run_results = np.empty([N_steps, N_pix]) @@ -143,138 +79,226 @@ def vth_scan(ETROC2): ETROC2.set_Vth_mV(vth) i = int(round((vth-vth_min)/vth_step)) run_results[i] = parse_data(run(N_l1a), N_pix) - + # transpose so each 1d list is for a pixel & normalize run_results = run_results.transpose()/N_l1a return [vth_axis.tolist(), run_results.tolist()] -# ========= Vth SCAN ========= -if args.vth: - print("<--- Testing Vth scan --->") - - # run only if no saved data or we want to rerun - if (not os.path.isfile("results/vth_scan.json")) or args.rerun: - - # scan - print("No data. Run new vth scan...") - result_data = vth_scan(ETROC2) - - #save - if not os.path.isdir('results'): - os.makedirs('results') - - with open("results/vth_scan.json", "w") as outfile: - json.dump(result_data, outfile) - print("Data saved to results/vth_scan.json\n") - - - # read and parse vth scan data - with open('results/vth_scan.json', 'r') as openfile: - vth_scan_data = json.load(openfile) - - vth_axis = np.array(vth_scan_data[0]) - hit_rate = np.array(vth_scan_data[1]) - - vth_min = vth_axis[0] # vth scan range - vth_max = vth_axis[-1] - N_pix = len(hit_rate) # total # of pixels - N_pix_w = int(round(np.sqrt(N_pix))) # N_pix in NxN layout - - - # ======= PERFORM FITS ======= - - # fit to sigmoid and save to NxN layout - slopes = np.empty([N_pix_w, N_pix_w]) - means = np.empty([N_pix_w, N_pix_w]) - widths = np.empty([N_pix_w, N_pix_w]) - - for pix in range(N_pix): - fitresults = sigmoid_fit(vth_axis, hit_rate[pix]) - r, c = fromPixNum(pix, N_pix_w) - slopes[r][c] = fitresults[0] - means[r][c] = fitresults[1] - widths[r][c] = 4/fitresults[0] - - # print out results nicely - for r in range(N_pix_w): - for c in range(N_pix_w): - pix = toPixNum(r, c, N_pix_w) - print("{:8s}".format("#"+str(pix)), end='') - print("") - for c in range(N_pix_w): - print("%4.2f"%means[r][c], end=' ') - print("") - for c in range(N_pix_w): - print("+-%2.2f"%widths[r][c], end=' ') - print("\n") - - - # ======= PLOT RESULTS ======= - - # fit results per pixel & save - if args.fitplots: - print('Creating plots and saving in ./results/...') - print('This may take a while.') - for expix in range(256): - exr = expix%N_pix_w - exc = int(np.floor(expix/N_pix_w)) - - fig, ax = plt.subplots() - - plt.title("S curve fit example (pixel #%d)"%expix) - plt.xlabel("Vth") - plt.ylabel("hit rate") - - plt.plot(vth_axis, hit_rate[expix], '.-') - fit_func = sigmoid(slopes[exr][exc], vth_axis, means[exr][exc]) - plt.plot(vth_axis, fit_func) - plt.axvline(x=means[exr][exc], color='r', linestyle='--') - plt.axvspan(means[exr][exc]-widths[exr][exc], means[exr][exc]+widths[exr][exc], - color='r', alpha=0.1) - - plt.xlim(vth_min, vth_max) - plt.grid(True) - plt.legend(["data","fit","baseline"]) - - fig.savefig(f'results/pixel_{expix}.png') - plt.close(fig) - del fig, ax - - # 2D histogram of the mean - fig, ax = plt.subplots() - plt.title("Mean values of baseline voltage") - cax = ax.matshow(means) - - fig.colorbar(cax) - ax.set_xticks(np.arange(N_pix_w)) - ax.set_yticks(np.arange(N_pix_w)) - - for i in range(N_pix_w): - for j in range(N_pix_w): - text = ax.text(j, i, "%.2f\n+/-%.2f"%(means[i,j],widths[i,j]), - ha="center", va="center", color="w", fontsize="xx-small") - - fig.savefig(f'results/sigmoid_mean_2D.png') - plt.show() - - plt.close(fig) - del fig, ax - - # 2D histogram of the width - fig, ax = plt.subplots() - plt.title("Width of the sigmoid") - cax = ax.matshow( - widths, - cmap='RdYlGn_r', - vmin=0, vmax=5, - ) - - fig.colorbar(cax) - ax.set_xticks(np.arange(N_pix_w)) - ax.set_yticks(np.arange(N_pix_w)) - - #cax.set_zlim(0, 10) - - fig.savefig(f'results/sigmoid_width_2D.png') - plt.show() +if __name__ == '__main__': + + # initiate + ETROC2 = ETROC(usefake=True) # currently using Software ETROC2 (fake) + DF = DataFrame('ETROC2') + + # argsparser + import argparse + argParser = argparse.ArgumentParser(description = "Argument parser") + argParser.add_argument('--test_readwrite', action='store_true', default=False, help="Test simple read/write functionality?") + argParser.add_argument('--vth', action='store_true', default=False, help="Parse Vth scan plots?") + argParser.add_argument('--rerun', action='store_true', default=False, help="Rerun Vth scan and overwrite data?") + argParser.add_argument('--fitplots', action='store_true', default=False, help="Create individual vth fit plots for all pixels?") + args = argParser.parse_args() + + + # ============================== + # === Test simple read/write === + # ============================== + # FIXME this needs to be fixed for new ETROC2 register table / structure + if args.test_readwrite: + print("<--- Test simple read/write --->") + print("Testing read/write to addresses...") + for r in range(16): + for c in range(16): + for n in range(32): + regadr = 'PixR%dC%dCfg%d'%(r,c,n) + ETROC2.wr_adr(regadr, 1) + readval = ETROC2.rd_adr(regadr) + if not(readval == 1): + raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) + for n in range(8): + regadr = 'PixR%dC%dSta%d'%(r,c,n) + ETROC2.wr_adr(regadr, 1) + readval = ETROC2.rd_adr(regadr) + if not(readval == 1): + raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) + print("Test passed.\n") + + print("Testing read/write for shared pixels...") + for n in range(32): + regadr = 'PixR%dC%dCfg%d'%(1,1,n) + ETROC2.wr_adr(regadr, 1) + for r in range(16): + for c in range(16): + readval = ETROC2.rd_adr(regadr) + if not(readval == 1): + raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) + print("Test passed.\n") + + print("Testing read/write with register names...") + with open(os.path.expandvars('$TAMALERO_BASE/address_table/ETROC2.yaml'), 'r') as f: + regnames = load(f, Loader=Loader) + for regname in list(regnames.keys()): + for pix in range(256): + ETROC2.wr_reg(regname, pix, 1) + readval = ETROC2.rd_reg(regname, pix) + if not(readval == 1): + raise Exception('Test failed for %s, value read was %d.'%(regname,readval)) + print("Test passed.\n") + + + # ============================== + # ======= Test Vth scan ======== + # ============================== + + + + # ========= Vth SCAN ========= + if args.vth: + print("<--- Testing Vth scan --->") + + # run only if no saved data or we want to rerun + if (not os.path.isfile("results/vth_scan.json")) or args.rerun: + + # scan + print("No data. Run new vth scan...") + result_data = vth_scan(ETROC2) + + #save + if not os.path.isdir('results'): + os.makedirs('results') + + with open("results/vth_scan.json", "w") as outfile: + json.dump(result_data, outfile) + print("Data saved to results/vth_scan.json\n") + + + # read and parse vth scan data + with open('results/vth_scan.json', 'r') as openfile: + vth_scan_data = json.load(openfile) + + vth_axis = np.array(vth_scan_data[0]) + hit_rate = np.array(vth_scan_data[1]) + + vth_min = vth_axis[0] # vth scan range + vth_max = vth_axis[-1] + N_pix = len(hit_rate) # total # of pixels + N_pix_w = int(round(np.sqrt(N_pix))) # N_pix in NxN layout + + + # ======= PERFORM FITS ======= + + # fit to sigmoid and save to NxN layout + slopes = np.empty([N_pix_w, N_pix_w]) + means = np.empty([N_pix_w, N_pix_w]) + widths = np.empty([N_pix_w, N_pix_w]) + + for pix in range(N_pix): + fitresults = sigmoid_fit(vth_axis, hit_rate[pix]) + r, c = fromPixNum(pix, N_pix_w) + slopes[r][c] = fitresults[0] + means[r][c] = fitresults[1] + widths[r][c] = 4/fitresults[0] + + # print out results nicely + for r in range(N_pix_w): + for c in range(N_pix_w): + pix = toPixNum(r, c, N_pix_w) + print("{:8s}".format("#"+str(pix)), end='') + print("") + for c in range(N_pix_w): + print("%4.2f"%means[r][c], end=' ') + print("") + for c in range(N_pix_w): + print("+-%2.2f"%widths[r][c], end=' ') + print("\n") + + + # ======= PLOT RESULTS ======= + + # fit results per pixel & save + if args.fitplots: + print('Creating plots and saving in ./results/...') + print('This may take a while.') + for expix in range(256): + exr = expix%N_pix_w + exc = int(np.floor(expix/N_pix_w)) + + fig, ax = plt.subplots() + + plt.title("S curve fit example (pixel #%d)"%expix) + plt.xlabel("Vth") + plt.ylabel("hit rate") + + plt.plot(vth_axis, hit_rate[expix], '.-') + fit_func = sigmoid(slopes[exr][exc], vth_axis, means[exr][exc]) + plt.plot(vth_axis, fit_func) + plt.axvline(x=means[exr][exc], color='r', linestyle='--') + plt.axvspan(means[exr][exc]-widths[exr][exc], means[exr][exc]+widths[exr][exc], + color='r', alpha=0.1) + + plt.xlim(vth_min, vth_max) + plt.grid(True) + plt.legend(["data","fit","baseline"]) + + fig.savefig(f'results/pixel_{expix}.png') + plt.close(fig) + del fig, ax + + # 2D histogram of the mean + fig, ax = plt.subplots() + plt.title("Mean values of baseline voltage") + cax = ax.matshow(means) + + fig.colorbar(cax) + ax.set_xticks(np.arange(N_pix_w)) + ax.set_yticks(np.arange(N_pix_w)) + + for i in range(N_pix_w): + for j in range(N_pix_w): + text = ax.text(j, i, "%.2f\n+/-%.2f"%(means[i,j],widths[i,j]), + ha="center", va="center", color="w", fontsize="xx-small") + + fig.savefig(f'results/sigmoid_mean_2D.png') + plt.show() + + plt.close(fig) + del fig, ax + + # 2D histogram of the width + fig, ax = plt.subplots() + plt.title("Width of the sigmoid") + cax = ax.matshow( + widths, + cmap='RdYlGn_r', + vmin=0, vmax=5, + ) + + fig.colorbar(cax) + ax.set_xticks(np.arange(N_pix_w)) + ax.set_yticks(np.arange(N_pix_w)) + + #cax.set_zlim(0, 10) + + fig.savefig(f'results/sigmoid_width_2D.png') + plt.show() + + else: + thresholds = [203-x*0.3 for x in range(10)] + print("Sending 10 L1As and reading back data, for the following thresholds:") + print(thresholds) + for th in thresholds: + print(f'Threshold at {th=}mV') + ETROC2.set_Vth_mV(th) # anything between 196 and 203 should give reasonable numbers of hits + data = ETROC2.runL1A() # this will spit out data for a single event, with an occupancy corresponding to the previously set threshold + unpacked = [DF.read(d) for d in data] + for d in data: + print(DF.read(d)) + + + if unpacked[-1][1]['hits'] == len(unpacked)-2: + print("Very simple check passed.") + sys.exit(0) + else: + print("Data looks inconsistent.") + sys.exit(1) diff --git a/tests/software_emulator.sh b/tests/software_emulator.sh new file mode 100644 index 0000000000000000000000000000000000000000..7c3354465286b58027c8798d5c97d2a39d150da0 --- /dev/null +++ b/tests/software_emulator.sh @@ -0,0 +1,22 @@ +#! /bin/bash + +#### Predefined variables and functions #### +RED='\033[1;31m' +GREEN='\033[1;32m' +BLUE='\033[1;34m' +NC='\033[0m' + +function info() { echo -e "${BLUE}${@}${NC}"; } +function error() { echo -e "${RED}${@}${NC}"; } +function success() { echo -e "${GREEN}${@}${NC}"; } + +# run test_tamalero with power up +info "Running test_ETROC..." +/usr/bin/env python3 test_ETROC.py +EXIT=$? +if [ ${EXIT} -ne 0 ]; then + error "Failure when running test_ETROC.py; exit code is ${EXIT}. Blocking merge." + return ${EXIT} +else + success "Success! test_ETROC.py exit with code ${EXIT}" +fi