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.
224 lines
8.6 KiB
Python
224 lines
8.6 KiB
Python
# -*- 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)
|