Files
willtools-demo/panellift/tests/test_recess.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

262 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""Blind recesses: carried through the job file, and taken into nothing.
THE DEFECT, fixed on the Revit side 2026-09-01
-----------------------------------------------
`liftexport._openings` reads rectangles off the side faces of the concrete
solid, and a blind pocket has side faces exactly like a hole does. The
exporter emitted every one of them as a `blockout`, and this package reads a
blockout as a void through the thickness: it takes that width out of the
section, CUTS every strand crossing it, and restarts development at its edges.
So a corner pocket 0.29 x 0.67 ft with 3.5 in of concrete behind it, a foot
from the end of a piece, severed fourteen strands that pass a clear 3.5 in
below and above it. `test_the_pocket_as_a_blockout_is_the_defect_itself`
measures what that did, and it is not subtle.
The exporter now splits them: `blockouts` are the through ones, `recesses` are
the blind ones with `concrete_in` and `depth_in`. This side reads the new key,
carries it on the model, counts it in the report -- and uses it in no
calculation whatever. That is an approximation and it is unconservative over
the pocket, so the report says so in a line of its own rather than leaving a
reader to assume the pocket was accounted for.
"""
import pytest
from panellift import capacity, cli, report
from panellift.model import Recess
RECT = {'x0': 11.2083, 'y0': 5.0, 'length': 0.2917, 'width': 0.6667}
# --------------------------------------------------------- reading the key
def test_a_recess_round_trips_through_the_job_file():
"""THE CONTRACT, from this side. The other half is
`WillTools.extension/tests/test_liftexport.py::
test_the_recess_keys_are_the_ones_panellift_reads` -- neither package can
import the other, so a rename under either is caught only by a pin at
each end."""
panel = cli.read_panel({
'mark': 'PNL101', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0,
'recesses': [dict(RECT, name='RECESS 1', concrete_in=3.5,
depth_in=4.5)],
})
assert panel.recesses == [Recess(name='RECESS 1', x0=11.2083, y0=5.0,
length=0.2917, width=0.6667,
concrete_in=3.5, depth_in=4.5)]
assert panel.blockouts == []
def test_a_recess_may_arrive_with_neither_depth_measured():
"""The exporter writes None where the geometry did not answer, and None
has to survive as None -- a 0 would read as a pocket of no depth."""
panel = cli.read_panel({
'mark': 'PNL101', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0,
'recesses': [dict(RECT, name='RECESS 1')],
})
assert panel.recesses[0].concrete_in is None
assert panel.recesses[0].depth_in is None
def test_a_job_file_from_before_the_split_still_reads():
"""Every job file written to date has no `recesses` key at all, including
the 184 of the reference sweep. Absent is an empty list here, and the
report says nothing about recesses on such a piece -- absence is never
rendered as a claim that it was looked for."""
panel = cli.read_panel({
'mark': 'PNL101', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0,
'blockouts': [dict(RECT, name='OPENING 1')],
})
assert panel.recesses == []
assert len(panel.blockouts) == 1
def test_a_recess_missing_a_dimension_is_refused_by_name():
"""Same treatment a blockout gets. A rectangle with no x0 is not a
rectangle at 0."""
with pytest.raises(cli.JobError) as excinfo:
cli.read_panel({
'mark': 'PNL101', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0,
'recesses': [{'name': 'RECESS 1', 'y0': 5.0, 'length': 0.3,
'width': 0.7}],
})
assert 'panel.recesses[0]' in str(excinfo.value)
assert 'x0' in str(excinfo.value)
def test_a_recess_is_turned_with_the_piece():
"""A panel erected on its long edge is transposed rather than taught
about orientation. A pocket left un-turned would be printed where the
piece does not have one."""
panel = cli.read_panel({
'mark': 'PNL101', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0,
'recesses': [dict(RECT, name='RECESS 1', concrete_in=3.5,
depth_in=4.5)],
})
turned = cli.transpose(panel)
assert (turned.recesses[0].x0, turned.recesses[0].y0) == (5.0, 11.2083)
assert (turned.recesses[0].length, turned.recesses[0].width) == \
(0.6667, 0.2917)
# Depth is through the panel either way, like the thickness.
assert turned.recesses[0].depth_in == 4.5
# --------------------------------------------------- WP128, the real shape
#
# 11.5 x 10.667 ft x 8 in, one 0.2917 x 0.6667 ft pocket at x0 11.2083 with
# 3.5 in of concrete behind it, fourteen 1/2 in strands in two layers at
# z 1.75 and 6.25 -- so the pocket is clear of every one of them. The piece is
# nearly square, so it trips ASPECT whatever else is true; that finding is
# unrelated and none of these tests reads the overall verdict.
def wp128_job(**panel_keys):
strands = ([{'y': 5.3333, 'z_in': 1.75, 'area_in2': 0.153,
'initial_pull_lb': 28920.0}] * 7 +
[{'y': 5.3333, 'z_in': 6.25, 'area_in2': 0.153,
'initial_pull_lb': 28920.0}] * 7)
def face(mark, x, y):
return {'mark': mark, 'family': 'TEST_LIFTER', 'x': x, 'y': y,
'capacity_lb': 8000.0, 'shear_capacity_lb': 8000.0,
'tension_fos': 4.0, 'shear_fos': 4.0}
def edge(mark, y):
return {'mark': mark, 'family': 'TEST_EDGE_ANCHOR', 'x': 11.5, 'y': y,
'capacity_lb': 16000.0, 'shear_capacity_lb': 16000.0,
'on_edge': True, 'pinned': True,
'tension_fos': 4.0, 'shear_fos': 4.0}
panel = {
'mark': 'WP128', 'length_ft': 11.5, 'width_ft': 10.6667,
'thickness_in': 8.0, 'unit_weight_pcf': 150.0, 'weight_lb': 12267.0,
'fc_psi': 6000.0, 'fci_psi': 3500.0,
'finish': 'smooth', 'product_type': 'flat',
'lifters': [face('L1', 2.1667, 2.0012), face('L2', 2.1667, 8.6678),
face('L3', 9.3333, 2.0012), face('L4', 9.3333, 8.6678),
edge('E1', 2.0), edge('E2', 8.6667)],
'prestress': {'diameter_in': 0.5, 'loss_pct': 8,
'transfer_length_in': None, 'strands': strands},
}
panel.update(panel_keys)
return {
'schema': 'panellift/1',
'panel': panel,
'rigging': {'mode': 'rolling', 'sling_angle_deg': 60.0,
'group_tolerance_ft': 0.4},
# Erection: the bottom face pair plus the two edge anchors in the end
# the piece stands on. This is the stage the phantom opening failed.
'stages': [{'name': 'erection', 'multiplier': 1.4, 'fc_psi': 6000.0,
'lifters': ['L1', 'L2', 'E1', 'E2']}],
}
THE_POCKET = dict(RECT, name='RECESS 1', concrete_in=3.5, depth_in=4.5)
THE_POCKET_AS_A_HOLE = dict(RECT, name='OPENING 1')
def erection(job):
"""(panel, capacity points, findings) for the one stage."""
panel, _rigging, cases = cli.analyse_job(job)
analysis, _rig, findings = cases[0]
return panel, capacity.profile_for(analysis), findings
def worst_flexural(points):
return max([p.m_over_phi_mn for p in points
if p.m_over_phi_mn is not None] or [None])
def test_the_pocket_carries_no_blockout_at_all():
panel, _points, _findings = erection(
wp128_job(recesses=[THE_POCKET]))
assert panel.blockouts == []
assert len(panel.recesses) == 1
def test_a_recess_restarts_no_strand_development():
"""The assertion this whole fixture exists for. Development restarts at a
free end, and a blockout edge is one; a pocket is not, and no station may
be measured from it."""
_panel, points, _findings = erection(wp128_job(recesses=[THE_POCKET]))
ends = sorted(set(p.free_end for p in points if p.free_end))
assert ends == ['the end of the piece']
def test_a_recess_is_arithmetically_identical_to_solid_concrete():
"""Not merely "does not fail" -- the same numbers, station for station.
That is what "carried and used in nothing" has to mean, and a check that
only looked at the verdict would pass while the section quietly moved."""
_panel, with_pocket, _f = erection(wp128_job(recesses=[THE_POCKET]))
_panel2, solid, _f2 = erection(wp128_job())
assert [p.x for p in with_pocket] == [p.x for p in solid]
assert [p.b_in for p in with_pocket] == [p.b_in for p in solid]
assert [p.m_over_phi_mn for p in with_pocket] == \
[p.m_over_phi_mn for p in solid]
def test_no_station_exceeds_its_flexural_capacity_on_erection():
"""The piece as its model really is: solid, with a pocket in a corner.
The reference sweep reported M/phi-Mn 1.04 on this piece and it was the
phantom hole, not the panel."""
_panel, points, findings = erection(wp128_job(recesses=[THE_POCKET]))
assert worst_flexural(points) < 1.0
assert [f for f in findings if f.code == 'FLEX-CAP' and
f.severity == 'FAIL'] == []
def test_the_pocket_as_a_blockout_is_the_defect_itself():
"""The before picture, kept so the fixture above is proving something.
One key moved and the same 0.29 x 0.67 ft rectangle severs every strand
it stands over, restarts their development at its edges a foot from the
end of the piece, and produces a flexural-capacity FAIL on a panel whose
model has no opening in it."""
_panel, points, findings = erection(
wp128_job(blockouts=[THE_POCKET_AS_A_HOLE]))
assert 'an opening edge, which cuts every strand it crosses' in \
[p.free_end for p in points]
assert worst_flexural(points) > 1.0
assert [f.code for f in findings if f.severity == 'FAIL'
and f.code == 'FLEX-CAP'] == ['FLEX-CAP']
# ------------------------------------------------------- what the page says
def one_page(job):
panel, rigging, cases = cli.analyse_job(job)
analysis, rig, findings = cases[0]
return report.build(analysis, rig, findings, recommendation=False)
def test_the_piece_summary_says_the_recess_is_not_in_the_numbers():
"""A stated approximation. The alternative is a page that counts the
pocket nowhere and mentions it nowhere, which is the silent omission this
codebase treats as the defect rather than the shortcut."""
page = one_page(wp128_job(recesses=[THE_POCKET]))
assert 'Recesses' in page
assert '1 blind recess recorded' in page
assert 'NOT reflected in the section properties or in strand development' \
in page
assert 'treats the piece as solid concrete there' in page
def test_the_count_is_the_count():
page = one_page(wp128_job(recesses=[
THE_POCKET, dict(THE_POCKET, name='RECESS 2', x0=0.5)]))
assert '2 blind recesses recorded' in page
def test_a_piece_with_no_recesses_says_nothing_about_them():
"""Absence is not a claim, and a row saying "Recesses: none" would be one
-- it would read as "we looked", on every job file written before the key
existed."""
page = one_page(wp128_job())
assert 'Recesses' not in page
assert 'blind recess' not in page