25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
225 lines
9.7 KiB
Python
225 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Deviation table: beamcalc vs an Eriksson Beam package's printed results.
|
|
|
|
python beamcalc/tools/compare_eriksson.py <job.json> <truth.json> [-o report.md]
|
|
|
|
`truth.json` comes from eriksson_extract.py. Engine stations are matched to
|
|
the printed stations by nearest x within TOL ft; where the engine has no
|
|
station within TOL, the engine is re-run with that station added, so every
|
|
printed row gets a comparison. Prints one Markdown block per table with the
|
|
largest deviations, and a one-line scoreboard (max |dev| per quantity)."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
from beamcalc import model, solve # noqa: E402
|
|
|
|
TOL = 0.03
|
|
|
|
|
|
def pct(a, b):
|
|
if a is None or b is None:
|
|
return None
|
|
if abs(b) < 1e-9:
|
|
return 0.0 if abs(a) < 1e-6 else None
|
|
return 100.0 * (a - b) / abs(b)
|
|
|
|
|
|
def fmt(v, nd=2):
|
|
return '-' if v is None else ('{0:.%df}' % nd).format(v)
|
|
|
|
|
|
def nearest(rows, x, key=lambda r: r.x_ft):
|
|
best = None
|
|
for r in rows:
|
|
if abs(key(r) - x) <= TOL and (best is None or abs(key(r) - x) < abs(key(best) - x)):
|
|
best = r
|
|
return best
|
|
|
|
|
|
def compare(job_path, truth_path):
|
|
truth = json.load(open(truth_path))
|
|
beam = model.load_job(job_path)
|
|
extra = sorted({r['x_ft'] * 12.0 for t in ('flexure', 'shear', 'tension') for r in truth[t]})
|
|
a = solve.analyse(beam, extra_stations_in=extra) if 'extra_stations_in' in solve.analyse.__code__.co_varnames else solve.analyse(beam)
|
|
out = {'tables': {}, 'score': {}}
|
|
|
|
def table(name, rows, pairs):
|
|
# pairs: list of (label, truth_key, engine_fn, nd, is_abs_tol)
|
|
lines = ['| x ft | ' + ' | '.join('%s (engine / Eriksson / dev)' % p[0] for p in pairs) + ' |',
|
|
'|---|' + '---|' * len(pairs)]
|
|
worst = {p[0]: (0.0, None) for p in pairs}
|
|
n = 0
|
|
for tr in rows:
|
|
x = tr['x_ft']
|
|
eng = nearest_engine(name, a, x)
|
|
if eng is None:
|
|
continue
|
|
n += 1
|
|
cells = []
|
|
for label, tkey, efn, nd, atol in pairs:
|
|
tv = tr.get(tkey)
|
|
ev = efn(eng)
|
|
if tv is None:
|
|
cells.append('%s / NC / -' % fmt(ev, nd)); continue
|
|
if name in ('shear',) or tkey == 'mcr12':
|
|
tv, ev = abs(tv), (abs(ev) if ev is not None else None) # the tool prints signed V
|
|
d = pct(ev, tv)
|
|
if tv is not None and ev is not None and atol is not None and abs(ev - tv) <= atol:
|
|
d = 0.0
|
|
if d is not None and abs(d) > worst[label][0]:
|
|
worst[label] = (abs(d), x)
|
|
cells.append('%s / %s / %s' % (fmt(ev, nd), fmt(tv, nd), ('%+.1f%%' % d) if d is not None else '-'))
|
|
lines.append('| %.2f | %s |' % (x, ' | '.join(cells)))
|
|
out['tables'][name] = {'md': '\n'.join(lines), 'n': n, 'worst': worst}
|
|
for label, (w, x) in worst.items():
|
|
out['score']['%s.%s' % (name, label)] = (w, x)
|
|
|
|
table('flexure', truth['flexure'], [
|
|
('Mu', 'mu', lambda e: e.mu, 2, 0.05),
|
|
('phiMn', 'phi_mn', lambda e: e.phi_mn, 2, 0.05),
|
|
('1.2Mcr', 'mcr12', lambda e: e.mcr12, 2, 0.05),
|
|
('c', 'c', lambda e: e.c, 2, 0.01),
|
|
('As', 'as', lambda e: e.as_t, 2, 0.01),
|
|
('d', 'd', lambda e: e.d, 2, 0.01),
|
|
])
|
|
table('shear', truth['shear'], [
|
|
('Vu', 'vu', lambda e: e.r.vu, 1, 0.1),
|
|
('phiVn', 'phi_vn', lambda e: e.r.phi_vn, 1, 0.1),
|
|
('Vc', 'vc', lambda e: e.r.vc, 1, 0.1),
|
|
('Vs prov', 'vs_prov', lambda e: e.r.vs_prov, 1, 0.1),
|
|
('bwd', 'bwd', lambda e: e.r.bwd, 1, 0.1),
|
|
('Av req', 'av_req', lambda e: e.r.av_req, 3, 0.002),
|
|
('f act', 'f_act', lambda e: e.r.f_act, 1, 0.5),
|
|
])
|
|
table('tension', truth['tension'], [
|
|
('f bot', 'f', lambda e: -min(e.f_bot, e.f_top, 0.0), 1, 1.0),
|
|
('Mz', 'mz', lambda e: e.m, 2, 0.05),
|
|
])
|
|
table('comp_total', truth['comp_total'], [
|
|
('f comp', 'f', lambda e: max(e.f_top, e.f_bot, 0.0), 1, 1.0),
|
|
])
|
|
table('comp_sust', truth['comp_sust'], [
|
|
('f comp sus', 'f', lambda e: max(e.f_top_sus, e.f_bot_sus, 0.0), 1, 1.0),
|
|
])
|
|
# deflection rows
|
|
d = a.deflection
|
|
xs = truth.get('defl_x') or []
|
|
dl = ['| stage | case | mult E/T | max E | max T | dev |', '|---|---|---|---|---|---|']
|
|
for tr in truth['deflection']:
|
|
case = tr['case']
|
|
if tr['stage'] == 'Total' or case == 'Total':
|
|
ev = d['total'] if tr['stage'] == 'Total' else None
|
|
m_e = None
|
|
elif tr['stage'].startswith('Start'):
|
|
if case != 'SW':
|
|
continue
|
|
ev = [v / d['start_sw_multiplier'] * d['start_sw_multiplier'] for v in d['start_total']]
|
|
m_e = d['start_sw_multiplier']
|
|
else:
|
|
row = d['rows'].get(case)
|
|
if not row:
|
|
continue
|
|
ev = [row['multiplier'] * v for v in row['elastic']]
|
|
m_e = row['multiplier']
|
|
if ev is None:
|
|
continue
|
|
# engine values are on the engine grid; sample at the printed stations
|
|
ge = _sample(a.grid, ev, [x * 12.0 for x in xs]) if xs else ev
|
|
tv = tr['values']
|
|
te = -max(abs(v) for v in tv) if tv else 0.0
|
|
ee = -max(abs(v) for v in ge) if ge else 0.0
|
|
dev = pct(ee, te)
|
|
dl.append('| %s | %s | %s / %.2f | %.3f | %.3f | %s |' % (
|
|
tr['stage'], case, fmt(m_e), tr['mult'], ee, te, ('%+.1f%%' % dev) if dev is not None else '-'))
|
|
if dev is not None and abs(te) >= 0.01:
|
|
out['score']['defl.%s.%s' % (tr['stage'], case)] = (abs(dev), None)
|
|
out['tables']['deflection'] = {'md': '\n'.join(dl), 'n': len(dl) - 2, 'worst': {}}
|
|
# summary
|
|
s = truth['summary']
|
|
sm = ['| item | engine | Eriksson | dev |', '|---|---|---|---|']
|
|
worst_f = max(a.flex, key=lambda f: f.util) if a.flex else None
|
|
items = [
|
|
('flexure util', worst_f.util if worst_f else None, s.get('flex_util')),
|
|
('phiMn governing', worst_f.phi_mn if worst_f else None, s.get('flex_phi_mn')),
|
|
('Mu governing', worst_f.mu if worst_f else None, s.get('flex_mu')),
|
|
('Av/s max', max(x.r.av_req for x in a.shr) if a.shr else None, s.get('av_s')),
|
|
('defl total down', d['total_down'], s.get('defl_total_down')),
|
|
('defl total up', d['total_up'], s.get('defl_total_up')),
|
|
('defl live down', d['live_down'], s.get('defl_live_down')),
|
|
('comp total max', max(max(r.f_top, r.f_bot) for r in a.service) if a.service else None, s.get('comp_tot_max')),
|
|
('comp sust max', max(max(r.f_top_sus, r.f_bot_sus) for r in a.service) if a.service else None, s.get('comp_sust_max')),
|
|
('tension max', max(-min(r.f_top, r.f_bot) for r in a.service) if a.service else None, s.get('tension_max')),
|
|
]
|
|
for k, side in (('left', 0), ('right', 1)):
|
|
for case in ('deadnc', 'deadc', 'live', 'roof'):
|
|
tv = (s.get('reactions') or {}).get(case)
|
|
ev = a.reactions.get(case)
|
|
if case == 'deadnc' and ev: # the tool's Deadnc includes self weight
|
|
sw = a.reactions.get('self') or [0.0, 0.0]
|
|
ev = [ev[0] + sw[0], ev[1] + sw[1]]
|
|
items.append(('R %s %s' % (case, k), ev[side] if ev else None, tv[side] if tv else None))
|
|
for k in ('left', 'right'):
|
|
b = (s.get('bearing') or {}).get(k) or {}
|
|
items.append(('bearing Vu %s' % k, (a.bearing.get(k) or {}).get('vu'), b.get('vu')))
|
|
items.append(('bearing plain phiVn %s' % k, (a.bearing.get(k) or {}).get('phi_vn_plain'), b.get('phi_vn_plain')))
|
|
st = s.get('stripping') or {}
|
|
if a.stripping:
|
|
items += [('strip Mu', a.stripping.get('mu'), st.get('mu')),
|
|
('strip phiMn', a.stripping.get('phi_mn'), st.get('phi_mn')),
|
|
('strip f tens', a.stripping.get('f_tens'), (st.get('tens') or [None])[0]),
|
|
('strip f comp', a.stripping.get('f_comp'), (st.get('comp') or [None])[0])]
|
|
for label, ev, tv in items:
|
|
dev = pct(ev, tv)
|
|
sm.append('| %s | %s | %s | %s |' % (label, fmt(ev, 3), fmt(tv, 3), ('%+.1f%%' % dev) if dev is not None else '-'))
|
|
if dev is not None:
|
|
out['score']['summary.%s' % label] = (abs(dev), None)
|
|
out['tables']['summary'] = {'md': '\n'.join(sm), 'n': len(items), 'worst': {}}
|
|
return out, a
|
|
|
|
|
|
def _sample(grid, vals, xs):
|
|
out = []
|
|
for x in xs:
|
|
i = min(range(len(grid)), key=lambda k: abs(grid[k] - x))
|
|
out.append(vals[i])
|
|
return out
|
|
|
|
|
|
def nearest_engine(name, a, x):
|
|
if name == 'flexure':
|
|
return nearest(a.flex, x)
|
|
if name == 'shear':
|
|
return nearest(a.shr, x)
|
|
return nearest(a.service, x)
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('job'); ap.add_argument('truth'); ap.add_argument('-o', '--out')
|
|
ap.add_argument('--full', action='store_true', help='print every row, not just the scoreboard')
|
|
args = ap.parse_args(argv)
|
|
out, a = compare(args.job, args.truth)
|
|
md = ['# %s vs Eriksson' % a.beam.mark, '']
|
|
md.append('## Scoreboard (max |dev|)'); md.append('')
|
|
md.append('| quantity | max dev | at x ft |'); md.append('|---|---|---|')
|
|
for k, (w, x) in sorted(out['score'].items(), key=lambda kv: -kv[1][0]):
|
|
md.append('| %s | %.1f%% | %s |' % (k, w, fmt(x)))
|
|
for name, t in out['tables'].items():
|
|
md += ['', '## %s (%d rows)' % (name, t['n']), '', t['md']]
|
|
text = '\n'.join(md)
|
|
if args.out:
|
|
open(args.out, 'w', encoding='utf-8').write(text)
|
|
if args.full:
|
|
print(text)
|
|
else:
|
|
print('\n'.join(md[:md.index('') + 40]) if len(md) > 40 else text)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|