|  | #!/usr/bin/env python3 | 
|  | # -*- coding: utf-8 -*- | 
|  | # | 
|  | # Copyright (C) 2017-2020  The Project X-Ray Authors. | 
|  | # | 
|  | # Use of this source code is governed by a ISC-style | 
|  | # license that can be found in the LICENSE file or at | 
|  | # https://opensource.org/licenses/ISC | 
|  | # | 
|  | # SPDX-License-Identifier: ISC | 
|  | """ This takes a JSON file generated with write_timing_info.tcl and generates | 
|  | a spreadsheet with the prjxray timing model and compares it with the | 
|  | interconnect timing output from Vivado. | 
|  |  | 
|  | """ | 
|  | import argparse | 
|  | import json | 
|  | from openpyxl import Workbook, utils | 
|  | from prjxray.tile import OutPinTiming, InPinTiming | 
|  | from prjxray.timing import Outpin, Inpin, Wire, Buffer, \ | 
|  | PassTransistor, IntristicDelay, RcElement, PvtCorner | 
|  | from prjxray.math_models import ExcelMathModel | 
|  | from prjxray.db import Database | 
|  | from prjxray.util import OpenSafeFile | 
|  | from prjxray import util | 
|  |  | 
|  | FAST = PvtCorner.FAST | 
|  | SLOW = PvtCorner.SLOW | 
|  |  | 
|  |  | 
|  | class TimingLookup(object): | 
|  | def __init__(self, db, nodes): | 
|  | self.db = db | 
|  | self.grid = db.grid() | 
|  | self.nodes = nodes | 
|  |  | 
|  | def try_find_site_pin(self, site_pin_node, node_idx): | 
|  | site_pin_wire = self.nodes[site_pin_node]['wires'][node_idx]['name'] | 
|  | tile, wire_in_tile = site_pin_wire.split('/') | 
|  |  | 
|  | gridinfo = self.grid.gridinfo_at_tilename(tile) | 
|  |  | 
|  | tile_type = self.db.get_tile_type(gridinfo.tile_type) | 
|  |  | 
|  | for site in tile_type.get_sites(): | 
|  | for site_pin in site.site_pins: | 
|  | if site_pin.wire == wire_in_tile: | 
|  | return site_pin | 
|  |  | 
|  | return None | 
|  |  | 
|  | def find_site_pin(self, site_pin_node, node_idx): | 
|  | site_pin = self.try_find_site_pin(site_pin_node, node_idx) | 
|  |  | 
|  | assert site_pin is not None, site_pin_node | 
|  | return site_pin | 
|  |  | 
|  | def find_pip(self, pip_name): | 
|  | tile, pip = pip_name.split('/') | 
|  |  | 
|  | gridinfo = self.grid.gridinfo_at_tilename(tile) | 
|  |  | 
|  | tile_type = self.db.get_tile_type(gridinfo.tile_type) | 
|  |  | 
|  | return tile_type.get_pip_by_name(pip) | 
|  |  | 
|  | def find_wire(self, wire_name): | 
|  | tile, wire_in_tile = wire_name.split('/') | 
|  |  | 
|  | gridinfo = self.grid.gridinfo_at_tilename(tile) | 
|  |  | 
|  | tile_type = self.db.get_tile_type(gridinfo.tile_type) | 
|  |  | 
|  | return tile_type.wires[wire_in_tile] | 
|  |  | 
|  |  | 
|  | def delays_to_cells(ws, row, delays, cells): | 
|  | cells['FAST_MAX'] = 'E{}'.format(row) | 
|  | cells['FAST_MIN'] = 'F{}'.format(row) | 
|  | cells['SLOW_MAX'] = 'G{}'.format(row) | 
|  | cells['SLOW_MIN'] = 'H{}'.format(row) | 
|  |  | 
|  | if delays is not None: | 
|  | ws[cells['FAST_MAX']] = delays[FAST].max | 
|  | ws[cells['FAST_MIN']] = delays[FAST].min | 
|  | ws[cells['SLOW_MAX']] = delays[SLOW].max | 
|  | ws[cells['SLOW_MIN']] = delays[SLOW].min | 
|  | else: | 
|  | ws[cells['FAST_MAX']] = 0 | 
|  | ws[cells['FAST_MIN']] = 0 | 
|  | ws[cells['SLOW_MAX']] = 0 | 
|  | ws[cells['SLOW_MIN']] = 0 | 
|  |  | 
|  |  | 
|  | def cells_to_delays(cells): | 
|  | return { | 
|  | FAST: IntristicDelay(min=cells['FAST_MIN'], max=cells['FAST_MAX']), | 
|  | SLOW: IntristicDelay(min=cells['SLOW_MIN'], max=cells['SLOW_MAX']), | 
|  | } | 
|  |  | 
|  |  | 
|  | class Net(object): | 
|  | def __init__(self, net): | 
|  | self.net = net | 
|  | self.ipin_nodes = {} | 
|  | self.row = None | 
|  | self.math = ExcelMathModel() | 
|  | self.models = {} | 
|  |  | 
|  | for ipin in net['ipins']: | 
|  | for ipin_node in ipin['node'].strip().split(' '): | 
|  | self.ipin_nodes[ipin_node] = ipin | 
|  |  | 
|  | # Map of wire name to parent node | 
|  | self.wire_to_node = {} | 
|  |  | 
|  | # Map of node name to node | 
|  | self.node_name_to_node = {} | 
|  |  | 
|  | for node in net['nodes']: | 
|  | self.node_name_to_node[node['name']] = node | 
|  | for wire in node['wires']: | 
|  | self.wire_to_node[wire['name']] = node | 
|  |  | 
|  | # Map of (src node, dst wire). | 
|  | self.pips = {} | 
|  | for pip in net['pips']: | 
|  | src_node = self.wire_to_node[pip['src_wire']]['name'] | 
|  | dst_wire = pip['dst_wire'].split('/')[1] | 
|  | self.pips[(src_node, dst_wire)] = pip | 
|  |  | 
|  | if not int(pip['is_directional']): | 
|  | dst_node = self.wire_to_node[pip['dst_wire']]['name'] | 
|  | src_wire = pip['src_wire'].split('/')[1] | 
|  | self.pips[(dst_node, src_wire)] = pip | 
|  |  | 
|  | def extend_rc_tree(self, ws, current_rc_root, timing_lookup, node): | 
|  | rc_elements = [] | 
|  |  | 
|  | # LV nodes have a workaround applied because of a work around in the | 
|  | # pip timing data. | 
|  | is_lv_node = any( | 
|  | wire['name'].split('/')[1].startswith('LV') | 
|  | for wire in node['wires']) | 
|  | for idx, wire in enumerate(node['wires']): | 
|  | wire_timing = timing_lookup.find_wire(wire['name']) | 
|  | ws['A{}'.format(self.row)] = wire['name'] | 
|  | ws['B{}'.format(self.row)] = 'Part of wire' | 
|  |  | 
|  | if wire_timing is not None: | 
|  | cells = {} | 
|  | cells['R'] = 'C{}'.format(self.row) | 
|  | cells['C'] = 'D{}'.format(self.row) | 
|  | if not is_lv_node: | 
|  | ws[cells['R']] = wire_timing.resistance | 
|  | ws[cells['C']] = wire_timing.capacitance | 
|  | else: | 
|  | # Only use first 2 wire RC's, ignore the rest.  It appears | 
|  | # that some of the RC constant was lumped into the switch | 
|  | # timing, so don't double count. | 
|  | if idx < 2: | 
|  | ws[cells['R']] = wire_timing.resistance | 
|  | ws[cells['C']] = wire_timing.capacitance | 
|  | else: | 
|  | ws[cells['R']] = 0 | 
|  | ws[cells['C']] = 0 | 
|  |  | 
|  | rc_elements.append( | 
|  | RcElement( | 
|  | resistance=cells['R'], | 
|  | capacitance=cells['C'], | 
|  | )) | 
|  |  | 
|  | self.row += 1 | 
|  |  | 
|  | wire_rc_node = Wire(rc_elements=rc_elements, math=self.math) | 
|  | self.models[self.row - 1] = wire_rc_node | 
|  |  | 
|  | current_rc_root.set_sink_wire(wire_rc_node) | 
|  |  | 
|  | return wire_rc_node | 
|  |  | 
|  | def descend_route( | 
|  | self, | 
|  | ws, | 
|  | timing_lookup, | 
|  | current_node, | 
|  | route, | 
|  | route_idx, | 
|  | current_rc_root, | 
|  | was_opin=False): | 
|  | """ Traverse the next pip, or recurse deeper. """ | 
|  |  | 
|  | # descend_route should've consumed this token | 
|  | assert route[route_idx] != '}' | 
|  |  | 
|  | while route[route_idx] == '{': | 
|  | # Go deeper | 
|  | route_idx = self.descend_route( | 
|  | ws, | 
|  | timing_lookup, | 
|  | current_node, | 
|  | route, | 
|  | route_idx=route_idx + 1, | 
|  | current_rc_root=current_rc_root, | 
|  | was_opin=was_opin) | 
|  |  | 
|  | next_edge = (current_node, route[route_idx]) | 
|  | route_idx += 1 | 
|  | assert next_edge in self.pips, (next_edge, self.pips.keys()) | 
|  |  | 
|  | pip = self.pips[next_edge] | 
|  | is_backward = self.wire_to_node[ | 
|  | pip['dst_wire']]['name'] == current_node | 
|  | if not is_backward: | 
|  | assert self.wire_to_node[ | 
|  | pip['src_wire']]['name'] == current_node, (current_node, pip) | 
|  |  | 
|  | pip_info = timing_lookup.find_pip(pip['name']) | 
|  | if not is_backward: | 
|  | pip_timing = pip_info.timing | 
|  | current_node = self.wire_to_node[pip['dst_wire']]['name'] | 
|  | else: | 
|  | pip_timing = pip_info.backward_timing | 
|  | current_node = self.wire_to_node[pip['src_wire']]['name'] | 
|  |  | 
|  | ws['A{}'.format(self.row)] = pip['name'] | 
|  |  | 
|  | cells = {} | 
|  | cells['R'] = 'C{}'.format(self.row) | 
|  | cells['C'] = 'D{}'.format(self.row) | 
|  | delays_to_cells( | 
|  | ws, row=self.row, delays=pip_timing.delays, cells=cells) | 
|  | delays = cells_to_delays(cells) | 
|  |  | 
|  | if pip_info.is_pass_transistor: | 
|  | ws['B{}'.format(self.row)] = 'PassTransistor' | 
|  | ws[cells['R']] = pip_timing.drive_resistance | 
|  |  | 
|  | pip_model = PassTransistor( | 
|  | drive_resistance=cells['R'], | 
|  | delays=delays, | 
|  | ) | 
|  | else: | 
|  | ws['B{}'.format(self.row)] = 'Buffer' | 
|  | if pip_timing.drive_resistance is not None: | 
|  | ws[cells['R']] = pip_timing.drive_resistance | 
|  | if pip_timing.drive_resistance == 0 and was_opin: | 
|  | new_site_pin = timing_lookup.try_find_site_pin( | 
|  | current_node, node_idx=0) | 
|  | if new_site_pin is not None: | 
|  | ws[cells['R']] = new_site_pin.timing.drive_resistance | 
|  | else: | 
|  | ws[cells['R']] = 0 | 
|  |  | 
|  | if pip_timing.internal_capacitance is not None: | 
|  | ws[cells['C']] = pip_timing.internal_capacitance | 
|  | else: | 
|  | ws[cells['C']] = 0 | 
|  |  | 
|  | pip_model = Buffer( | 
|  | drive_resistance=cells['R'], | 
|  | internal_capacitance=cells['C'], | 
|  | delays=cells_to_delays(cells), | 
|  | ) | 
|  |  | 
|  | self.models[self.row] = pip_model | 
|  |  | 
|  | current_rc_root.add_child(pip_model) | 
|  | self.row += 1 | 
|  |  | 
|  | if current_node in self.ipin_nodes: | 
|  | assert route[route_idx] in ['}', 'IOB_O_OUT0', 'IOB_T_OUT0'], ( | 
|  | route_idx, | 
|  | route[route_idx], | 
|  | ) | 
|  | route_idx += 1 | 
|  |  | 
|  | node = self.node_name_to_node[current_node] | 
|  |  | 
|  | current_rc_root = self.extend_rc_tree( | 
|  | ws, pip_model, timing_lookup, node) | 
|  |  | 
|  | if current_node in self.ipin_nodes: | 
|  | ipin = self.ipin_nodes[current_node] | 
|  |  | 
|  | cells = {} | 
|  | name = '{} to {}'.format(self.net['opin']['name'], ipin['name']) | 
|  | ws['A{}'.format(self.row)] = ipin['name'] | 
|  | ws['B{}'.format(self.row)] = 'Inpin' | 
|  |  | 
|  | site_pin = timing_lookup.find_site_pin(current_node, node_idx=-1) | 
|  | assert isinstance(site_pin.timing, InPinTiming) | 
|  |  | 
|  | cells = {} | 
|  | cells['C'] = 'D{}'.format(self.row) | 
|  | delays_to_cells( | 
|  | ws, row=self.row, delays=site_pin.timing.delays, cells=cells) | 
|  | delays = cells_to_delays(cells) | 
|  |  | 
|  | ws[cells['C']] = site_pin.timing.capacitance | 
|  | ipin_model = Inpin( | 
|  | capacitance=cells['C'], delays=delays, name=name) | 
|  | self.models[self.row] = ipin_model | 
|  | current_rc_root.add_child(ipin_model) | 
|  | self.row += 1 | 
|  |  | 
|  | #Sum delays only (sum*1000) | 
|  | #Sum delays + capacitive delay | 
|  | # | 
|  | #Total delay (from Vivado) | 
|  | ws['A{}'.format(self.row)] = '{}: {} sum delays'.format( | 
|  | self.net['net'], name) | 
|  | self.row += 1 | 
|  |  | 
|  | ws['A{}'.format(self.row)] = '{}: {}sum delays + cap delay'.format( | 
|  | self.net['net'], name) | 
|  | self.row += 2 | 
|  |  | 
|  | ws['A{}'.format( | 
|  | self.row)] = '{}: {}Total delay (from Vivado)'.format( | 
|  | self.net['net'], name) | 
|  |  | 
|  | ws['E{}'.format(self.row)] = ipin['ic_delays']['FAST_MAX'] | 
|  | ws['F{}'.format(self.row)] = ipin['ic_delays']['FAST_MIN'] | 
|  | ws['G{}'.format(self.row)] = ipin['ic_delays']['SLOW_MAX'] | 
|  | ws['H{}'.format(self.row)] = ipin['ic_delays']['SLOW_MIN'] | 
|  |  | 
|  | self.row += 2 | 
|  |  | 
|  | return route_idx | 
|  | else: | 
|  | return self.descend_route( | 
|  | ws, | 
|  | timing_lookup, | 
|  | current_node, | 
|  | route, | 
|  | route_idx=route_idx, | 
|  | current_rc_root=current_rc_root) | 
|  |  | 
|  | def walk_route(self, ws, timing_lookup): | 
|  | """ Walk route, creating rows in table. | 
|  |  | 
|  | First row will always be the OPIN, followed by the node/wire connected | 
|  | to the OPIN.  After a node/wire is always 1 or more pips. After a pip | 
|  | is always a node/wire.  A terminal node/wire will then reach an IPIN. | 
|  | """ | 
|  |  | 
|  | self.row = 2 | 
|  | ws['A{}'.format(self.row)] = self.net['opin']['wire'] | 
|  |  | 
|  | site_pin = timing_lookup.find_site_pin( | 
|  | self.net['opin']['node'], node_idx=0) | 
|  | assert isinstance(site_pin.timing, OutPinTiming) | 
|  |  | 
|  | ws['B{}'.format(self.row)] = 'Outpin' | 
|  | ws['C{}'.format(self.row)] = site_pin.timing.drive_resistance | 
|  |  | 
|  | cells = {} | 
|  | cells['R'] = 'C{}'.format(self.row) | 
|  |  | 
|  | delays_to_cells( | 
|  | ws, row=self.row, delays=site_pin.timing.delays, cells=cells) | 
|  |  | 
|  | model_root = Outpin( | 
|  | resistance=cells['R'], delays=cells_to_delays(cells)) | 
|  | self.models[self.row] = model_root | 
|  | self.row += 1 | 
|  |  | 
|  | node = self.net['opin']['node'] | 
|  |  | 
|  | tile, first_wire = self.net['opin']['node'].split('/') | 
|  |  | 
|  | route = [r for r in self.net['route'].strip().split(' ') if r != ''] | 
|  | assert route[0] == '{' | 
|  | assert route[1] == first_wire | 
|  |  | 
|  | node = self.node_name_to_node[node] | 
|  | current_rc_root = self.extend_rc_tree( | 
|  | ws, model_root, timing_lookup, node) | 
|  |  | 
|  | self.descend_route( | 
|  | ws, | 
|  | timing_lookup, | 
|  | node['name'], | 
|  | route, | 
|  | route_idx=2, | 
|  | current_rc_root=current_rc_root, | 
|  | was_opin=True) | 
|  |  | 
|  | model_root.propigate_delays(self.math) | 
|  |  | 
|  | model_rows = {} | 
|  |  | 
|  | for row, model in self.models.items(): | 
|  | model_rows[id(model)] = row | 
|  |  | 
|  | for row, model in self.models.items(): | 
|  | rc_delay = model.get_rc_delay() | 
|  | if rc_delay is not None: | 
|  | ws['J{}'.format(row)] = self.math.eval(rc_delay) | 
|  |  | 
|  | downstream_cap = model.get_downstream_cap() | 
|  | if downstream_cap is not None: | 
|  | ws['I{}'.format(row)] = self.math.eval(downstream_cap) | 
|  |  | 
|  | if isinstance(model, Inpin): | 
|  | ipin_results = { | 
|  | 'Name': model.name, | 
|  | 'truth': {}, | 
|  | 'computed': {}, | 
|  | } | 
|  |  | 
|  | ipin_delays = {} | 
|  |  | 
|  | DELAY_COLS = ( | 
|  | ('E', 'FAST_MAX'), | 
|  | ('F', 'FAST_MIN'), | 
|  | ('G', 'SLOW_MAX'), | 
|  | ('H', 'SLOW_MIN'), | 
|  | ) | 
|  |  | 
|  | for col, value in DELAY_COLS: | 
|  | ipin_delays[value] = [] | 
|  | ipin_results['computed'][ | 
|  | value] = '{title}!{col}{row}'.format( | 
|  | title=utils.quote_sheetname(ws.title), | 
|  | col=col, | 
|  | row=row + 2) | 
|  | ipin_results['truth'][value] = '{title}!{col}{row}'.format( | 
|  | title=utils.quote_sheetname(ws.title), | 
|  | col=col, | 
|  | row=row + 4) | 
|  |  | 
|  | rc_delays = [] | 
|  |  | 
|  | for model in model.get_delays(): | 
|  | delays = model.get_intrinsic_delays() | 
|  | if delays is not None: | 
|  | ipin_delays['FAST_MAX'].append(delays[FAST].max) | 
|  | ipin_delays['FAST_MIN'].append(delays[FAST].min) | 
|  | ipin_delays['SLOW_MAX'].append(delays[SLOW].max) | 
|  | ipin_delays['SLOW_MIN'].append(delays[SLOW].min) | 
|  |  | 
|  | if id(model) in model_rows: | 
|  | rc_delays.append('J{}'.format(model_rows[id(model)])) | 
|  |  | 
|  | ws['J{}'.format(row + 1)] = self.math.eval( | 
|  | self.math.sum(rc_delays)) | 
|  |  | 
|  | for col, value in DELAY_COLS: | 
|  | ws['{}{}'.format(col, row + 1)] = self.math.eval( | 
|  | self.math.sum(ipin_delays[value])) | 
|  | ws['{}{}'.format( | 
|  | col, row + 2)] = '=1000*({col}{row} + J{row})'.format( | 
|  | col=col, row=row + 1) | 
|  |  | 
|  | yield ipin_results | 
|  |  | 
|  |  | 
|  | def add_net(wb, net, timing_lookup): | 
|  | replace_underscore = str.maketrans('[]\\:/', '_____') | 
|  | ws = wb.create_sheet( | 
|  | title="Net {}".format(net['net'].translate(replace_underscore))) | 
|  |  | 
|  | # Header | 
|  | ws['A1'] = 'Name' | 
|  | ws['B1'] = 'Type' | 
|  | ws['C1'] = 'RES' | 
|  | ws['D1'] = 'CAP' | 
|  | ws['E1'] = 'FAST_MAX' | 
|  | ws['F1'] = 'FAST_MIN' | 
|  | ws['G1'] = 'SLOW_MAX' | 
|  | ws['H1'] = 'SLOW_MIN' | 
|  | ws['I1'] = 'Downstream C' | 
|  | ws['J1'] = 'Delay from cap' | 
|  |  | 
|  | net_obj = Net(net) | 
|  | yield from net_obj.walk_route(ws, timing_lookup) | 
|  |  | 
|  |  | 
|  | def build_wire_filter(wire_filter): | 
|  | wires_to_include = set() | 
|  |  | 
|  | with OpenSafeFile(wire_filter) as f: | 
|  | for l in f: | 
|  | wire = l.strip() | 
|  | if not wire: | 
|  | continue | 
|  | wires_to_include.add(wire) | 
|  |  | 
|  | def filter_net(net): | 
|  | wires_in_net = set() | 
|  |  | 
|  | for node in net['nodes']: | 
|  | for wire in node['wires']: | 
|  | wires_in_net.add(wire['name']) | 
|  |  | 
|  | return len(wires_in_net & wires_to_include) > 0 | 
|  |  | 
|  | return filter_net | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Create timing worksheet for 7-series timing analysis.") | 
|  |  | 
|  | util.db_root_arg(parser) | 
|  | util.part_arg(parser) | 
|  | parser.add_argument('--timing_json', required=True) | 
|  | parser.add_argument('--output_xlsx', required=True) | 
|  | parser.add_argument( | 
|  | '--wire_filter', | 
|  | help='List of wires that must be present in a net to be output') | 
|  |  | 
|  | args = parser.parse_args() | 
|  |  | 
|  | with OpenSafeFile(args.timing_json) as f: | 
|  | timing = json.load(f) | 
|  |  | 
|  | db = Database(args.db_root, args.part) | 
|  |  | 
|  | nodes = {} | 
|  | for net in timing: | 
|  | for node in net['nodes']: | 
|  | nodes[node['name']] = node | 
|  |  | 
|  | timing_lookup = TimingLookup(db, nodes) | 
|  |  | 
|  | wb = Workbook() | 
|  | summary_ws = wb[wb.sheetnames[0]] | 
|  | summary_ws.title = 'Summary' | 
|  |  | 
|  | summary_ws['A1'] = 'Name' | 
|  |  | 
|  | cols = ['FAST_MAX', 'FAST_MIN', 'SLOW_MAX', 'SLOW_MIN'] | 
|  | cur_col = 'B' | 
|  | for col in cols: | 
|  | summary_ws['{}1'.format(cur_col)] = col | 
|  | cur_col = chr(ord(cur_col) + 1) | 
|  | summary_ws['{}1'.format(cur_col)] = 'Computed ' + col | 
|  | cur_col = chr(ord(cur_col) + 3) | 
|  |  | 
|  | if args.wire_filter: | 
|  | wire_filter = build_wire_filter(args.wire_filter) | 
|  | else: | 
|  | wire_filter = lambda x: True | 
|  |  | 
|  | summary_row = 2 | 
|  |  | 
|  | timing = [net for net in timing if wire_filter(net)] | 
|  | for idx, net in enumerate(timing): | 
|  | if '<' in net['route']: | 
|  | print( | 
|  | "WARNING: Skipping net {} because it has complicated route description." | 
|  | .format(net['net'])) | 
|  | continue | 
|  |  | 
|  | print('Process net {} ({} / {})'.format(net['net'], idx, len(timing))) | 
|  | for summary_cells in add_net(wb, net, timing_lookup): | 
|  | summary_ws['A{}'.format(summary_row)] = summary_cells['Name'] | 
|  |  | 
|  | cur_col = 'B' | 
|  | for col in cols: | 
|  | truth_col = chr(ord(cur_col) + 0) | 
|  | computed_col = chr(ord(cur_col) + 1) | 
|  | error_col = chr(ord(cur_col) + 2) | 
|  | error_per_col = chr(ord(cur_col) + 3) | 
|  | summary_ws['{}{}'.format( | 
|  | truth_col, | 
|  | summary_row)] = '=' + summary_cells['truth'][col] | 
|  | summary_ws['{}{}'.format( | 
|  | computed_col, | 
|  | summary_row)] = '=' + summary_cells['computed'][col] | 
|  | summary_ws['{}{}'.format( | 
|  | error_col, | 
|  | summary_row)] = '={truth}{row}-{comp}{row}'.format( | 
|  | truth=truth_col, comp=computed_col, row=summary_row) | 
|  | summary_ws['{}{}'.format( | 
|  | error_per_col, | 
|  | summary_row)] = '={error}{row}/{truth}{row}'.format( | 
|  | error=error_col, truth=truth_col, row=summary_row) | 
|  |  | 
|  | cur_col = chr(ord(cur_col) + 4) | 
|  |  | 
|  | summary_row += 1 | 
|  |  | 
|  | wb.save(filename=args.output_xlsx) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |