# -*- 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