# -*- 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