WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
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.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""tension_fos / shear_fos: a number that had no source, and its removal.
|
||||
|
||||
They divide a lifter's rated capacity -- `shear_fos` when the lifter sits on
|
||||
an edge, `tension_fos` otherwise -- and only when `capacity_is_ultimate` is
|
||||
true, i.e. only where the catalogue quotes an ultimate rather than a working
|
||||
load. Nothing in this repository sets that flag, so the arithmetic is not
|
||||
reached on any shipped path; it is reachable, it is public API, and what it
|
||||
would have computed was wrong.
|
||||
|
||||
WHAT WAS WRONG. `shear_fos` was 2.67, as a `Rigging` default, as a
|
||||
`read_rigging` default, as a `catalogue.safety_factors` entry and as what
|
||||
`liftexport` wrote into every job file. No page published it, nobody typed
|
||||
it, and the comment justifying it as "the manufacturer's 4:1 tension /
|
||||
2.67:1 shear" was written after the constant and cited nothing. The one
|
||||
datasheet in this repository for the parts it reached -- ALP catalog p53,
|
||||
02/2024 -- heads its capacity table "Capacity in Concrete 4:1 SWL" over BOTH
|
||||
load columns and prints shear equal to tension on all seven QLPD rows.
|
||||
|
||||
WHAT IT WOULD HAVE DONE. 32,000 lb ultimate / 2.67 = 11,985 lb allowed,
|
||||
against a published SWL of 8,000. Half again as much load on an anchor as
|
||||
the manufacturer permits, in the direction that passes one which would fail.
|
||||
|
||||
SO THE GLOBAL IS GONE, and it was not replaced by a global 4.0 -- that is
|
||||
the same defect wearing the right number, because the next manufacturer's
|
||||
insert would inherit a factor read off ALP's page. The factors are per
|
||||
product in the catalogue and per insert in `Lifter`, and an insert with none
|
||||
is NOT JUDGED: `INSERT-NO-FOS`, named, never defaulted.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from panellift import checks, cli, statics
|
||||
from panellift.model import Lifter, Rigging, Stage
|
||||
from conftest import make_panel
|
||||
|
||||
CATALOGUE = (Path(__file__).resolve().parents[2]
|
||||
/ 'knowledge' / 'catalogue.yaml')
|
||||
|
||||
# This is the one file in this suite that is NOT self-contained -- it pins
|
||||
# published catalogue rows against the real, tracked `knowledge/catalogue.yaml`
|
||||
# rather than a fixture, on purpose (see the module docstring: this is a
|
||||
# golden-value check against a page, not a recomputation). A knowledge-less
|
||||
# distribution (tools/make_dist.py) ships no catalogue.yaml at all, so the
|
||||
# question this file asks -- "does the catalogue publish X" -- has no
|
||||
# catalogue to ask it of. SKIP, not FAIL: the same "could not judge" vs.
|
||||
# "judged, found nothing" discipline the rest of this project uses, applied
|
||||
# to a test file rather than a knowledge lookup.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not CATALOGUE.is_file(),
|
||||
reason='knowledge/catalogue.yaml is not shipped in this checkout '
|
||||
'(see knowledge/README.md) -- nothing to pin these golden values '
|
||||
'against')
|
||||
|
||||
# The seven QuikLift disk-anchor rows of ALP catalog p53, 02/2024, typed in
|
||||
# from the page: (part, tension SWL, shear SWL, ultimate tension).
|
||||
# A pin against a written-down golden, not a recomputation.
|
||||
PAGE_53 = [
|
||||
('QLPD2T3WG', 4000, 4000, 16000),
|
||||
('QLPD4T3WG', 6257, 6257, 32000),
|
||||
('QLPD4T4WG', 7200, 7200, 32000),
|
||||
('QLPD4T5WG', 8000, 8000, 32000),
|
||||
('QLPD8T6WG', 11019, 11019, 64000),
|
||||
('QLPD8T7WG', 14640, 14640, 64000),
|
||||
('QLPD8T8WG', 16000, 16000, 64000),
|
||||
]
|
||||
|
||||
|
||||
def catalogue_text():
|
||||
return CATALOGUE.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def entry_text(part):
|
||||
"""One `catalogue.inserts` entry, as raw YAML text.
|
||||
|
||||
Read with a regex rather than a parser: this package has no YAML
|
||||
dependency and is not going to grow one to check a handful of numbers.
|
||||
"""
|
||||
block = re.search(r'\n %s:\n((?: \S.*\n| .*\n)+)' % part,
|
||||
catalogue_text())
|
||||
assert block, 'catalogue.yaml has no entry for {0}'.format(part)
|
||||
return block.group(1)
|
||||
|
||||
|
||||
def field(part, name):
|
||||
found = re.search(r'^\s+%s:\s*([-\d.]+)\s*$' % name, entry_text(part),
|
||||
re.M)
|
||||
return float(found.group(1)) if found else None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ the page
|
||||
|
||||
@pytest.mark.parametrize('part,tension,shear,_ultimate', PAGE_53)
|
||||
def test_the_catalogue_publishes_four_to_one_in_shear_for_every_qlpd_row(
|
||||
part, tension, shear, _ultimate):
|
||||
"""The page's own heading, landed as data on the part it belongs to."""
|
||||
text = entry_text(part)
|
||||
assert 'safety_factors:' in text, (
|
||||
'{0} carries no safety_factors block, so an ultimate rating for it '
|
||||
'has no published factor and it will be reported unjudged'
|
||||
.format(part))
|
||||
assert field(part, 'tension_fos') == 4.0
|
||||
assert field(part, 'shear_fos') == 4.0, (
|
||||
'ALP catalog p53 heads the capacity table "Capacity in Concrete 4:1 '
|
||||
'SWL" across BOTH load columns, so shear is 4:1 here too. 2.67 is '
|
||||
'the number this test exists to keep out.')
|
||||
assert 'p53' in text and '02/2024' in text, (
|
||||
'a safety factor with no page beside it is how 2.67 survived for '
|
||||
'twelve days')
|
||||
# ...and the working loads it is a factor on are still the page's.
|
||||
assert field(part, 'working_load_lb') == tension
|
||||
assert field(part, 'shear_load_lb') == shear
|
||||
|
||||
|
||||
def test_no_global_safety_factor_survives_anywhere():
|
||||
"""A tombstone. The failure was not the value 2.67 but the SHAPE that
|
||||
let one number apply to every manufacturer's insert, so replacing it
|
||||
with a global 4.0 would have been the same defect with a better number.
|
||||
"""
|
||||
block = re.search(r'\n safety_factors:\n((?: \w+:.*\n| .*\n)+)',
|
||||
catalogue_text())
|
||||
assert block, 'catalogue.yaml has no catalogue.safety_factors block'
|
||||
assert re.search(r'^ tension_fos: null\s*$', block.group(1), re.M)
|
||||
assert re.search(r'^ shear_fos: null\s*$', block.group(1), re.M)
|
||||
assert Rigging().tension_fos is None
|
||||
assert Rigging().shear_fos is None
|
||||
assert cli.read_rigging({}).tension_fos is None
|
||||
assert cli.read_rigging({}).shear_fos is None
|
||||
assert Lifter(mark='m', family='f', x=0.0, y=0.0).shear_fos is None
|
||||
# 2.67 may still be NAMED in the catalogue -- the comment that records
|
||||
# what it was and why it went is the provenance, and deleting the number
|
||||
# would leave the next reader to rediscover it from the same page. What
|
||||
# may not come back is 2.67 as a VALUE.
|
||||
live = re.findall(r'^\s*\w*fos:\s*2\.67\s*$', catalogue_text(), re.M)
|
||||
assert not live, live
|
||||
|
||||
|
||||
def test_the_ultimate_route_overshoots_the_published_swl_even_at_four_to_one():
|
||||
"""Why `capacity_is_ultimate` stays false for these parts.
|
||||
|
||||
The page shades four of its seven rows and says the shading marks where
|
||||
the anchor's own mechanical capacity governs -- so the printed SWL is
|
||||
the LESSER of ultimate/4 and what the concrete holds. Reading an
|
||||
ultimate through the right factor is still unconservative on those four,
|
||||
which is a fact about the route and not about the factor, and it is here
|
||||
so that nobody "fixes" the factor and then turns the flag on.
|
||||
"""
|
||||
over = [(part, ultimate / 4.0, tension)
|
||||
for (part, tension, _shear, ultimate) in PAGE_53
|
||||
if ultimate / 4.0 > tension + 0.5]
|
||||
assert [p for (p, _a, _b) in over] == [
|
||||
'QLPD4T3WG', 'QLPD4T4WG', 'QLPD8T6WG', 'QLPD8T7WG']
|
||||
assert max(a / b for (_p, a, b) in over) > 1.4 # QLPD8T6WG, +45%
|
||||
|
||||
|
||||
# ------------------------------------------------------- what the check does
|
||||
|
||||
def _panel_with(factor_kind, factor):
|
||||
"""A rating and a factor on ONLY the axis under test.
|
||||
|
||||
2026-09-02: check_insert_capacity now decomposes the sling's line force
|
||||
into a tension and a shear share and judges both, for every lifter --
|
||||
`on_edge` no longer picks a single axis to check. These tests are about
|
||||
factor resolution on one column, not about the decomposition, so the
|
||||
OTHER column is left with no capacity at all (rated() returns None,
|
||||
joining INSERT-UNRATED, never INSERT-NO-FOS) rather than a rating with
|
||||
no factor of its own, which would otherwise also land in
|
||||
INSERT-NO-FOS and be mistaken for the thing under test.
|
||||
"""
|
||||
panel = make_panel()
|
||||
lifters = [
|
||||
Lifter(mark=l.mark, family=l.family, x=l.x, y=l.y, role=l.role,
|
||||
capacity_lb=32000.0 if factor_kind == 'tension' else None,
|
||||
shear_capacity_lb=32000.0 if factor_kind == 'shear' else None,
|
||||
on_edge=(factor_kind == 'shear'),
|
||||
**{factor_kind + '_fos': factor})
|
||||
for l in panel.lifters]
|
||||
return dataclass_replace_lifters(panel, lifters)
|
||||
|
||||
|
||||
def dataclass_replace_lifters(panel, lifters):
|
||||
import dataclasses
|
||||
return dataclasses.replace(panel, lifters=lifters)
|
||||
|
||||
|
||||
def _findings(panel, stage, ultimate=True):
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=60.0)
|
||||
analysis = statics.analyse(panel, stage, rigging, reconcile_weight=False)
|
||||
return checks.check_insert_capacity(analysis, rigging, ultimate)
|
||||
|
||||
|
||||
def test_an_insert_with_no_published_factor_is_named_not_defaulted(stripping):
|
||||
"""The whole point. An ultimate rating and no factor used to become an
|
||||
allowed load through a constant; it now becomes a sentence."""
|
||||
out = _findings(_panel_with('shear', None), stripping)
|
||||
codes = [f.code for f in out]
|
||||
assert 'INSERT-NO-FOS' in codes
|
||||
note = [f for f in out if f.code == 'INSERT-NO-FOS'][0]
|
||||
assert note.severity == 'WARN'
|
||||
assert 'NOT judged' in note.message
|
||||
assert 'borrowed' in note.message
|
||||
# and no insert was judged behind its back
|
||||
assert not [f for f in out if f.code == 'INSERT']
|
||||
|
||||
|
||||
def test_the_page_factor_gives_exactly_the_published_working_load(stripping):
|
||||
"""32,000 / 4 = 8,000, which is what p53 prints for QLPD4T5WG. The old
|
||||
2.67 gave 11,985 for the same insert -- allowing nearly half again as
|
||||
much shear as the manufacturer's own page. Both numbers appear in the
|
||||
message (2026-09-02: on the SHEAR clause specifically, since the check
|
||||
now decomposes the line force rather than comparing it whole), so this
|
||||
compares them."""
|
||||
with_page = _findings(_panel_with('shear', 4.0), stripping)
|
||||
with_2_67 = _findings(_panel_with('shear', 2.67), stripping)
|
||||
page_msg = ' '.join(f.message for f in with_page if f.code == 'INSERT')
|
||||
old_msg = ' '.join(f.message for f in with_2_67 if f.code == 'INSERT')
|
||||
assert 'shear' in page_msg
|
||||
assert 'vs 8,000 lb' in page_msg or 'vs 8000 lb' in page_msg
|
||||
assert page_msg != old_msg
|
||||
assert '11,98' in old_msg or '11985' in old_msg
|
||||
|
||||
|
||||
def test_the_insert_s_own_factor_beats_the_job_wide_one(stripping):
|
||||
"""Resolution order, and the direction that matters: a job file naming
|
||||
2.67 must not override what the part's datasheet publishes."""
|
||||
panel = _panel_with('shear', 4.0)
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=60.0, shear_fos=2.67)
|
||||
analysis = statics.analyse(panel, stripping, rigging,
|
||||
reconcile_weight=False)
|
||||
out = checks.check_insert_capacity(analysis, rigging, True)
|
||||
assert not [f for f in out if f.code == 'INSERT-NO-FOS']
|
||||
msg = ' '.join(f.message for f in out if f.code == 'INSERT')
|
||||
assert '11,98' not in msg and '11985' not in msg
|
||||
|
||||
|
||||
def test_a_working_load_reading_never_touches_a_factor(stripping):
|
||||
"""The default path, which is every shipped path: with the ratings read
|
||||
as working loads the factors are not applied at all, so an insert with
|
||||
none is judged normally and no INSERT-NO-FOS is raised."""
|
||||
out = _findings(_panel_with('shear', None), stripping, ultimate=False)
|
||||
assert not [f for f in out if f.code == 'INSERT-NO-FOS']
|
||||
assert [f for f in out if f.code == 'INSERT']
|
||||
Reference in New Issue
Block a user