mirror of https://github.com/openXC7/prjxray.git
519 lines
17 KiB
Python
519 lines
17 KiB
Python
|
|
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
|
||
|
|
|
||
|
|
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']:
|
||
|
|
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 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 = []
|
||
|
|
for wire in 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)
|
||
|
|
ws[cells['R']] = wire_timing.resistance
|
||
|
|
ws[cells['C']] = wire_timing.capacitance
|
||
|
|
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] == '}'
|
||
|
|
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(ipin['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):
|
||
|
|
ws = wb.create_sheet(
|
||
|
|
title="Net {}".format(net['net'].replace('[', '_').replace(']', '_')))
|
||
|
|
|
||
|
|
# 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 main():
|
||
|
|
parser = argparse.ArgumentParser(
|
||
|
|
description="Create timing worksheet for 7-series timing analysis.")
|
||
|
|
|
||
|
|
parser.add_argument('--timing_json', required=True)
|
||
|
|
parser.add_argument('--db_root', required=True)
|
||
|
|
parser.add_argument('--output_xlsx', required=True)
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
with open(args.timing_json) as f:
|
||
|
|
timing = json.load(f)
|
||
|
|
|
||
|
|
db = Database(args.db_root)
|
||
|
|
|
||
|
|
nodes = {}
|
||
|
|
for net in timing:
|
||
|
|
for node in net['nodes']:
|
||
|
|
nodes[node['name']] = node
|
||
|
|
|
||
|
|
timing_lookup = TimingLookup(db, nodes)
|
||
|
|
|
||
|
|
wb = Workbook()
|
||
|
|
summary_ws = wb.get_sheet_by_name(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)
|
||
|
|
|
||
|
|
summary_row = 2
|
||
|
|
for net in timing:
|
||
|
|
if '<' in net['route']:
|
||
|
|
print(
|
||
|
|
"WARNING: Skipping net {} because it has complicated route description."
|
||
|
|
.format(net['net']))
|
||
|
|
continue
|
||
|
|
|
||
|
|
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()
|