Files
willtools-demo/panellift/tests/test_plots.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

238 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""The figures have to be the data, and the data has to be readable.
Two complaints from the engineer using this, both about the same thing --
a picture that does not answer a question is decoration on a calculation:
1. The free body's load was the same drawing on every piece. It was: the
tall stroke at each station came out of `beam_y - 30 - h + h`, so the
load height cancelled itself out and the one stroke that DID scale was
drawn inside the constant one. A panel with a run of openings looked
exactly like a solid slab. `test_two_load_profiles_draw_differently` is
the guard.
2. Shear, moment and stress were curves with a peak label and nothing else,
so reading a value at a station meant measuring the page with your eye.
They now carry their own samples and the cursor reads them off, in the
units the engineer states them in: kip, kip-ft, psi.
The rule underneath both: the report never shows a number the engine did not
compute. The hover values are the plotted samples, converted once at the point
of display, written onto the SVG -- not recalculated in the browser.
"""
import re
from conftest import aspire_pockets, make_panel
from panellift import checks, layout, report, statics
from panellift.model import Blockout
def _analyse(panel, stage, rigging):
return statics.analyse(panel, stage, rigging)
def _svg_of(page, title_fragment):
"""The one <svg> whose title contains this fragment."""
i = page.index(title_fragment)
return page[page.rindex('<svg', 0, i):page.index('</svg>', i)]
# --------------------------------------------------- 1. the free body is data
def test_the_load_band_steps_where_the_section_steps(stripping, rolling):
"""A blockout takes width out of the piece, so w drops over it. The
drawing has to drop over it too, or it is not the free body of anything.
"""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
svg = _svg_of(report.free_body(a), 'Free body')
band = re.search(r'<path class="loadband" d="([^"]+)"', svg).group(1)
ys = set(re.findall(r',([0-9.]+)', band))
assert len(ys) >= 3 # beam line, full load, reduced load
def test_two_load_profiles_draw_differently(stripping, rolling):
"""The complaint, as a test. Same piece, same pick, same everything but
the openings -- and before this the two free bodies were identical."""
solid = _analyse(make_panel(), stripping, rolling)
pocketed = _analyse(make_panel(blockouts=aspire_pockets()),
stripping, rolling)
a = _svg_of(report.free_body(solid), 'Free body')
b = _svg_of(report.free_body(pocketed), 'Free body')
band_a = re.search(r'<path class="loadband" d="([^"]+)"', a).group(1)
band_b = re.search(r'<path class="loadband" d="([^"]+)"', b).group(1)
assert band_a != band_b
# and the arrows, which are what makes the band read as a load
arrows_a = re.findall(r'<path class="load" d="([^"]+)"', a)
arrows_b = re.findall(r'<path class="load" d="([^"]+)"', b)
assert arrows_a != arrows_b
def test_a_uniform_piece_draws_one_flat_band(stripping, rolling):
"""The other half of the same guarantee: an unbroken piece must not be
given a shape it does not have."""
a = _analyse(make_panel(), stripping, rolling)
segs = report.load_segments(a.profile)
assert len(segs) == 1
svg = _svg_of(report.free_body(a), 'Free body')
assert 'plf factored' in svg
assert ' to ' not in re.search(r'<text class="note"[^>]*>([^<]*)<',
svg).group(1)
def test_the_segments_carry_their_own_magnitudes(stripping, rolling):
"""Per-segment labels, in the unit the profile is in -- plf, factored."""
panel = make_panel(blockouts=[
Blockout(name='B1', x0=8.0, y0=0.0, length=10.0, width=4.0)])
a = _analyse(panel, stripping, rolling)
svg = _svg_of(report.free_body(a), 'Free body')
labels = re.findall(r'<text class="loadlab"[^>]*>([^<]*)</text>', svg)
assert len(labels) >= 2
assert len(set(labels)) >= 2 # not the same number twice
note = re.search(r'<text class="note"[^>]*>([^<]*)<', svg).group(1)
assert 'plf factored' in note and ' to ' in note
def test_the_load_segments_are_the_profile_not_an_average(stripping, rolling):
"""load_segments must return the real w in each run, and cover the piece
end to end -- the free body's numbers are quoted, so they are exact."""
panel = make_panel(blockouts=[
Blockout(name='B1', x0=8.0, y0=0.0, length=10.0, width=4.0)])
a = _analyse(panel, stripping, rolling)
segs = report.load_segments(a.profile)
assert len(segs) == 3
assert segs[0][0] == 0.0
assert abs(segs[-1][1] - panel.length_ft) < 1e-9
for i in range(len(segs) - 1):
assert segs[i][1] == segs[i + 1][0]
assert segs[1][2] < segs[0][2] # the opening lightens the middle
# every value is one the profile actually holds
held = set(a.profile.w)
assert all(w in held for _x0, _x1, w in segs)
# ------------------------------------------------- 2 and 3. hover, and units
def test_the_diagrams_carry_their_samples_in_display_units(stripping,
rolling):
"""Shear in kip, moment in kip-ft, stress in psi, x in feet. Converted
once, here, and written onto the plot -- nothing converts in the browser.
"""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
units = re.findall(r'data-yu="([^"]*)" data-yl="([^"]*)"', page)
# 'fₜ' (real Unicode subscript), not 'f_t': this label lands in an <svg>
# <text> node and in the hover script's textContent, which parse a
# <sub> tag as four literal characters rather than a subscript -- see
# report.notate_plain().
assert units == [('kip', 'V'), ('kip-ft', 'M'), ('psi', 'fₜ')]
assert page.count('data-xu="ft"') == 3
shear_svg = _svg_of(page, 'Shear V(x)')
hy = re.search(r'data-hy="([^"]*)"', shear_svg).group(1).split()
peak_kip = max(abs(float(v)) for v in hy)
peak_lb = max(abs(v) for v in a.diagram.shear_lb)
assert abs(peak_kip - peak_lb / report.LB_PER_KIP) < 0.02
assert peak_kip < peak_lb # it really was divided
moment_svg = _svg_of(page, 'Moment M(x)')
hy = re.search(r'data-hy="([^"]*)"', moment_svg).group(1).split()
peak_kipft = max(abs(float(v)) for v in hy)
assert abs(peak_kipft -
max(abs(m) for m in a.diagram.moment_lbft)
/ report.LB_PER_KIP) < 0.2
assert all(len(v.split('.')[1]) == 1 for v in hy) # 1 dp, consistent
def test_every_hover_sample_has_a_pixel_and_a_value(stripping, rolling):
"""Four arrays of the same length, or the lookup is off by one somewhere
and the tooltip quietly reports the wrong station."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
for svg in re.findall(r'<svg[^>]*class="hoverplot"[^>]*>', page):
got = [len(re.search(k + r'="([^"]*)"', svg).group(1).split())
for k in ('data-hx', 'data-hy', 'data-px', 'data-py')]
assert len(set(got)) == 1 and got[0] > 10
assert re.search(r'data-t="([-0-9.]+)"', svg)
assert re.search(r'data-b="([-0-9.]+)"', svg)
def test_the_stations_are_feet_to_two_decimals(stripping, rolling):
a = _analyse(make_panel(), stripping, rolling)
svg = _svg_of(report.stress_plot(a), 'Tensile stress')
hx = re.search(r'data-hx="([^"]*)"', svg).group(1).split()
assert all(len(v.split('.')[1]) == 2 for v in hx)
assert float(hx[0]) >= 0.0
assert float(hx[-1]) <= round(a.panel.length_ft, 2) + 0.01
def test_the_script_is_present_exactly_once(stripping, rolling):
"""One document, one listener. build() nested inside build_many() must
not bring a second copy with it."""
a = _analyse(make_panel(), stripping, rolling)
found = checks.run_all(a, rolling)
one = report.build(a, rolling, found, recommendation=False)
assert one.count('<script>') == one.count('</script>') == 1
nested = report.build(a, rolling, found, recommendation=False,
standalone=False)
assert '<script>' not in nested
many = report.build_many([(a, rolling, found)],
recommendation=layout.not_searched())
assert many.count('<script>') == many.count('</script>') == 1
assert many.count("querySelectorAll('svg.hoverplot')") == 1
def test_a_page_with_no_script_still_shows_every_curve(stripping, rolling):
"""Hover is additive. Strip the script out and the report is exactly the
report it was -- same curves, same peak rules, same disclosures, and the
print overrides untouched. That is the no-JS and the printed case."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
without = page[:page.index('<script>')]
assert without.count('<svg') == without.count('</svg>') == 5
assert without.count('class="series"') == 3
assert 'kip-ft' in without and 'allowable' in without
assert 'details::details-content' in without # print override intact
assert '<details class="section">' in without # closed by default
assert '<details class="section" open' not in without
assert '.hoverlayer{display:none !important}' in without
def test_hover_values_are_the_values_that_were_plotted(stripping, rolling):
"""The samples on the <svg> are the samples the path was drawn from --
the same decimation, the same points. A tooltip reading off a different
series is a second, unreviewed calculation."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
svg = _svg_of(report.stress_plot(a), 'Tensile stress')
px = re.search(r'data-px="([^"]*)"', svg).group(1).split()
py = re.search(r'data-py="([^"]*)"', svg).group(1).split()
drawn = re.search(r'<path class="series" d="M ([^"]+)"', svg).group(1)
pairs = [seg.strip().split(',') for seg in drawn.split(' L ')]
assert len(pairs) == len(px)
for (dx, dy), hx, hy in zip(pairs, px, py):
assert abs(float(dx) - float(hx)) < 0.06
assert abs(float(dy) - float(hy)) < 0.06
def test_the_stations_are_monotonic_so_the_lookup_can_bisect(stripping,
rolling):
"""The script binary-searches data-px. It may do that only because the
stations come out of a sorted diagram."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
for svg in re.findall(r'<svg[^>]*class="hoverplot"[^>]*>', page):
px = [float(v) for v in
re.search(r'data-px="([^"]*)"', svg).group(1).split()]
assert px == sorted(px)