WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s

History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
This commit is contained in:
2026-09-09 08:32:15 -05:00
commit 8e6ee31596
406 changed files with 223435 additions and 0 deletions
+372
View File
@@ -0,0 +1,372 @@
# -*- coding: utf-8 -*-
"""The numbers behind the curves, station by station.
Eriksson Wall prints, after each stage's plots, a summary table of the same
quantities at engineering stations with the governing row bolded. This is our
version of it, and these are the properties that make it worth printing:
1. It carries the columns we actually compute, in the units the engineer
states them in -- ft, kip, kip-ft, psi -- and does NOT carry the ones we
don't. A zero in a capacity column is worse than no column at all,
because somebody would divide by it. Six capacity columns landed
2026-08-31 (M_cr, phi, phi-Mn, M/phi-Mn, phi-Vn, V/phi-Vn); the rest are
still named as absent in the footnote, and a station whose capacity
cannot be computed prints a dash and a REASON -- never a zero, never the
neighbouring station's number.
2. Its rows are the analysis's own stations. Thinning may drop a station
that is only there because the grid is uniform; it may never drop an
opening edge, a pick line, a peak, or the governing station.
3. The governing row is marked once. Not zero times, and not twice --
"which row is the answer" has exactly one answer.
4. It is closed on load like every other section, and it survives the
print overrides, because a printed report that hides its numbers is a
report that says nothing.
"""
import re
from pathlib import Path
import pytest
from conftest import aspire_pockets, make_panel
from panellift import checks, layout, report, statics
from panellift.model import Blockout, Stage
from panellift.units import parse_length
def _analyse(panel, stage, rigging):
return statics.analyse(panel, stage, rigging)
def _table_of(page, after='Station table'):
"""The one <table class="stations"> that follows this heading."""
i = page.index(after)
j = page.index('<table class="stations"', i)
return page[j:page.index('</table>', j) + 8]
def _rows(table):
return re.findall(r'<tr[^>]*>(.*?)</tr>', table[table.index('<tbody>'):])
def _cells(row):
return [re.sub(r'<[^>]+>', '', c)
for c in re.findall(r'<td[^>]*>(.*?)</td>', row)]
# ------------------------------------------------ 1. the columns, converted
def test_the_columns_are_the_ones_we_compute_in_display_units(stripping,
rolling):
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
table = _table_of(report.station_table(a), '<table')
head = re.sub(r'<[^>]+>', '|', table[:table.index('</thead>')])
for column in ('x, ft', 'V, kip', 'M, kip-ft', 'S, in', 'allow, psi',
'/allow', 'note'):
assert column in head, column
assert head.count(', psi') == 3 # f_t, f_c, allow
def test_the_capacity_columns_are_carried_and_the_rest_are_named_absent(
stripping, rolling):
"""The load-bearing honesty of the whole table, in its 2026-08-31 form.
Six capacity columns are computed and carried. The ones that are still
not computed are NAMED in the footnote rather than being quietly missing,
and the sentence that forbids zero-filling them survives.
"""
a = _analyse(make_panel(), stripping, rolling)
html = report.station_table(a)
table = _table_of(html, '<table')
head = table[:table.index('</thead>')]
for carried in ('M<sub>cr</sub>', '&phi;M<sub>n</sub>',
'M/&phi;M<sub>n</sub>', '&phi;V<sub>n</sub>',
'V/&phi;V<sub>n</sub>', '&phi;'):
assert carried in head, carried
footnote = html[html.index('<ul class="basis">'):]
for still_absent in ('V<sub>ci</sub>', 'V<sub>cw</sub>', 'deflection',
'A<sub>v</sub>'):
assert still_absent in footnote, still_absent
assert 'left out rather than filled with zeros' in footnote
# and the footnote does not claim the new ones are missing any more
assert 'This revision computes none of those' not in footnote
def test_every_value_is_the_engines_value_converted_once(stripping, rolling):
"""A row's V and M are diagram.shear_lb / moment_lbft at that station
over LB_PER_KIP -- not a re-solve, not an interpolation."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
points = dict((p.x, p) for p in checks.stress_profile(a))
table = _table_of(report.station_table(a), '<table')
xs = a.diagram.xs
# Column order since the 2026-09-01 reorder (f_se, added later the same
# day, sits after f_c): x=0, M/phi-Mn=1, V/phi-Vn=2, M=3, phi-Mn=4,
# M_cr=5, V=6, phi-Vn=7, phi=8, S=9, f_c=10, f_se=11, f_t/allow=12,
# f_t=13, allow=14, note=15.
for row in _rows(table):
c = _cells(row)
# Column 0 is now feet-inches ("8'-4 1/2\"") per the owner's
# 2026-09-05 ruling, not a bare decimal -- parse it back rather than
# `float()`.
x = parse_length(c[0])
i = min(range(len(xs)), key=lambda j: abs(xs[j] - x))
assert abs(float(c[6].replace(',', ''))
- a.diagram.shear_lb[i] / report.LB_PER_KIP) < 0.01
assert abs(float(c[3].replace(',', ''))
- a.diagram.moment_lbft[i] / report.LB_PER_KIP) < 0.1
# and the stress column is the same profile the plot was drawn from
gov = [r for r in _rows(table) if 'gov' in r][0]
worst = max(points.values(), key=lambda p: p.stress_psi)
assert abs(float(_cells(gov)[13]) - worst.stress_psi) < 0.05
assert _cells(gov)[12] == '{0:.0%}'.format(
worst.stress_psi / stripping.allowable_tension_psi)
def test_a_negative_zero_never_reaches_a_cell(stripping, rolling):
a = _analyse(make_panel(), stripping, rolling)
table = _table_of(report.station_table(a), '<table')
for row in _rows(table):
for cell in _cells(row):
assert not re.match(r'^-0(\.0*)?$', cell), cell
# --------------------------------- 2. what thinning may and may not drop
def test_the_table_is_thinned_but_keeps_every_jump_station(stripping,
rolling):
"""Five pockets in a row: ten edge stations, every one of them a row,
out of two thousand computed."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
assert len(a.stations) >= 2
table = _table_of(report.station_table(a), '<table')
rows = _rows(table)
assert len(rows) < len(a.diagram.xs) / 10 # it really is thinned
shown = [parse_length(_cells(r)[0]) for r in rows]
# 0.01 ft, not 0.005 -- column 0 is now rounded to the nearest 1/8" for
# display, so a round trip through the printed string can be off by up
# to 1/16" (~0.0052 ft) even for the exact same station.
for x in a.stations:
assert any(abs(x - s) < 0.01 for s in shown), x
assert sum('opening edge' in r for r in rows) == len(a.stations)
def test_an_opening_edge_is_shown_from_both_sides(stripping, rolling):
"""The moment is continuous across the edge and S is not. Two rows at the
same station with different S is the section jumping -- the one thing a
uniform grid would step over."""
panel = make_panel(blockouts=[
Blockout(name='B1', x0=10.0, y0=0.0, length=6.0, width=4.0)])
a = _analyse(panel, stripping, rolling)
rows = _rows(_table_of(report.station_table(a), '<table'))
edges = [r for r in rows if 'opening edge' in r]
assert len(edges) == 4 # two edges, both sides
pair = [_cells(r) for r in edges[:2]]
assert pair[0][0] == pair[1][0] # same station
assert pair[0][9] != pair[1][9] # different S (col 9)
def test_the_peaks_and_the_pick_lines_survive_thinning(stripping, rolling):
a = _analyse(make_panel(), stripping, rolling)
rows = _rows(_table_of(report.station_table(a), '<table'))
text = ''.join(rows)
for tag in ('pick line', 'max V', 'min V', 'max M', 'min M'):
assert tag in text, tag
shown = [parse_length(_cells(r)[0]) for r in rows]
for line in a.rig.lines:
assert any(abs(line.x - s) < 0.02 for s in shown), line.x
# the ends of the piece are both there
assert shown[0] == 0.0
assert abs(shown[-1] - a.panel.length_ft) < 0.01
def test_a_supported_stage_names_its_supports_not_pick_lines(stripping,
rolling):
"""Transportation on dunnage is not a lift, and the note column says the
word the rest of that stage uses."""
stage = Stage(name='transportation', multiplier=1.5, fc_psi=5000.0,
supports=[6.0, 26.0])
a = _analyse(make_panel(), stage, rolling)
html = report.station_table(a)
rows = _rows(_table_of(html, '<table'))
assert sum('support' in r for r in rows) == 2
assert 'pick line' not in ''.join(rows)
assert 'every support' in html
def test_the_rule_that_was_applied_is_printed(stripping, rolling):
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
html = report.station_table(a)
kept = len(_rows(_table_of(html, '<table')))
assert '{0} rows out of {1}'.format(kept, len(a.diagram.xs)) in html
assert 'thinned' in html and 'never drop' in html
def test_a_short_diagram_is_not_thinned_at_all(stripping, rolling):
"""The stride is a cap, not a rule: nothing is dropped that there was
room for."""
a = _analyse(make_panel(), stripping, rolling)
a.diagram.xs = a.diagram.xs[:8]
a.diagram.shear_lb = a.diagram.shear_lb[:8]
a.diagram.moment_lbft = a.diagram.moment_lbft[:8]
a.stations = []
rows, _gov = report.station_indices(a, ())
assert [i for i, _tags in rows] == list(range(8))
# --------------------------------------- 3. the governing row, marked once
def test_the_governing_row_is_marked_exactly_once(stripping, rolling):
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
for html in (report.station_table(a),
report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)):
table = _table_of(html, '<table class="stations"')
assert table.count('<tr class="gov">') == 1
assert table.count('governing') == 1
gov = [r for r in _rows(table) if 'gov' in r][0]
assert 'governing' in gov
def test_the_governing_row_is_the_worst_ratio_on_the_page(stripping, rolling):
"""It has to be the same station the verdict, the red dot on the stress
plot and the CRACK finding all point at."""
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
points = checks.stress_profile(a)
worst = max(points, key=lambda p: p.stress_psi)
table = _table_of(report.station_table(a, points), '<table')
gov = _cells([r for r in _rows(table) if 'gov' in r][0])
# 0.01 ft, not 0.005 -- see the note in
# test_the_table_is_thinned_but_keeps_every_jump_station above.
assert abs(parse_length(gov[0]) - worst.x) < 0.01
# f_t/allow is column 12 since the 2026-09-01 reorder, f_se included (see
# the column map at the top of this file's test_every_value_is_the_
# engines_value...)
ratios = [int(_cells(r)[12].rstrip('%')) for r in _rows(table)
if _cells(r)[12].endswith('%')]
assert int(gov[12].rstrip('%')) == max(ratios)
def test_the_mark_is_bold_and_tinted_and_also_a_word(stripping, rolling):
"""Three ways, because a print drops the tint and a monochrome print
drops the colour. The word in the note column survives both."""
a = _analyse(make_panel(), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
assert ('table.stations tr.gov td'
'{background:var(--fillc);font-weight:700}') in page
gov = [r for r in _rows(_table_of(page)) if 'gov' in r][0]
assert 'governing' in gov
def test_every_stage_of_a_multi_stage_report_gets_its_own_table(stripping,
shipping,
rolling):
panel = make_panel(blockouts=aspire_pockets())
cases = [(_analyse(panel, stage, rolling), rolling, [])
for stage in (stripping, shipping)]
for a, rig, _f in cases:
_f.extend(checks.run_all(a, rig))
page = report.build_many(cases, recommendation=layout.not_searched())
assert page.count('<table class="stations"') == 2
assert page.count('<tr class="gov">') == 2
assert page.count('>Station table</summary>') == 2
# ------------------------------------------- 4. collapsed, and it prints
def test_the_section_is_closed_on_load_like_every_other(stripping, rolling):
a = _analyse(make_panel(), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
assert ('<details class="section stationsec"><summary>Station table'
'</summary>') in page
assert '<details class="section stationsec" open' not in page
# nested inside the stage's own disclosure, not beside it
stage_body = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False, standalone=False)
assert 'stationsec' in stage_body
def test_the_table_survives_the_print_overrides(stripping, rolling):
"""A closed <details> is hidden on screen by two different mechanisms in
two generations of engine, and the print block overrides both. The table
must be inside that, and its scroll box must open up on paper."""
a = _analyse(make_panel(), stripping, rolling)
page = report.build(a, rolling, checks.run_all(a, rolling),
recommendation=False)
printed = page[page.index('@media print'):]
printed = printed[:printed.index('</style>')]
assert 'details>*:not(summary){display:block !important}' in printed
assert 'details::details-content' in printed
assert '.tablewrap{overflow:visible !important}' in printed
assert 'table.stations thead{display:table-header-group}' in printed
assert 'details.stationsec{break-inside:auto}' in printed
# and the table is inside a details, so those overrides reach it
assert '<details class="section stationsec">' in page
without = page[:page.index('<script>')]
assert '<table class="stations"' in without # no JS needed at all
# ----------------------------------------------------- 5. a real piece
def _sweep_jobs():
root = Path(__file__).resolve().parents[2] / 'knowledge' / 'job'
if not root.exists():
return []
return sorted(root.glob('*/sweep/jobs/*.job.json'))
def _is_sandwich_job(job):
"""Two or more concrete solids in the layup -- an insulated (or
two-wythe) body, the makeup that used to be refused outright."""
parts = ((job.get('source') or {}).get('solid_parts')) or []
return sum(1 for p in parts if p.get('counted')) >= 2
def test_a_real_job_renders_a_station_table():
"""The fixtures are a published worked example; a real piece is a real
piece. Skipped where the local job library is not checked out.
Renders the first real jobs that ANALYSE, and requires at least one of
them to be an insulated/two-wythe sandwich -- the makeup that was refused
before 2026-09-02 and now runs on the concrete-wythe composite section.
A job that cannot be LIFT-analysed for a reason unrelated to its section
(e.g. an erection stage whose two picked inserts collapse onto one line
across the span -- a rigging-geometry refusal, `statics` ValueError, that
fires on WP100-102 whether the piece is solid or sandwich) is skipped,
not counted as a rendering failure: this test is about the station table,
not about those pieces' rigging.
"""
from panellift import cli
jobs = _sweep_jobs()
if not jobs:
pytest.skip('no local job library')
import json
rendered = 0
sandwich_rendered = 0
for path in jobs:
with open(str(path), 'r', encoding='utf-8') as fh:
job = json.load(fh)
try:
_panel, _rig, cases = cli.analyse_job(job)
except (cli.JobError, ValueError):
# Refused (an unmeasured sandwich) or un-riggable (WP100-style
# one-line erection pick). Neither is a station-table failure.
continue
page = report.build_many(cases, recommendation=layout.not_searched())
assert page.count('<table class="stations"') == len(cases)
assert page.count('<tr class="gov">') == len(cases)
for table in re.findall(
r'<table class="stations".*?</table>', page, re.S):
rows = _rows(table)
assert 8 <= len(rows) <= 200
assert all(len(_cells(r)) == 16 for r in rows)
rendered += 1
if _is_sandwich_job(job):
sandwich_rendered += 1
if rendered >= 3 and sandwich_rendered >= 1:
break
assert rendered >= 3
# the sandwich path is the point of this work: at least one real
# insulated/two-wythe piece must render its table, not be refused.
assert sandwich_rendered >= 1