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.
203 lines
7.6 KiB
Python
203 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The ASCE C&C wind engine wired into Panel Analysis (solve side).
|
|
|
|
`panellift.windload` is CPython (dataclasses + typing) and the Panel Analysis
|
|
form is IronPython 2.7, so the pressure is COMPUTED here, on the CPython
|
|
deck-solve side (`panelanalysis_schema` / `panelanalysis`), from the ASCE inputs
|
|
the form collects -- not in the form. These tests pin:
|
|
|
|
* the computed C&C pressure matches `windload` directly (no second opinion);
|
|
* the direct > computed > blank precedence that decides what the in-place
|
|
wind case runs on;
|
|
* the NOT-JUDGED pass-through (h>60 ft): a reason, never a fabricated pressure;
|
|
* the enclosure -> GCpi value that reaches the page;
|
|
* a wired deck actually RUNS the in-place wind case on the computed pressure,
|
|
and the page renders the pressure + GCpi with no traceback and no CJK.
|
|
|
|
Host CPython, loaded like its `test_panelanalysis*` siblings. Generic ASCE
|
|
example numbers only (the textbook low-rise wall from test_windload); no client
|
|
data.
|
|
"""
|
|
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)
|
|
|
|
pa = pytest.importorskip('panelanalysis')
|
|
schema = pytest.importorskip('panelanalysis_schema')
|
|
windload = pytest.importorskip('panellift.windload')
|
|
|
|
CJK = re.compile(u'[一-鿿]')
|
|
|
|
|
|
def _asce_deck(wind_over=None, restraints=True):
|
|
"""A mild solid wall pinned top/bottom with ASCE C&C wind inputs on it."""
|
|
deck = schema.new_deck(length_ft=30.0, width_ft=10.0, thickness_in=12.0,
|
|
fc_psi=6000.0, strands=[])
|
|
if restraints:
|
|
deck['structural_model']['restraints'] = [
|
|
{'x_ft': 0.0, 'type': 'pin'}, {'x_ft': 30.0, 'type': 'pin'}]
|
|
wind = {'asce': {
|
|
'v_ult_mph': 115, 'risk_category': 'II', 'exposure': 'C',
|
|
'enclosure': 'enclosed', 'code_version': '7-16',
|
|
'mean_roof_height_ft': 30, 'effective_wind_area_ft2': 100,
|
|
'cc_chapter': 30}}
|
|
if wind_over is not None:
|
|
wind = wind_over
|
|
deck['loading']['wind'] = wind
|
|
return deck
|
|
|
|
|
|
def _windload_governing(**kw):
|
|
"""The governing |p| windload reports directly, for the same inputs."""
|
|
res = windload.compute(
|
|
v_ult_mph=115.0, risk_category='II', exposure='C',
|
|
enclosure='enclosed', code_version='7-16',
|
|
mean_roof_height_ft=30.0, effective_wind_area_ft2=100.0, **kw)
|
|
mags = [abs(z.positive_psf) for z in res.zones] + \
|
|
[abs(z.negative_psf) for z in res.zones]
|
|
return max(mags)
|
|
|
|
|
|
# ------------------------------------------------------------- the decision
|
|
|
|
def test_computed_pressure_matches_windload_directly():
|
|
deck = _asce_deck()
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'computed'
|
|
assert info['pressure_psf'] == pytest.approx(_windload_governing(), abs=0.01)
|
|
# the figure-read uncertainty and the ASCE cite travel with it
|
|
assert info['uncertainty_psf'] > 0.0
|
|
assert info['cite']
|
|
# enclosure -> GCpi reaches the page (enclosed = 0.18)
|
|
assert info['gcpi'] == 0.18
|
|
|
|
|
|
def test_direct_entry_overrides_the_computed_pressure():
|
|
deck = _asce_deck()
|
|
deck['loading']['wind']['direct_pressure_psf'] = 45.0
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'direct'
|
|
assert info['pressure_psf'] == 45.0
|
|
# the computed values are still carried for reference
|
|
assert info['result'] is not None and not info['result'].not_judged
|
|
|
|
|
|
def test_direct_suction_alone_still_drives_the_case():
|
|
deck = _asce_deck(wind_over={'direct_suction_psf': 40.0})
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'direct'
|
|
assert info['pressure_psf'] == 40.0
|
|
|
|
|
|
def test_h_over_60_is_not_judged_never_a_fabricated_pressure():
|
|
deck = _asce_deck()
|
|
deck['loading']['wind']['asce']['mean_roof_height_ft'] = 80
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'not_judged'
|
|
assert info['pressure_psf'] is None
|
|
assert 'NOT JUDGED' in info['reason'] and '60' in info['reason']
|
|
|
|
|
|
def test_unknown_version_is_not_judged():
|
|
deck = _asce_deck()
|
|
deck['loading']['wind']['asce']['code_version'] = '7-05'
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'not_judged'
|
|
assert info['pressure_psf'] is None
|
|
|
|
|
|
def test_incomplete_asce_is_not_judged_with_what_is_missing():
|
|
deck = _asce_deck(wind_over={'asce_incomplete': 'still need Vult (mph)'})
|
|
info = schema.resolve_wind(deck)
|
|
assert info['source'] == 'not_judged'
|
|
assert 'Vult' in info['reason']
|
|
|
|
|
|
# ------------------------------------------------------------- feed to inplace
|
|
|
|
def test_wind_zones_builds_one_zone_over_the_panel_length():
|
|
deck = _asce_deck()
|
|
panel = schema.to_panel(deck)
|
|
zones, trib, reason = schema.wind_zones(deck, panel)
|
|
assert reason is None and len(zones) == 1
|
|
z = zones[0]
|
|
assert z.x0_ft == 0.0 and z.x1_ft == pytest.approx(panel.length_ft)
|
|
assert z.pressure_psf == pytest.approx(_windload_governing(), abs=0.01)
|
|
|
|
|
|
def test_explicit_given_zones_still_work_untouched():
|
|
deck = _asce_deck(wind_over={'zones': [
|
|
{'x0_ft': 0.0, 'x1_ft': 30.0, 'pressure_psf': 50.0, 'label': 'W1'}]})
|
|
panel = schema.to_panel(deck)
|
|
zones, _t, reason = schema.wind_zones(deck, panel)
|
|
assert reason is None and zones[0].pressure_psf == 50.0
|
|
# the ASCE decision is not taken for the given-pressure path
|
|
assert schema.resolve_wind(deck) is None
|
|
|
|
|
|
def test_in_place_runs_on_the_computed_wind_pressure():
|
|
deck = _asce_deck()
|
|
panel = schema.to_panel(deck)
|
|
result, reason = pa.solve_in_place(deck, panel)
|
|
assert reason is None and result is not None and not result.refusal
|
|
w = result.cases.get('W')
|
|
assert w is not None and w.built # wind case is real
|
|
assert max(abs(v) for v in w.moment_lbft) > 0.0 # it bends the wall
|
|
|
|
|
|
def test_no_restraints_still_refuses_but_wind_is_computed_for_display():
|
|
deck = _asce_deck(restraints=False)
|
|
panel = schema.to_panel(deck)
|
|
result, reason = pa.solve_in_place(deck, panel)
|
|
# in-place needs >=2 restraints; the pressure is still computable for show
|
|
assert result is None and reason
|
|
assert schema.resolve_wind(deck)['source'] == 'computed'
|
|
|
|
|
|
# ------------------------------------------------------------- the rendering
|
|
|
|
def test_page_renders_the_wind_section_pressure_and_gcpi():
|
|
deck = _asce_deck()
|
|
html = pa.build_analysis_page(deck)
|
|
assert 'In-Place Wind (ASCE C&C, computed)' in html
|
|
assert 'Computed C&C wall pressures' in html
|
|
assert 'GCpi' in html and '0.18' in html
|
|
assert 'Traceback' not in html
|
|
m = CJK.search(html)
|
|
assert not m, 'CJK: {0!r}'.format(m.group() if m else '')
|
|
|
|
|
|
def test_page_shows_not_judged_reason_and_no_pressure_for_tall_wall():
|
|
deck = _asce_deck()
|
|
deck['loading']['wind']['asce']['mean_roof_height_ft'] = 80
|
|
html = pa.build_analysis_page(deck)
|
|
assert 'NOT JUDGED' in html
|
|
assert 'Traceback' not in html
|
|
|
|
|
|
def test_cli_end_to_end_renders_wind_under_tmp_path(tmp_path):
|
|
deck = _asce_deck()
|
|
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 and out_path.exists()
|
|
html = io.open(str(out_path), encoding='utf-8').read()
|
|
assert 'In-Place Wind' in html and 'Traceback' not in html
|