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.
262 lines
11 KiB
Python
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
|