WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the panelanalysis/1 SOLVE path -- the self-weight Handling model.
|
||||
|
||||
Loaded by path, exactly like `test_panelanalysis.py` beside it: this is host
|
||||
CPython (it imports `panellift`, dataclasses, typing syntax) sitting in an
|
||||
IronPython 2.7 extension, and it reaches the tool through `tools/`, which the
|
||||
sibling knowledge merge in `deckcheck.py` puts the `vibe` stub in place for.
|
||||
|
||||
The four things the Stage 0 brief asked these to nail down:
|
||||
|
||||
1. the self-weight solve reactions sum to the load, the moment diagram is
|
||||
real, and the per-station capacity comes from `panellift`;
|
||||
2. editing the deck's supports and re-solving changes the reactions -- the
|
||||
round-trip the owner wants;
|
||||
3. a deck with fewer than two supports is REFUSED loudly (panellift's own
|
||||
"a supported stage needs at least two supports"), surfaced as NOT JUDGED
|
||||
on the page, never a traceback and never a silent OK;
|
||||
4. missing prestress dashes with panellift's reason, never a zero; the
|
||||
In-Place sections are explicit placeholders; zero CJK on the page.
|
||||
|
||||
Everything written at runtime goes under `tmp_path`; nothing touches the
|
||||
working directory.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
TOOLS = os.path.join(ROOT, 'tools')
|
||||
PANELLIFT_SRC = os.path.join(ROOT, 'panellift', 'src')
|
||||
for p in (TOOLS, PANELLIFT_SRC):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from vibe import knowledge # noqa: E402,F401 (registers the vibe stub)
|
||||
from vibe import plantpath # noqa: E402,F401
|
||||
|
||||
pa = pytest.importorskip('panelanalysis')
|
||||
schema = pytest.importorskip('panelanalysis_schema')
|
||||
|
||||
CJK = re.compile(u'[一-鿿]')
|
||||
|
||||
IN_PLACE_TITLES = [
|
||||
'In-Place Load Case (not computed)',
|
||||
'In-Place Load Combination (not computed)',
|
||||
'In-Place Strength (not computed)',
|
||||
'In-Place Service (not computed)',
|
||||
]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- helpers
|
||||
|
||||
def _solve(deck):
|
||||
panel = schema.to_panel(deck)
|
||||
return panel, pa.solve_handling(deck, panel)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- solve
|
||||
|
||||
def test_self_weight_reactions_sum_to_the_load():
|
||||
deck = schema.new_deck(support_xs=(2.0, 22.0))
|
||||
panel, (analysis, refusal) = _solve(deck)
|
||||
assert refusal is None
|
||||
total_r = sum(l.reaction_lb for l in analysis.rig.lines)
|
||||
total_load = analysis.profile.total_lb
|
||||
assert total_load > 0
|
||||
# panellift's consistent-load formulation makes this exact; a loose floor.
|
||||
assert abs(total_r - total_load) <= max(1.0, 1e-6 * total_load)
|
||||
|
||||
|
||||
def test_moment_diagram_is_real():
|
||||
deck = schema.new_deck(support_xs=(2.0, 22.0))
|
||||
_panel, (analysis, refusal) = _solve(deck)
|
||||
assert refusal is None
|
||||
assert len(analysis.diagram.moment_lbft) > 10
|
||||
assert max(abs(m) for m in analysis.diagram.moment_lbft) > 0.0
|
||||
# A simply-supported self-weight beam sags between the supports.
|
||||
_x, mmax = analysis.diagram.max_positive
|
||||
assert mmax > 0.0
|
||||
|
||||
|
||||
def test_capacity_stations_come_from_panellift():
|
||||
"""f_se and phi*Mn at mid-span are real numbers from panellift.capacity."""
|
||||
from panellift import capacity
|
||||
deck = schema.new_deck(support_xs=(2.0, 22.0))
|
||||
panel, (analysis, refusal) = _solve(deck)
|
||||
assert refusal is None
|
||||
x = panel.length_ft / 2.0
|
||||
m = analysis.diagram.moment_at(x)
|
||||
v = analysis.diagram.shear_at(x)
|
||||
stage = schema.handling_stage(deck, panel)
|
||||
cap = capacity.capacity_at(panel, stage, x, m, v)
|
||||
# A prestressed piece with developed strand at mid-span: real f_se and a
|
||||
# real phi*Mn / phi*Vn, not None.
|
||||
assert cap.fse_ksi is not None and cap.fse_ksi > 0
|
||||
assert cap.phi_mn_lbft is not None and cap.phi_mn_lbft > 0
|
||||
assert cap.phi_vn_lb is not None and cap.phi_vn_lb > 0
|
||||
|
||||
|
||||
def test_editing_supports_changes_the_reactions():
|
||||
"""The round-trip: move the supports, re-solve, the answer moves."""
|
||||
# Asymmetric supports so the change is unmistakable: a symmetric panel on
|
||||
# supports symmetric about its centroid gives equal reactions either way,
|
||||
# which would hide the very round-trip this asserts.
|
||||
a = schema.new_deck(support_xs=(2.0, 22.0)) # symmetric: half and half
|
||||
b = schema.new_deck(support_xs=(2.0, 14.0)) # right support inboard
|
||||
_pa, (aa, ra) = _solve(a)
|
||||
_pb, (ab, rb) = _solve(b)
|
||||
assert ra is None and rb is None
|
||||
rx_a = [round(l.reaction_lb, 1) for l in aa.rig.lines]
|
||||
rx_b = [round(l.reaction_lb, 1) for l in ab.rig.lines]
|
||||
assert rx_a != rx_b
|
||||
# and the sum is unchanged -- the load did not move, only how it is shared.
|
||||
assert abs(sum(rx_a) - sum(rx_b)) <= 1.0
|
||||
|
||||
|
||||
def test_three_supports_are_indeterminate_and_still_close():
|
||||
deck = schema.new_deck(support_xs=(2.0, 12.0, 22.0))
|
||||
_panel, (analysis, refusal) = _solve(deck)
|
||||
assert refusal is None
|
||||
total_r = sum(l.reaction_lb for l in analysis.rig.lines)
|
||||
total_load = analysis.profile.total_lb
|
||||
assert abs(total_r - total_load) <= max(1.0, 1e-6 * total_load)
|
||||
|
||||
|
||||
# ------------------------------------------------------- the loud refusals
|
||||
|
||||
def test_fewer_than_two_supports_is_refused_loudly():
|
||||
deck = schema.new_deck(support_xs=(5.0,))
|
||||
_panel, (analysis, refusal) = _solve(deck)
|
||||
assert analysis is None
|
||||
assert 'at least two supports' in refusal
|
||||
|
||||
|
||||
def test_one_support_page_says_not_judged_not_a_traceback():
|
||||
deck = schema.new_deck(support_xs=(5.0,))
|
||||
html = pa.build_analysis_page(deck) # must not raise
|
||||
assert 'NOT JUDGED' in html
|
||||
assert 'at least two supports' in html
|
||||
assert 'Traceback' not in html
|
||||
# The In-Place placeholders still render -- the page is whole.
|
||||
for title in IN_PLACE_TITLES:
|
||||
assert '>{0}<'.format(title) in html, title
|
||||
|
||||
|
||||
def test_support_off_the_piece_is_refused():
|
||||
deck = schema.new_deck(length_ft=24.0, support_xs=(2.0, 99.0))
|
||||
_panel, (analysis, refusal) = _solve(deck)
|
||||
assert analysis is None
|
||||
assert 'outside the piece' in refusal
|
||||
|
||||
|
||||
def test_no_prestress_capacity_dashes_with_reason_not_a_zero():
|
||||
deck = schema.new_deck(strands=[])
|
||||
panel = schema.to_panel(deck)
|
||||
assert panel.prestress is None
|
||||
html = pa.build_analysis_page(deck) # solve still runs (dunnage)
|
||||
# panellift.capacity's own words for a strand-only tool with no strand --
|
||||
# a dash carrying this reason, never a fabricated phi*Mn of 0.
|
||||
assert 'no prestressing on this piece' in html
|
||||
# The self-weight solve itself still ran (reactions do not need strand).
|
||||
assert 'Support reactions' in html
|
||||
|
||||
|
||||
# ------------------------------------------------------------- the whole page
|
||||
|
||||
def test_page_has_every_section_and_no_cjk():
|
||||
deck = schema.new_deck(support_xs=(2.0, 22.0))
|
||||
html = pa.build_analysis_page(deck)
|
||||
for title in ['General / Project Information', 'Concrete Extents',
|
||||
'Materials', 'Prestress / Reinforcement',
|
||||
'Section Properties'] + IN_PLACE_TITLES:
|
||||
assert '>{0}<'.format(title) in html, title
|
||||
assert 'Support reactions' in html
|
||||
assert 'Demand vs capacity' in html
|
||||
m = CJK.search(html)
|
||||
assert not m, 'CJK character found: {0!r}'.format(m.group() if m else '')
|
||||
assert 'WP242' not in html # neutral fixture marks only
|
||||
|
||||
|
||||
def test_cli_end_to_end_writes_only_under_tmp_path(tmp_path):
|
||||
deck = schema.new_deck(support_xs=(2.0, 22.0))
|
||||
deck_path = tmp_path / 'PNL101.pa.json'
|
||||
out_path = tmp_path / 'out.html'
|
||||
with io.open(str(deck_path), 'w', encoding='utf-8') as fh:
|
||||
json.dump(deck, fh)
|
||||
rc = pa.main([str(deck_path), '-o', str(out_path)])
|
||||
assert rc == 0
|
||||
assert out_path.exists()
|
||||
html = out_path.read_text(encoding='utf-8')
|
||||
assert 'Support reactions' in html
|
||||
assert 'Handling' in html
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- schema
|
||||
|
||||
def test_unread_fields_carry_an_explicit_reason():
|
||||
deck = schema.new_deck()
|
||||
# in_place is the whole-block placeholder.
|
||||
assert deck['in_place']['value'] is None
|
||||
assert deck['in_place']['reason']
|
||||
# loading.wind / combinations are the file contract's null+reason shape.
|
||||
for key in ('wind', 'combinations'):
|
||||
assert deck['loading'][key]['value'] is None
|
||||
assert deck['loading'][key]['reason']
|
||||
# structural_model restraints/bearing likewise.
|
||||
for key in ('restraints', 'bearing'):
|
||||
assert deck['structural_model'][key]['value'] is None
|
||||
assert deck['structural_model'][key]['reason']
|
||||
|
||||
|
||||
def test_a_foreign_schema_is_refused_by_name():
|
||||
deck = schema.new_deck()
|
||||
deck['schema'] = 'panellift/1'
|
||||
with pytest.raises(schema.DeckError):
|
||||
schema.check_schema(deck)
|
||||
Reference in New Issue
Block a user