# -*- coding: utf-8 -*- """Deviation table: beamcalc vs an Eriksson Beam package's printed results. python beamcalc/tools/compare_eriksson.py [-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()