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.
137 lines
5.6 KiB
Python
137 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""A piece does not always hang the way it was cast.
|
|
|
|
A horizontal panel is stripped flat, spanning its long direction like
|
|
everything else, and then stood up on its long EDGE. Gravity then runs across
|
|
what used to be the width, and that is the direction it spans.
|
|
|
|
The solver is not taught about orientation. The piece is turned instead, so
|
|
the mechanics stay one implementation checked against published examples.
|
|
"""
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
from conftest import LENGTH_FT, THICKNESS_IN, WIDTH_FT, make_panel
|
|
from panellift import cli, statics
|
|
from panellift.model import Blockout, Lifter
|
|
|
|
|
|
def horizontal_job(span):
|
|
"""Anchors along one long edge, face lifters inboard of it."""
|
|
return {
|
|
'schema': cli.SCHEMA,
|
|
'panel': {
|
|
'mark': 'H1', 'length_ft': 30.0, 'width_ft': 8.0,
|
|
'thickness_in': 8.0, 'weight_lb': 24000.0, 'fc_psi': 5000.0,
|
|
'blockouts': [{'name': 'O', 'x0': 10.0, 'y0': 2.0,
|
|
'length': 6.0, 'width': 3.0}],
|
|
'lifters': [
|
|
{'mark': 'A', 'family': 'ANCHOR', 'x': 6.0, 'y': 8.0,
|
|
'on_edge': True, 'pinned_y': True, 'capacity_lb': 7690.0,
|
|
'shear_capacity_lb': 4010.0},
|
|
{'mark': 'B', 'family': 'ANCHOR', 'x': 24.0, 'y': 8.0,
|
|
'on_edge': True, 'pinned_y': True, 'capacity_lb': 7690.0,
|
|
'shear_capacity_lb': 4010.0},
|
|
{'mark': 'C', 'family': 'FACE', 'x': 6.0, 'y': 1.5,
|
|
'capacity_lb': 8000.0},
|
|
{'mark': 'D', 'family': 'FACE', 'x': 24.0, 'y': 1.5,
|
|
'capacity_lb': 8000.0},
|
|
],
|
|
},
|
|
'stages': [{'name': 'erection', 'multiplier': 1.2, 'fc_psi': 5000.0,
|
|
'span': span}],
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------- the transposition
|
|
|
|
def test_turning_the_piece_swaps_its_two_in_plane_dimensions():
|
|
panel = make_panel()
|
|
turned = cli.transpose(panel)
|
|
assert turned.length_ft == pytest.approx(WIDTH_FT)
|
|
assert turned.width_ft == pytest.approx(LENGTH_FT)
|
|
assert turned.thickness_in == pytest.approx(THICKNESS_IN)
|
|
|
|
|
|
def test_turning_the_piece_takes_the_blockouts_with_it():
|
|
panel = dataclasses.replace(make_panel(), blockouts=[
|
|
Blockout(name='O', x0=4.0, y0=1.0, length=6.0, width=2.0)])
|
|
hole = cli.transpose(panel).blockouts[0]
|
|
assert (hole.x0, hole.y0, hole.length, hole.width) == (1.0, 4.0, 2.0, 6.0)
|
|
|
|
|
|
def test_turning_the_piece_swaps_which_way_an_insert_is_pinned():
|
|
"""An anchor pinned to a long edge is pinned across the piece as cast and
|
|
ALONG it once the piece has been stood up on that edge. Carrying the flag
|
|
through unchanged would free it to slide off its own edge."""
|
|
panel = dataclasses.replace(make_panel(), lifters=[
|
|
Lifter(mark='A', family='F', x=2.0, y=9.0, pinned_y=True)])
|
|
turned = cli.transpose(panel).lifters[0]
|
|
assert (turned.x, turned.y) == (9.0, 2.0)
|
|
assert turned.pinned and not turned.pinned_y
|
|
|
|
|
|
def test_turning_twice_is_the_piece_you_started_with():
|
|
panel = make_panel()
|
|
there_and_back = cli.transpose(cli.transpose(panel))
|
|
assert there_and_back.length_ft == pytest.approx(panel.length_ft)
|
|
assert [(l.x, l.y) for l in there_and_back.lifters] == \
|
|
[(l.x, l.y) for l in panel.lifters]
|
|
|
|
|
|
# ------------------------------------------------------------ through a job
|
|
|
|
def test_a_stage_spanning_the_width_really_spans_the_width():
|
|
_p, _r, cases = cli.analyse_job(horizontal_job('width'))
|
|
assert cases[0][0].panel.length_ft == pytest.approx(8.0)
|
|
assert cases[0][0].stage.span == 'width'
|
|
|
|
|
|
def test_the_same_inserts_group_differently_when_the_piece_turns():
|
|
"""The anchors along the top edge are ONE support when the piece is
|
|
standing and TWO when it is flat. That is the whole reason to turn it."""
|
|
_p, _r, flat = cli.analyse_job(horizontal_job('length'))
|
|
_p, _r, stood = cli.analyse_job(horizontal_job('width'))
|
|
assert len(flat[0][0].rig.lines) == 2 # by x: 6 ft and 24 ft
|
|
assert len(stood[0][0].rig.lines) == 2 # by y: 1.5 ft and 8 ft
|
|
stood_line = max(stood[0][0].rig.lines, key=lambda l: l.x)
|
|
assert stood_line.count == 2 # both anchors, one support
|
|
|
|
|
|
def test_a_stage_defaults_to_spanning_its_length():
|
|
_p, _r, cases = cli.analyse_job(horizontal_job(None))
|
|
assert cases[0][0].stage.span == 'length'
|
|
assert cases[0][0].panel.length_ft == pytest.approx(30.0)
|
|
|
|
|
|
def test_an_unknown_span_is_refused():
|
|
with pytest.raises(cli.JobError):
|
|
cli.analyse_job(horizontal_job('diagonally'))
|
|
|
|
|
|
def test_the_position_search_stands_aside_for_a_turned_stage():
|
|
"""It optimises one set of coordinates. A turned stage's x is the panel's
|
|
y, so including it would be optimising against two different pieces.
|
|
|
|
It still answers for the stages it can, and says which one it left out --
|
|
silently optimising against two of three stages would read as an answer
|
|
for all three.
|
|
"""
|
|
from panellift import layout
|
|
job = horizontal_job('width')
|
|
job['stages'].insert(0, {'name': 'stripping', 'multiplier': 1.3,
|
|
'fc_psi': 4000.0})
|
|
_p, _r, cases = cli.analyse_job(job)
|
|
rec = layout.recommend(cases)
|
|
assert rec.moves # stripping still answered
|
|
assert [s.name for s in rec.stages] == ['stripping']
|
|
assert 'erection' in rec.note
|
|
|
|
|
|
def test_the_search_says_so_when_every_stage_is_turned():
|
|
from panellift import layout
|
|
_p, _r, cases = cli.analyse_job(horizontal_job('width'))
|
|
rec = layout.recommend(cases)
|
|
assert not rec.moves and 'erection' in rec.note
|