Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user