8e6ee31596
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.
175 lines
7.6 KiB
Python
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
|