feat(536-3): E 보강 검토 앞부분 — 벽 외적 안정(활동 · 전도 · 지지력 · KDS 11 80 05) · 비탈 완화 · 소단 가정 재해석 · 최소 완화 찾기 · E03 「보강 검토」 탭 · E05 보강검토 시트(있을 때만)
- E03_StabSection_WallCheck: Coulomb · Rankine · M-O 토압 · 상재하중 · 수압 · 양압력 · Meyerhof 지지력 · 평상시 / 지진 시 판정 - E03_StabSection_WhatIf: 한 쪽 비탈 1:n(+ 소단) 다시 그림 · 원지반 만나는 가장 완만한 n · 성긴 걸음 + 이분 최소 완화(완만할수록 늘 안전하지 않음) - E03_StabSection_Router_WhatIf: POST whatif · wallcheck · GET whatif · 저장본 _derived/stab2d_whatif(+ _view) · 원본 측점 · 요약 그대로 - E05: s8 보강검토(양식 · 자리표) · 저장본 없으면 시트 뺌 · 갱신 때 사라진 프로그램 몫 뺌 - 시험 test_536_3 12 · 534-3 양식 시험 시트 8(보강검토 빈 판 1쪽) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3078,6 +3078,351 @@
|
||||
21
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s8",
|
||||
"이름": "보강검토",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "보강 검토 (가정)",
|
||||
"서식": 1
|
||||
},
|
||||
"B1": {
|
||||
"서식": 1
|
||||
},
|
||||
"C1": {
|
||||
"서식": 1
|
||||
},
|
||||
"D1": {
|
||||
"서식": 1
|
||||
},
|
||||
"E1": {
|
||||
"서식": 1
|
||||
},
|
||||
"F1": {
|
||||
"서식": 1
|
||||
},
|
||||
"G1": {
|
||||
"서식": 1
|
||||
},
|
||||
"H1": {
|
||||
"서식": 1
|
||||
},
|
||||
"I1": {
|
||||
"서식": 1
|
||||
},
|
||||
"A2": {
|
||||
"값": "E 안 가정 검토 — 비탈 완화 · 소단은 형상만 바꿔 다시 해석 · 설계(임도)에 안 돌려 씀 · 문헌 추정치",
|
||||
"서식": 2
|
||||
},
|
||||
"B2": {
|
||||
"서식": 2
|
||||
},
|
||||
"C2": {
|
||||
"서식": 2
|
||||
},
|
||||
"D2": {
|
||||
"서식": 2
|
||||
},
|
||||
"E2": {
|
||||
"서식": 2
|
||||
},
|
||||
"F2": {
|
||||
"서식": 2
|
||||
},
|
||||
"G2": {
|
||||
"서식": 2
|
||||
},
|
||||
"H2": {
|
||||
"서식": 2
|
||||
},
|
||||
"I2": {
|
||||
"서식": 2
|
||||
},
|
||||
"A4": {
|
||||
"값": "1. 비탈 완화 · 소단 가정 — 전후 안전율 (Bishop)",
|
||||
"서식": 3
|
||||
},
|
||||
"B4": {
|
||||
"서식": 3
|
||||
},
|
||||
"C4": {
|
||||
"서식": 3
|
||||
},
|
||||
"D4": {
|
||||
"서식": 3
|
||||
},
|
||||
"E4": {
|
||||
"서식": 3
|
||||
},
|
||||
"F4": {
|
||||
"서식": 3
|
||||
},
|
||||
"G4": {
|
||||
"서식": 3
|
||||
},
|
||||
"H4": {
|
||||
"서식": 3
|
||||
},
|
||||
"I4": {
|
||||
"서식": 3
|
||||
},
|
||||
"A5": {
|
||||
"값": "측점",
|
||||
"서식": 6
|
||||
},
|
||||
"B5": {
|
||||
"값": "비탈",
|
||||
"서식": 6
|
||||
},
|
||||
"C5": {
|
||||
"값": "가정",
|
||||
"서식": 6
|
||||
},
|
||||
"D5": {
|
||||
"값": "조건",
|
||||
"서식": 6
|
||||
},
|
||||
"E5": {
|
||||
"값": "전 Fs",
|
||||
"서식": 6
|
||||
},
|
||||
"F5": {
|
||||
"값": "후 Fs",
|
||||
"서식": 6
|
||||
},
|
||||
"G5": {
|
||||
"값": "기준",
|
||||
"서식": 6
|
||||
},
|
||||
"H5": {
|
||||
"값": "전 판정",
|
||||
"서식": 6
|
||||
},
|
||||
"I5": {
|
||||
"값": "후 판정",
|
||||
"서식": 6
|
||||
},
|
||||
"A6": {
|
||||
"서식": 7
|
||||
},
|
||||
"B6": {
|
||||
"서식": 7
|
||||
},
|
||||
"C6": {
|
||||
"서식": 5
|
||||
},
|
||||
"D6": {
|
||||
"서식": 7
|
||||
},
|
||||
"E6": {
|
||||
"서식": 8
|
||||
},
|
||||
"F6": {
|
||||
"서식": 8
|
||||
},
|
||||
"G6": {
|
||||
"서식": 8
|
||||
},
|
||||
"H6": {
|
||||
"서식": 7
|
||||
},
|
||||
"I6": {
|
||||
"서식": 7
|
||||
},
|
||||
"A8": {
|
||||
"값": "2. 벽 외적 안정 — 활동 · 전도 · 지지력 (KDS 11 80 05)",
|
||||
"서식": 3
|
||||
},
|
||||
"B8": {
|
||||
"서식": 3
|
||||
},
|
||||
"C8": {
|
||||
"서식": 3
|
||||
},
|
||||
"D8": {
|
||||
"서식": 3
|
||||
},
|
||||
"E8": {
|
||||
"서식": 3
|
||||
},
|
||||
"F8": {
|
||||
"서식": 3
|
||||
},
|
||||
"G8": {
|
||||
"서식": 3
|
||||
},
|
||||
"H8": {
|
||||
"서식": 3
|
||||
},
|
||||
"I8": {
|
||||
"서식": 3
|
||||
},
|
||||
"A9": {
|
||||
"값": "측점",
|
||||
"서식": 6
|
||||
},
|
||||
"B9": {
|
||||
"값": "벽 · 경우",
|
||||
"서식": 6
|
||||
},
|
||||
"C9": {
|
||||
"값": "활동 Fs",
|
||||
"서식": 6
|
||||
},
|
||||
"D9": {
|
||||
"값": "기준",
|
||||
"서식": 6
|
||||
},
|
||||
"E9": {
|
||||
"값": "전도 Fs",
|
||||
"서식": 6
|
||||
},
|
||||
"F9": {
|
||||
"값": "기준",
|
||||
"서식": 6
|
||||
},
|
||||
"G9": {
|
||||
"값": "지지력 Fs",
|
||||
"서식": 6
|
||||
},
|
||||
"H9": {
|
||||
"값": "기준",
|
||||
"서식": 6
|
||||
},
|
||||
"I9": {
|
||||
"값": "판정",
|
||||
"서식": 6
|
||||
},
|
||||
"A10": {
|
||||
"서식": 7
|
||||
},
|
||||
"B10": {
|
||||
"서식": 5
|
||||
},
|
||||
"C10": {
|
||||
"서식": 8
|
||||
},
|
||||
"D10": {
|
||||
"서식": 8
|
||||
},
|
||||
"E10": {
|
||||
"서식": 8
|
||||
},
|
||||
"F10": {
|
||||
"서식": 8
|
||||
},
|
||||
"G10": {
|
||||
"서식": 8
|
||||
},
|
||||
"H10": {
|
||||
"서식": 8
|
||||
},
|
||||
"I10": {
|
||||
"서식": 7
|
||||
},
|
||||
"A12": {
|
||||
"값": "가정 · 벽 검토 없음 — E03 [보강 검토] 뒤 [보고서 갱신]",
|
||||
"서식": 5
|
||||
},
|
||||
"B12": {
|
||||
"서식": 5
|
||||
},
|
||||
"C12": {
|
||||
"서식": 5
|
||||
},
|
||||
"D12": {
|
||||
"서식": 5
|
||||
},
|
||||
"E12": {
|
||||
"서식": 5
|
||||
},
|
||||
"F12": {
|
||||
"서식": 5
|
||||
},
|
||||
"G12": {
|
||||
"서식": 5
|
||||
},
|
||||
"H12": {
|
||||
"서식": 5
|
||||
},
|
||||
"I12": {
|
||||
"서식": 5
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
"A1:I1",
|
||||
"A2:I2",
|
||||
"A4:I4",
|
||||
"A8:I8",
|
||||
"A12:I12"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
"폭": 11
|
||||
},
|
||||
"B": {
|
||||
"폭": 14
|
||||
},
|
||||
"C": {
|
||||
"폭": 20
|
||||
},
|
||||
"D": {
|
||||
"폭": 8
|
||||
},
|
||||
"E": {
|
||||
"폭": 9
|
||||
},
|
||||
"F": {
|
||||
"폭": 9
|
||||
},
|
||||
"G": {
|
||||
"폭": 9
|
||||
},
|
||||
"H": {
|
||||
"폭": 9
|
||||
},
|
||||
"I": {
|
||||
"폭": 9
|
||||
}
|
||||
},
|
||||
"행": {
|
||||
"1": {
|
||||
"높이": 36
|
||||
},
|
||||
"2": {
|
||||
"높이": 30
|
||||
},
|
||||
"4": {
|
||||
"높이": 22
|
||||
},
|
||||
"5": {
|
||||
"높이": 22
|
||||
},
|
||||
"8": {
|
||||
"높이": 22
|
||||
},
|
||||
"9": {
|
||||
"높이": 22
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
"눈금선": false
|
||||
},
|
||||
"인쇄": {
|
||||
"용지": "A4",
|
||||
"방향": "portrait",
|
||||
"맞춤": {
|
||||
"폭": 1,
|
||||
"높이": 0
|
||||
},
|
||||
"여백": {
|
||||
"위": 15,
|
||||
"아래": 15,
|
||||
"왼": 10,
|
||||
"오른": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"활성": "s1"
|
||||
|
||||
@@ -90,3 +90,13 @@
|
||||
| 22 행부터 보통 · 높음 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 측점 등급 + SI<1 비율 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 |
|
||||
|
||||
- 22 행 서식을 줄마다 복사. 측점 등급 = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI < 1(등급 4 이상) 셀 비율 — 낮음 < 5 % · 보통 5~20 % · 높음 ≥ 20 %(문턱 = E07 설정).
|
||||
|
||||
## s8 보강검토 (프로그램 몫 · E03 보강 검토 저장본 · 없으면 시트를 뺌)
|
||||
|
||||
| 칸 | 내용 |
|
||||
| --- | --- |
|
||||
| 6 행부터 가정 하나 × 조건 하나 = 한 줄 | A 측점 · B 비탈(좌 · 우 · 깎기 · 쌓기)(서식 7) · C 가정 글(1:n 전 → 후 · 소단 폭 / 높이 · 최소 완화)(5) · D 조건(7) · E 전 Fs · F 후 Fs · G 기준(8) · H 전 판정 · I 후 판정(7 → N.G 10 · O.K 11) · 없으면 「가정 없음」 |
|
||||
| 가정 줄 다음 한 줄 띄고 `8` · `9` 행 꼴(제목 3 · 머리 6) | 2. 벽 외적 안정 |
|
||||
| 그 아래 벽 하나 × 경우 하나 = 한 줄 | A 측점(7) · B 벽 이름 · 평상시 / 지진 시 · 토압법 · 배면 수위(5) · C 활동 · E 전도 · G 지지력 Fs(8) · D · F · H 기준(8 · KDS 11 80 05) · I 판정(7 → 10 · 11) · 없으면 「벽 검토 없음」 |
|
||||
|
||||
- 6 행 · 10 행 서식을 줄마다 복사 · 저장본 = (측점 · 종류 · 비탈)마다 가장 최근 하나.
|
||||
|
||||
@@ -36,7 +36,7 @@ STYLE_KEYS = {
|
||||
} # fmt: skip
|
||||
PRINT_KEYS = {"용지", "방향", "배율", "맞춤", "여백", "머리행", "쪽나눔"}
|
||||
A1 = re.compile(r"^([A-Z]{1,3})(\d+)$")
|
||||
NAMES = ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약"]
|
||||
NAMES = ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "보강검토"]
|
||||
|
||||
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
@@ -148,7 +148,7 @@ def test_양식_계약():
|
||||
book = _form()
|
||||
_contract(book)
|
||||
assert [s["이름"] for s in book["시트"]] == NAMES
|
||||
assert [s["id"] for s in book["시트"]] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7"]
|
||||
assert [s["id"] for s in book["시트"]] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8"]
|
||||
|
||||
|
||||
def test_양식_인쇄_쪽나눔_머리행_그림():
|
||||
@@ -167,6 +167,7 @@ def test_양식_인쇄_쪽나눔_머리행_그림():
|
||||
"대표단면": 1,
|
||||
"3D요약": 1,
|
||||
"광역요약": 0,
|
||||
"보강검토": 0,
|
||||
}
|
||||
# N.G 빨강 조건부 서식 — 측점표 판정 열 · 서식 표에 N.G 칸 서식(빌더가 인쇄 · xlsx 용으로 직접 씀)
|
||||
assert by["측점표"]["조건부서식"][0]["범위"][0]["c0"] == 9
|
||||
@@ -230,8 +231,8 @@ def test_견본_pdf_쪽수_머리행(png):
|
||||
)
|
||||
sections = [i for i, t in enumerate(texts) if "대표 단면" in t and "안전율 Fs" in t]
|
||||
assert len(sections) == 2 # 단면마다 한 쪽
|
||||
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1
|
||||
assert len(pages) == 1 + 1 + 1 + len(stab) + 2 + 1 + 1
|
||||
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1 + 보강 1(양식 빈 판)
|
||||
assert len(pages) == 1 + 1 + 1 + len(stab) + 2 + 1 + 1 + 1
|
||||
assert all(float(pages[i].mediabox.width) > float(pages[i].mediabox.height) for i in stab)
|
||||
html = spreadsheet.print_htmls([{"book": book, "figures": {k: png for k in figs}}])[0]
|
||||
assert html.count("<img ") == 3 # 단면 2 + 3D 띠 1
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""536-3 E03 보강 검토 — 벽 외적 안정 · 비탈 완화 · 소단 가정 재해석 · E05 보강검토 시트.
|
||||
|
||||
- 벽 외적 안정(순수): 교과서 계수표(Coulomb · Rankine · Meyerhof) · M-O 닫힌 꼴 · 손계산 예제
|
||||
(직사각 벽 · 상재하중 · 수압 · 뒷굽 위 흙) ±2 %
|
||||
- 가정 형상(순수): 1:n 비탈이 원지반과 만나는 점 = 해석해 · 소단 꼭짓점 · 최소 완화 찾기(단조 · 창)
|
||||
- 길(검증_sub2 동결 복사 · 벽 측점 + 하나): 지금 기울기 그대로 = 원본 안전율(±3 %) · 최소 완화가
|
||||
목표 안전율을 맞춤 · 원본 측점 · 요약 저장본 그대로 · 벽 측점 외적 안정 · 벽 쪽 비탈 가정 거절 ·
|
||||
E05 보강검토 시트(있을 때만)
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_536_3_e03_reinforce.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from common_util import common_util_project_copy as cp # noqa: E402
|
||||
from common_util.common_util_station_key import chainage_key # noqa: E402
|
||||
from E01_StabRegister import E01_StabRegister_Source as src # noqa: E402
|
||||
from E02_StabGround import E02_StabGround_Router as e02 # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Router as router # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Router_WhatIf as wf # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Worker as worker # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_WallCheck as wc # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_WhatIf as whatif # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
|
||||
|
||||
SOURCE_ID = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(읽기만)
|
||||
PID = "e" * 8 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 12
|
||||
WALL_KEY = "351.00" # 검증_sub2 기슭막이 측점
|
||||
SOIL = {"gamma_t": 18.0, "gamma_sat": 19.0, "c": 0.0, "phi": 30.0}
|
||||
|
||||
|
||||
def close(a: float, b: float, tol: float = 0.02) -> bool:
|
||||
return abs(a - b) <= tol * abs(b)
|
||||
|
||||
|
||||
# ── 벽 외적 안정 ─────────────────────────────────────────────────────────────
|
||||
def test_토압_지지력_계수_교과서표():
|
||||
"""Das(기초공학) 표 — Coulomb δ = 2/3φ · Rankine 경사 배면 · Meyerhof 지지력 계수."""
|
||||
assert close(wc.ka_coulomb(30, 20, 0, 0), 0.2973)
|
||||
assert close(wc.ka_coulomb(30, 0, 0, 0), 1 / 3)
|
||||
assert close(wc.ka_rankine(30, 0), 1 / 3)
|
||||
assert close(wc.ka_rankine(30, 10), 0.3495)
|
||||
nc, nq, ng = wc.bearing_factors(30)
|
||||
assert close(nc, 30.14) and close(nq, 18.40) and close(ng, 15.67)
|
||||
assert wc.bearing_factors(0) == (5.14, 1.0, 0.0)
|
||||
|
||||
|
||||
def test_MO_닫힌꼴():
|
||||
"""δ = θ = β = 0 · kv = 0 — KAE = cos²(φ−ψ) / (cos²ψ [1 + √(sinφ sin(φ−ψ) / cosψ)]²) · ψ = 0 이면 Coulomb."""
|
||||
for phi, kh in ((30, 0.2), (35, 0.15)):
|
||||
psi = math.atan(kh)
|
||||
f = math.radians(phi)
|
||||
want = math.cos(f - psi) ** 2 / (
|
||||
math.cos(psi) ** 2
|
||||
* (1 + math.sqrt(math.sin(f) * math.sin(f - psi) / math.cos(psi))) ** 2
|
||||
)
|
||||
assert close(wc.ka_coulomb(phi, 0, 0, 0, math.degrees(psi)), want, 1e-9)
|
||||
assert close(wc.ka_coulomb(30, 0, 0, 0, math.degrees(math.atan(0.2))), 0.4733)
|
||||
assert wc.ka_coulomb(32, 21, 5, 10, 0.0) == pytest.approx(wc.ka_coulomb(32, 21, 5, 10))
|
||||
|
||||
|
||||
RECT = [[0, 0], [2, 0], [2, 4], [0, 4]] # B 2 · H 4 · 배면 = +x(뒷굽 x = 2)
|
||||
|
||||
|
||||
def test_직사각_벽_손계산():
|
||||
"""Rankine · q 0 · 물 없음 · μ 0.5 — W 184 · Pa 48(H/3) · 활동 1.917 · 전도 2.875 ·
|
||||
e = 1 − 0.6522 · q_max · Meyerhof q_ult(유효폭 · 경사 계수)."""
|
||||
r = wc.check_wall(
|
||||
RECT, +1, backfill=SOIL, foundation=SOIL, method="rankine",
|
||||
surcharge_kpa=0.0, base_friction=0.5,
|
||||
) # fmt: skip
|
||||
W, Pa = 2 * 4 * 23.0, 0.5 * 18 * 16 / 3
|
||||
assert close(r["loads"]["W"], W) and close(r["loads"]["Pa"], Pa) and r["loads"]["W_soil"] == 0
|
||||
assert close(r["fs"]["sliding"], W * 0.5 / Pa)
|
||||
m_res, m_over = W * 1.0, Pa * 4 / 3
|
||||
assert close(r["fs"]["overturning"], m_res / m_over)
|
||||
xbar = (m_res - m_over) / W
|
||||
e = 1.0 - xbar
|
||||
q_max = W / 2 * (1 + 6 * e / 2) if e <= 2 / 6 else 2 * W / (3 * xbar)
|
||||
alpha = math.degrees(math.atan(Pa / W))
|
||||
q_ult = 0.5 * 18 * (2 - 2 * e) * 15.668 * (1 - alpha / 30) ** 2
|
||||
assert close(r["base"]["q_max"], q_max) and close(r["fs"]["bearing"], q_ult / q_max)
|
||||
assert r["case"] == "normal" and r["required"] == wc.REQUIRED["normal"]
|
||||
assert r["ok"] == {k: r["fs"][k] >= r["required"][k] for k in r["fs"]}
|
||||
|
||||
|
||||
def test_상재하중_수압_양압력():
|
||||
"""q 10 · 배면 수위 = 벽 높이 — Pa = Ka(½γ'H² + qH) · Pw = ½γwH² · U = ½γwHB · ΣH · V."""
|
||||
r = wc.check_wall(
|
||||
RECT, +1, backfill=SOIL, foundation=SOIL, method="rankine",
|
||||
surcharge_kpa=10.0, water_ratio=1.0, base_friction=0.5,
|
||||
) # fmt: skip
|
||||
g = SOIL["gamma_sat"] - wc.GAMMA_W
|
||||
Pa = (0.5 * g * 16 + 10 * 4) / 3
|
||||
Pw, U = 0.5 * wc.GAMMA_W * 16, 0.5 * wc.GAMMA_W * 4 * 2
|
||||
assert close(r["loads"]["Pa"], Pa) and close(r["loads"]["Pw"], Pw) and close(r["loads"]["U"], U)
|
||||
assert close(r["loads"]["H"], Pa + Pw) and close(r["loads"]["V"], 184 - U)
|
||||
arm = (0.5 * g * 16 * 4 / 3 + 40 * 2) / (0.5 * g * 16 + 40) # 삼각 + 사각 합력 높이
|
||||
assert close(r["loads"]["Pa_arm"], arm)
|
||||
m_over = Pa * arm + Pw * 4 / 3 + U * 4 / 3
|
||||
assert close(r["loads"]["M_overturn"], m_over)
|
||||
|
||||
|
||||
def test_L형_벽_뒷굽_위_흙_지진():
|
||||
"""역 T 형(앞굽 0 · 뒷굽 3 · 벽체 0.6~1.0) — Rankine 가상면 위 흙 = (3 − 1.0) × 4.5 × γ ·
|
||||
Coulomb 가상면 θ = atan(2 / 5) · 지진 시 = M-O 증분 0.6H + 관성 · 기준 = 지진 시 표."""
|
||||
L = [[0, 0], [3, 0], [3, 0.5], [1.0, 0.5], [1.0, 5], [0.6, 5], [0.6, 0.5], [0, 0.5]]
|
||||
r = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL, method="rankine")
|
||||
assert close(r["loads"]["W_soil"], 2.0 * 4.5 * 18)
|
||||
c = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL)
|
||||
assert close(c["geometry"]["theta_deg"], math.degrees(math.atan(2 / 5)))
|
||||
# 가상면(x = 3 − 0.4z)과 벽체(x = 1.0) 사이 · 0.5 ≤ z ≤ 5 → ∫(2 − 0.4z)dz = 4.05 m²
|
||||
assert close(c["loads"]["W_soil"], 4.05 * 18)
|
||||
s = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL, kh=0.15)
|
||||
assert s["case"] == "seismic" and s["required"] == wc.REQUIRED["seismic"]
|
||||
assert s["coefficients"]["kae"] > c["coefficients"]["ka"]
|
||||
assert s["loads"]["H"] > c["loads"]["H"] and s["fs"]["sliding"] < c["fs"]["sliding"]
|
||||
|
||||
|
||||
def test_배면이_반대쪽이면_거울():
|
||||
a = wc.check_wall(RECT, +1, backfill=SOIL, foundation=SOIL)
|
||||
b = wc.check_wall([[-x, z] for x, z in RECT], -1, backfill=SOIL, foundation=SOIL)
|
||||
for k in a["fs"]:
|
||||
assert a["fs"][k] == pytest.approx(b["fs"][k])
|
||||
|
||||
|
||||
# ── 가정 형상 · 최소 완화 찾기 ────────────────────────────────────────────────
|
||||
GROUND = [(-40.0, 120.0), (0.0, 100.0), (40.0, 80.0)] # 1:2 비탈(좌 = +x 낮음)
|
||||
LINE = [(-3.0, 101.5), (-2.0, 100.0), (2.0, 100.0), (3.0, 99.5)]
|
||||
|
||||
|
||||
def test_비탈_다시_그림_해석해():
|
||||
"""지반 z = 100 − x/2 · 우(−x) 깎기 시작 (−2, 100) · 1:n — u = −x 에서 (u − 2)/n = u/2 →
|
||||
u = 2 / (1 − n/2) · z = 100 + u/2 · n ≥ 2 = 못 만남. 좌 쌓기 (2, 100) 1:1.5 → (8, 96)."""
|
||||
for n in (0.5, 1.0, 1.5):
|
||||
line = whatif.reshape_side(LINE, GROUND, side="right", role="cut", start_x=-2.0, ratio=n)
|
||||
u = 2.0 / (1.0 - n / 2)
|
||||
x, z = min(line)
|
||||
assert x == pytest.approx(-u) and z == pytest.approx(100.0 + u / 2)
|
||||
assert [p for p in line if p[0] > -2.0 + 1e-9] == [list(p) for p in LINE[2:]]
|
||||
gentle = dict(side="right", role="cut", start_x=-2.0, ratio=2.5)
|
||||
assert whatif.reshape_side(LINE, GROUND, **gentle) is None
|
||||
line = whatif.reshape_side(
|
||||
LINE, GROUND, side="right", role="cut", start_x=-2.0, ratio=0.5, bench_w=1.0, bench_h=1.0
|
||||
)
|
||||
corners = [p for p in line if -3.6 < p[0] < -2.4]
|
||||
assert [[round(x, 6), round(z, 6)] for x, z in corners] == [[-3.5, 101.0], [-2.5, 101.0]]
|
||||
fill = whatif.reshape_side(LINE, GROUND, side="left", role="fill", start_x=2.0, ratio=1.5)
|
||||
assert max(fill) == pytest.approx([8.0, 96.0])
|
||||
assert [p for p in fill if p[0] < 2.0 - 1e-9] == [list(p) for p in LINE[:2]]
|
||||
assert (
|
||||
whatif.reshape_side(LINE, GROUND, side="left", role="fill", start_x=2.0, ratio=3.0) is None
|
||||
)
|
||||
|
||||
|
||||
def test_최소_완화_찾기():
|
||||
found = whatif.search_ratio(lambda n: n >= 1.23, 0.4)
|
||||
assert found["found"] == pytest.approx(1.25)
|
||||
window = whatif.search_ratio(lambda n: 1.0 <= n <= 1.3, 0.4) # 완만할수록 늘 안전하지 않음
|
||||
assert 1.0 <= window["found"] <= 1.3
|
||||
assert whatif.search_ratio(lambda n: False, 0.4, max_ratio=1.1)["found"] is None
|
||||
assert whatif.search_ratio(lambda n: True, 0.4) == {"found": 0.4, "tried": [(0.4, True)]}
|
||||
assert len(whatif.search_ratio(lambda n: n >= 3.9, 0.4)["tried"]) <= 10
|
||||
|
||||
|
||||
# ── 길(검증_sub2 동결 복사) ──────────────────────────────────────────────────
|
||||
async def _copy(target: Path) -> None:
|
||||
import aiomysql
|
||||
|
||||
from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER
|
||||
|
||||
connection = await aiomysql.connect(
|
||||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, autocommit=True
|
||||
)
|
||||
try:
|
||||
await cp.copy_project(
|
||||
connection, SOURCE_ID, scope="stability", new_id=PID, project_root=target
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def frozen():
|
||||
folder = Path(tempfile.mkdtemp(prefix="aislo_e03_rf_"))
|
||||
try:
|
||||
asyncio.run(_copy(folder / "copy"))
|
||||
except Exception as error: # noqa: BLE001
|
||||
shutil.rmtree(folder, ignore_errors=True)
|
||||
pytest.skip(f"검증 원본 복사 실패: {error}")
|
||||
yield folder / "copy"
|
||||
shutil.rmtree(folder, ignore_errors=True)
|
||||
|
||||
|
||||
def _fast(cross, design, **kw):
|
||||
"""진짜 엔진 — Bishop 원호만(빠르게)."""
|
||||
return analyze_station(cross, design, **{**kw, "methods": ("bishop",), "noncircular": False})
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def project(frozen):
|
||||
"""벽 측점 + 깎기 비탈 있는 벽 없는 측점 하나 · 진짜 엔진(빠르게) · [해석] 한 번."""
|
||||
mp = pytest.MonkeyPatch()
|
||||
original = src.stations
|
||||
|
||||
def pick(root):
|
||||
rows = original(root)
|
||||
wall = [r for r in rows if chainage_key(r["chainage_m"]) == WALL_KEY]
|
||||
rest = [r for r in rows if chainage_key(r["chainage_m"]) != WALL_KEY]
|
||||
return wall + rest[10:11]
|
||||
|
||||
mp.setattr(router.src, "stations", pick)
|
||||
|
||||
async def fake_project(_pid):
|
||||
return frozen, {"company_id": 1, "project_kind": "STABILITY"}
|
||||
|
||||
mp.setattr(e02, "_project", fake_project)
|
||||
mp.setattr(worker, "analyze_station", _fast)
|
||||
mp.setattr(wf, "analyze_station", _fast)
|
||||
mp.setattr(router, "WORKERS", 1)
|
||||
router._JOBS.clear()
|
||||
|
||||
async def go():
|
||||
await router.analyze2d(UUID(PID))
|
||||
while router._TASKS:
|
||||
await asyncio.gather(*list(router._TASKS))
|
||||
|
||||
asyncio.run(go())
|
||||
yield frozen
|
||||
mp.undo()
|
||||
|
||||
|
||||
def _stamps(root: Path) -> dict[str, bytes]:
|
||||
out = {}
|
||||
for kind in (router.KIND_STATION, router.KIND_SUMMARY):
|
||||
for p in (root / "_derived" / kind).glob("*.json"):
|
||||
out[str(p)] = p.read_bytes() # 읽기가 고친 때를 건드림(최근 표시) — 내용만 견줌
|
||||
return out
|
||||
|
||||
|
||||
def _plain_station(root: Path) -> tuple[str, str, float, dict]:
|
||||
summary, _ = router._latest_summary(root, "")
|
||||
row = next(r for r in summary["stations"] if r["key"] != WALL_KEY)
|
||||
side = next(s for s in row["sides"] if s["role"] == "cut")["side"]
|
||||
return row["key"], side, row["cases"]["dry"][side]["fs"], row
|
||||
|
||||
|
||||
def test_지금_기울기_그대로면_원본_안전율(project):
|
||||
key, side, before, _ = _plain_station(project)
|
||||
rec = wf.run_whatif(project, key, wf.WhatIfRequest(side=side))
|
||||
assert rec["assume"]["ratio"] == rec["assume"]["ratio_before"]
|
||||
assert close(rec["after"]["dry"]["fs"], before, 0.03)
|
||||
assert rec["view"]["analyses"] and rec["before"]["dry"]["fs"] == before
|
||||
|
||||
|
||||
def test_최소_완화가_목표를_맞춤_원본_불변(project):
|
||||
key, side, before, _ = _plain_station(project)
|
||||
stamps = _stamps(project)
|
||||
target = round(before + 0.02, 3)
|
||||
rec = wf.run_whatif(
|
||||
project,
|
||||
key,
|
||||
wf.WhatIfRequest(side=side, search=True, condition="dry", target_fs=target),
|
||||
)
|
||||
search = rec["search"]
|
||||
if search["found"] is None: # 완화가 깎기 높이를 키워 오히려 낮아지는 지형 — 끝값까지 봄
|
||||
assert all(not ok for _, ok in search["tried"])
|
||||
else:
|
||||
assert rec["after"]["dry"]["fs"] >= target - 1e-9
|
||||
assert rec["assume"]["ratio"] == search["found"] > rec["assume"]["ratio_before"]
|
||||
steeper = [n for n, ok in search["tried"] if n < search["found"]]
|
||||
assert all(not ok for n, ok in search["tried"] if n in steeper)
|
||||
assert _stamps(project) == stamps # 원본 측점 · 요약 저장본 그대로
|
||||
again = wf.run_whatif(
|
||||
project,
|
||||
key,
|
||||
wf.WhatIfRequest(side=side, search=True, condition="dry", target_fs=target),
|
||||
)
|
||||
assert again["computed_at"] == rec["computed_at"] # 같은 가정 = 저장본
|
||||
|
||||
|
||||
def test_벽_측점_외적_안정_벽쪽_가정_거절(project):
|
||||
rec = wf.run_wallcheck(project, WALL_KEY, wf.WallRequest(kh=0.15))
|
||||
assert len(rec["walls"]) == 1
|
||||
wall = rec["walls"][0]
|
||||
assert [c["case"] for c in wall["cases"]] == ["normal", "seismic"]
|
||||
for c in wall["cases"]:
|
||||
assert c["required"] == wc.REQUIRED[c["case"]]
|
||||
assert all(v > 0 for v in c["fs"].values())
|
||||
assert c["geometry"]["H"] == pytest.approx(1.5, abs=0.05)
|
||||
with pytest.raises(HTTPException) as error:
|
||||
wf.run_whatif(project, WALL_KEY, wf.WhatIfRequest(side=wall["side"]))
|
||||
assert error.value.status_code == 422
|
||||
|
||||
|
||||
def test_E05_보강검토_시트(project, monkeypatch):
|
||||
from E05_StabReport import E05_StabReport_Build as build
|
||||
|
||||
recs = wf.records(project)
|
||||
assert {r["type"] for r in recs} == {"slope", "wall"}
|
||||
book = build.form()
|
||||
build.build_program(book, build.gather(project))
|
||||
s8 = next(s for s in book["시트"] if s["id"] == "s8")
|
||||
cells = s8["칸"]
|
||||
slopes = [r for r in recs if r["type"] == "slope"]
|
||||
rows = sum(len(r["after"]) for r in slopes)
|
||||
assert all(cells[f"D{6 + i}"]["값"] in ("건기", "우기", "지진") for i in range(rows))
|
||||
wall_rows = [a1 for a1, c in cells.items() if a1[0] == "B" and "평상시" in str(c.get("값"))]
|
||||
assert len(wall_rows) == 1 and cells[f"I{wall_rows[0][1:]}"]["값"] in ("O.K", "N.G")
|
||||
monkeypatch.setattr(build.reinforce, "read", lambda root: [])
|
||||
empty = build.form()
|
||||
build.build_program(empty, build.gather(project))
|
||||
assert "s8" not in {s["id"] for s in empty["시트"]} # 없으면 시트 뺌
|
||||
Reference in New Issue
Block a user