| #!/usr/bin/env python3 | |
| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Note: this script relies on numpy and matplotlib, which both currently are | |
| # not standard python requirements of OpenTitan. If you wish to use this script, | |
| # make sure you install them with "pip3 install --user numpy matplotlib" | |
| import argparse | |
| import re | |
| import sys | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| USAGE = """ | |
| This script parses reports generated with the sweep.tcl DC synthesis | |
| script and creates an AT-plot with that data. | |
| usage: ./at-plot.py ./REPORTS/<dut1_basename> ./REPORTS/<dut2_basename> ... | |
| In order to get convert area into gate equivalents (GE) the size of a GE | |
| can be supplied using the -g switch. | |
| The script currently supports plotting up to 10 different result series in | |
| one plot. | |
| """ | |
| def main(): | |
| parser = argparse.ArgumentParser(prog="") | |
| parser.add_argument('result_series', | |
| type=str, | |
| nargs='+', | |
| help="Path and basename of result series.") | |
| parser.add_argument( | |
| '-g', | |
| '--gate_equivalent', | |
| type=float, | |
| default=1.0, | |
| help="Gate equivalent in square micrometre. Defaults to 1.0.") | |
| parser.add_argument('-o', | |
| '--output', | |
| type=str, | |
| default="at-plot.png", | |
| help='Filename of at plot. Defaults to "at-plot.png"') | |
| parser.add_argument('-t', | |
| '--title', | |
| type=str, | |
| default="AT Plot", | |
| help='Title of AT plot.') | |
| parser.add_argument('-l', | |
| '--labels', | |
| type=str, | |
| default="", | |
| help='Comma separated labels for plots.') | |
| parser.add_argument('--semilogy', | |
| action='store_true', | |
| help='semilogy plot.') | |
| l = 0 | |
| labels = [] | |
| # line style and color setup | |
| linestyles = ['--', '--', '--', '--', '--', '--', '--', '--', '--', '--'] | |
| markers = ['^', 'd', '.', '*', '>', '<', 'v', 's', 'h', 'o'] | |
| cmap = plt.get_cmap("Set1") | |
| args = parser.parse_args() | |
| if len(args.result_series) > 10: | |
| print("Only up to ten result series can be plottet at once") | |
| sys.exit(1) | |
| print("Gate Equivalent is %.2f square micrometre" % args.gate_equivalent) | |
| for basename in args.result_series: | |
| report_filebase = Path(basename) | |
| print("%s" % (report_filebase.name)) | |
| results = np.array([[]]) | |
| for rpt_area in report_filebase.parent.rglob(report_filebase.name + | |
| '_*_area.rpt'): | |
| tmp_period = Path(rpt_area).name.split('_area') | |
| tmp_period = tmp_period[0].split(report_filebase.name + '_') | |
| row = np.array([[float(tmp_period[1]), 0.0, 0.0]]) | |
| if np.any(results): | |
| results = np.append(results, row, axis=0) | |
| else: | |
| results = row | |
| try: | |
| with open(rpt_area, 'r') as f: | |
| full_file = f.read() | |
| tmp_area = re.findall(r"^Total cell area:.*", | |
| full_file, | |
| flags=re.MULTILINE) | |
| if tmp_area: | |
| tmp_area = tmp_area[0].split("Total cell area:") | |
| results[-1, 1] = float( | |
| tmp_area[1]) / args.gate_equivalent | |
| else: | |
| print("Error, could not find total cell area in %s" % | |
| (report_area)) | |
| sys.exit(1) | |
| except IOError as e: | |
| print(str(e)) | |
| sys.exit(1) | |
| try: | |
| rpt_timing = rpt_area.parent.joinpath( | |
| rpt_area.name.replace('_area', '_timing')) | |
| with open(rpt_timing, 'r') as f: | |
| full_file = f.read() | |
| tmp_slack = re.findall(r"^ slack \(.*", | |
| full_file, | |
| flags=re.MULTILINE) | |
| if tmp_slack: | |
| tmp_slack = tmp_slack[0].split(")") | |
| # adjust period with slack | |
| results[-1, 2] = results[-1][0] - float(tmp_slack[1]) | |
| else: | |
| print("Error, could not find slack in %s" % | |
| (rpt_timing)) | |
| sys.exit(1) | |
| except IOError as e: | |
| print(str(e)) | |
| sys.exit(1) | |
| if np.any(results): | |
| results = results[np.argsort(results[:, 0])] | |
| if args.gate_equivalent == 1.0: | |
| print("constraint [ns], achieved [ns], complexity [GE]") | |
| else: | |
| print("constraint [ns], achieved [ns], complexity [um^2]") | |
| for k in range(len(results)): | |
| print("%.2f, %.2f, %.2f" % | |
| (results[k][0], results[k][2], results[k][1])) | |
| if args.semilogy: | |
| plt.semilogy(results[:, 2], | |
| results[:, 1], | |
| color=cmap(l), | |
| linestyle=linestyles[l], | |
| marker=markers[l], | |
| linewidth=1.5, | |
| markersize=6, | |
| markeredgecolor='k') | |
| else: | |
| plt.plot(results[:, 2], | |
| results[:, 1], | |
| color=cmap(l), | |
| linestyle=linestyles[l], | |
| marker=markers[l], | |
| linewidth=1.5, | |
| markersize=6, | |
| markeredgecolor='k') | |
| l += 1 | |
| labels += [report_filebase.name] | |
| print("Parsed %d result series" % l) | |
| plt.xlabel('Period [ns]') | |
| if args.gate_equivalent == 1.0: | |
| plt.ylabel('Complexity [um^2]') | |
| else: | |
| plt.ylabel('Complexity [GE]') | |
| plt.grid(which='both') | |
| if args.labels: | |
| plt.legend(args.labels.split(',')) | |
| else: | |
| plt.legend(labels) | |
| plt.title(args.title) | |
| plt.savefig(args.output) | |
| plt.show() | |
| if __name__ == '__main__': | |
| main() |