Files
willtools-demo/panellift/tests/test_layout_memo.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

175 lines
7.6 KiB
Python

# -*- coding: utf-8 -*-
"""The search remembers layouts it has already costed, and two dead branches.
`_peak_psi` is the inner loop of the whole package -- 884 calls on the median
job of the reference sweep, 15 ms each, 93% of the run. 17% of those calls ask
for a layout the search has already costed: the coarse sweep and the first
refinement pass over the same stations, and each of the three refinement sweeps
re-tries every line's CURRENT position before deciding it cannot improve on it.
Memoising is only safe because the function is pure, and it is pure for a
reason that has to stay true: everything it reads apart from `xs` lives on the
frame, and the frame holds the parts of the problem that DO NOT MOVE when a
pick moves -- the load profile, the section, the station list. The first test
below is the one that matters; it is what catches somebody later making the
frame depend on the positions.
"""
import dataclasses
import pytest
from panellift import layout, section, statics
from conftest import LENGTH_FT, aspire_pockets, make_panel
def _frame(panel, stage, rigging, counts=(2, 2)):
return layout._make_frame(panel, stage, rigging, list(counts))
# ------------------------------------------------------------- it is pure
def test_the_frame_does_not_depend_on_where_the_picks_are(solid_panel,
stripping, rolling):
"""The memo's whole licence. If costing a layout could change the frame,
a remembered answer would be an answer to a different question."""
frame = _frame(solid_panel, stripping, rolling)
before = (list(frame.inv_s), list(frame.jumps), list(frame.counts),
list(frame.profile.w), frame.profile.dx)
for xs in ([4.0, 28.0], [8.0, 24.0], [2.0, 30.0], [15.0, 16.0]):
layout._peak_psi(frame, xs)
after = (list(frame.inv_s), list(frame.jumps), list(frame.counts),
list(frame.profile.w), frame.profile.dx)
assert before == after
def test_a_remembered_answer_is_the_answer(solid_panel, stripping, rolling):
"""Bit-identical, not merely close. The search compares improvements at
1e-9, so a memo that returned a differently-rounded number could pick a
different layout."""
frame = _frame(solid_panel, stripping, rolling)
first = layout._peak_psi(frame, [6.0, 26.0])
again = layout._peak_psi(frame, [6.0, 26.0])
assert again == first
fresh = _frame(solid_panel, stripping, rolling)
assert layout._peak_psi(fresh, [6.0, 26.0]) == first
def test_the_memo_actually_hits(solid_panel, stripping, rolling):
"""A cache nobody hits is a dict that costs memory and buys nothing."""
frame = _frame(solid_panel, stripping, rolling)
layout._peak_psi(frame, [6.0, 26.0])
assert len(frame.memo) == 1
layout._peak_psi(frame, [6.0, 26.0])
assert len(frame.memo) == 1 # hit, not a second entry
layout._peak_psi(frame, [7.0, 26.0])
assert len(frame.memo) == 2
def test_a_real_search_repeats_itself_enough_to_be_worth_it(solid_panel,
stripping,
rolling):
"""Measured 17% repeats on the reference sweep; this pins that the
repetition is structural and not a property of one job file."""
calls = []
real = layout._peak_psi
def counting(frame, xs):
calls.append(tuple(xs))
return real(frame, xs)
layout._peak_psi = counting
try:
layout.best_stations(solid_panel, stripping, rolling,
start=[6.0, 26.0], counts=[2, 2],
free=[True, True])
finally:
layout._peak_psi = real
assert len(calls) > len(set(calls))
def test_an_unbuildable_layout_is_remembered_too(solid_panel, stripping,
rolling):
"""Two lines on one station raise out of group_lifters and cost inf. That
is an answer like any other and re-deriving it is the same wasted solve."""
frame = _frame(solid_panel, stripping, rolling)
value = layout._peak_psi(frame, [10.0, 10.0])
assert value == float('inf')
assert frame.memo[(10.0, 10.0)] == float('inf')
def test_the_memo_is_bounded(solid_panel, stripping, rolling, monkeypatch):
"""A pathological piece must not turn a cache into a memory problem.
Overflowing costs a recomputation, never a different answer."""
monkeypatch.setattr(layout, '_MEMO_MAX', 2)
frame = _frame(solid_panel, stripping, rolling)
for x in (4.0, 5.0, 6.0, 7.0, 8.0):
layout._peak_psi(frame, [x, 26.0])
assert len(frame.memo) == 2
# and the answers are still right past the cap
assert layout._peak_psi(frame, [8.0, 26.0]) == \
layout._peak_psi(_frame(solid_panel, stripping, rolling), [8.0, 26.0])
# --------------------------------------------- the search answer is unchanged
@pytest.mark.parametrize('with_pockets', [False, True])
def test_the_recommendation_is_what_it_was_without_the_memo(
with_pockets, stripping, rolling, monkeypatch):
"""The memo must be invisible in the answer. Run the same search with the
cache disabled (cap 0) and demand the same stations to the last bit."""
panel = make_panel(blockouts=aspire_pockets() if with_pockets else None)
def search():
return layout.best_stations(panel, stripping, rolling,
start=[6.0, 26.0], counts=[2, 2],
free=[True, True])
monkeypatch.setattr(layout, '_MEMO_MAX', 0)
xs_off, best_off = search()
monkeypatch.setattr(layout, '_MEMO_MAX', 200000)
xs_on, best_on = search()
assert xs_on == xs_off
assert best_on == best_off
# ------------------------------------------------- the branch that was deleted
def test_the_section_is_solved_on_exactly_the_diagram_stations(solid_panel,
stripping,
rolling):
"""`_peak_psi` used to carry a guard re-solving the section when the
station list "changed under us". It could not fire: `_make_frame` builds
the station list out of the same profile and the same discontinuities that
`diagram()` does, from the same expression. The guard is gone; this is
what it was guarding, asserted where it can actually fail.
A dead branch in the hottest loop in the package is a branch nobody can
test, and the comment on it claimed a thing that could not happen.
"""
for panel in (solid_panel, make_panel(blockouts=aspire_pockets())):
frame = _frame(panel, stripping, rolling)
for xs in ([6.0, 26.0], [4.0, 28.0], [10.0, 22.0]):
lifters = []
for i, (x, n) in enumerate(zip(xs, frame.counts)):
for j in range(n):
lifters.append(layout.Lifter(
mark='r{0}.{1}'.format(i, j), family='?',
x=x, y=0.0, role='strip'))
staged = dataclasses.replace(panel, lifters=lifters)
rig = statics.group_lifters(staged, rolling, role='strip')
statics.solve_rolling(rig, frame.profile)
dia = statics.diagram(rig, frame.profile,
extra_stations=frame.jumps)
assert len(dia.xs) == len(frame.inv_s)
def test_the_frame_carries_no_spare_lifters():
"""`_Frame._spares` claimed a reuse optimisation that did not exist --
nothing ever read it or wrote it. A field whose comment describes code
that is not there is worse than no field."""
names = [f.name for f in dataclasses.fields(layout._Frame)]
assert '_spares' not in names
assert 'memo' in names