- 부품 처음값 spoil_bank 형상 · 3D = none · 형상 부품 「사토(성토 채움)」 지움 - 사토 단면 셈 파이썬 · TS 짝 지움(Engine_SpoilFill · common_util_spoil_fill · spoil_fill 옵션 · fill_area_beyond) - 카드 그림 · 면적표 줄 · 모달 띠 · 고름 선 · B07 표 「사토장」 칸 지움(빈 줄로 되돌림) - B08 수량 · 유토 배분(haul-plan) 미사용 확인 · 남은 사토 값 = [저장] 서버 재계산 · 브라우저 단계 다시 셈으로 정리 - 시험 고쳐 씀: 322_1 · 349_0n · b06_spoil_fill_allocation(남은 값 정리) · spoil_fill_mirror(죽은 길) · 52_9 · cross_design_mirror · 150 · 164 · 179_4 · 316 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
521 lines
20 KiB
Python
521 lines
20 KiB
Python
"""횡단 설계선·단면적 거울 테스트 — 파이썬 엔진과 TS 짝이 **같은 값**을 내는지 대조한다.
|
||
|
||
짝: `B06_Section/B06_Section_Engine_Design.py` (`compute_cross_design`)
|
||
↔ `common_util/common_util_cross_design.ts` (`computeCrossDesign`)
|
||
(단면적은 그 안에서 `..._Engine_Areas.py` ↔ `..._cross_design_areas.ts` 를 탄다)
|
||
|
||
**계산 자리 규칙**(CLAUDE.md 5장)이 「초기값은 서버, 조작 중은 브라우저, 코드는 한 벌」이라
|
||
이 짝은 반드시 같은 값을 내야 한다. 한쪽만 고치면 **화면에 보이는 수량과 저장되는 수량이
|
||
갈린다** — 그런데 이 짝에는 거울 테스트가 없었다(2026-09-07 신설).
|
||
|
||
표준단면 수치는 파이썬 config 를 그대로 TS 에 넘긴다 — 출처가 같아야 기본값 차이가 아니라
|
||
**산식 차이**만 잡힌다(배수관 세트 거울 테스트와 같은 방식).
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||
if str(PROJECT_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||
|
||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||
|
||
# 값이 소수점 아래에서 갈리는지 보려면 자릿수를 그대로 비교해야 한다. 두 쪽 다 반올림해
|
||
# 내보내므로 완전 일치를 기대하되, 부동소수 마지막 자리만 어긋나는 것은 허용한다.
|
||
_TOLERANCE = 1e-9
|
||
|
||
|
||
def _ground(slope: float, *, gap: tuple[float, float] | None = None) -> list[dict]:
|
||
"""가로 -12m ~ +12m 를 0.5m 간격으로 훑은 지반선. `gap` 구간은 결측으로 둔다."""
|
||
samples = []
|
||
offset = -12.0
|
||
while offset <= 12.0001:
|
||
invalid = gap is not None and gap[0] <= offset <= gap[1]
|
||
samples.append(
|
||
{
|
||
"offset_m": round(offset, 3),
|
||
"elevation_m": None if invalid else round(100.0 + slope * offset, 4),
|
||
"valid": not invalid,
|
||
}
|
||
)
|
||
offset += 0.5
|
||
return samples
|
||
|
||
|
||
def _dip_ground() -> list[dict]:
|
||
"""양측 절토 · 우측 노면 끝 너머(−4.5 m 밖)에서 원지반이 급히 내려가는 지반(294-3)."""
|
||
samples = []
|
||
for index in range(49):
|
||
offset = -12.0 + index * 0.5
|
||
z = 101.05 + 0.2 * offset if offset >= -4.5 else 100.15 + 1.0 * (offset + 4.5)
|
||
samples.append({"offset_m": offset, "elevation_m": round(z, 4), "valid": True})
|
||
return samples
|
||
|
||
|
||
def _ridge_ground() -> list[dict]:
|
||
"""우측 −4 m 까지 노면보다 높은 평지(절토) · 그 너머 급히 내려가는 마루(370-1)."""
|
||
return [
|
||
{"offset_m": o, "elevation_m": round(100.3 - 0.6 * max(0.0, -o - 4.0), 4), "valid": True}
|
||
for o in (-12.0 + i * 0.5 for i in range(49))
|
||
]
|
||
|
||
|
||
def _rise_ground() -> list[dict]:
|
||
"""좌측 +3 m 까지 노면보다 낮은 평지(성토) · 그 너머 오르막(370-1)."""
|
||
return [
|
||
{"offset_m": o, "elevation_m": round(99.6 + 0.4 * max(0.0, o - 3.0), 4), "valid": True}
|
||
for o in (-12.0 + i * 0.5 for i in range(49))
|
||
]
|
||
|
||
|
||
# 갈래를 골고루 태운다 — 지반유형 셋 · 단면유형 · 측구 형식 · 포장 · 2단 경사 · 세월교
|
||
# 노면 하강 · 곡선부 확폭(바깥쪽 좌/우) · 지반선 결측 구간.
|
||
CASES = [
|
||
{
|
||
"name": "토사 좌절토 일반측구",
|
||
"samples": _ground(0.35),
|
||
"design_elevation_m": 100.4,
|
||
"options": {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"},
|
||
},
|
||
{
|
||
"name": "리핑암 우절토 L형측구 2단경사",
|
||
"samples": _ground(-0.42),
|
||
"design_elevation_m": 99.6,
|
||
"options": {
|
||
"ground_type": "ripping_rock",
|
||
"section_mode": "right_cut",
|
||
"ditch_side": "right",
|
||
"ditch_type": "l_type",
|
||
"rock_boundary_offset_m": -0.8,
|
||
"two_stage_slope": True,
|
||
},
|
||
},
|
||
{
|
||
"name": "발파암 2단경사 해제",
|
||
"samples": _ground(0.55),
|
||
"design_elevation_m": 101.2,
|
||
"options": {
|
||
"ground_type": "blasting_rock",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"rock_boundary_offset_m": -1.4,
|
||
"two_stage_slope": False,
|
||
},
|
||
},
|
||
{
|
||
# 316-1 — 복합경사 값 없음 = 기본 꺼짐(두 짝 같은 기본)
|
||
"name": "발파암 복합경사 값 없음",
|
||
"samples": _ground(-0.55),
|
||
"design_elevation_m": 99.0,
|
||
"options": {
|
||
"ground_type": "blasting_rock",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"rock_boundary_offset_m": -1.4,
|
||
},
|
||
},
|
||
{
|
||
"name": "포장 + 측구 없음",
|
||
"samples": _ground(0.12),
|
||
"design_elevation_m": 100.05,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"paved": True,
|
||
"ditch_enabled": False,
|
||
},
|
||
},
|
||
{
|
||
"name": "세월교 노면 하강",
|
||
"samples": _ground(0.2),
|
||
"design_elevation_m": 100.6,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "right_cut",
|
||
"ditch_side": "right",
|
||
"surface_drop_m": 0.45,
|
||
},
|
||
},
|
||
{
|
||
"name": "곡선부 확폭 — 바깥쪽 좌",
|
||
"samples": _ground(0.28),
|
||
"design_elevation_m": 100.3,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"plan_radius_m": 18.0,
|
||
"curve_outer_side": "left",
|
||
},
|
||
},
|
||
{
|
||
"name": "곡선부 확폭 — 저장된 확폭량 우",
|
||
"samples": _ground(-0.3),
|
||
"design_elevation_m": 100.9,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "right_cut",
|
||
"ditch_side": "right",
|
||
"plan_radius_m": 25.0,
|
||
"curve_outer_side": "right",
|
||
"curve_widening_m": 0.62,
|
||
},
|
||
},
|
||
{
|
||
"name": "지반선 결측 구간(사면이 안 닫힐 수 있음)",
|
||
"samples": _ground(0.9, gap=(6.0, 12.0)),
|
||
"design_elevation_m": 100.0,
|
||
"options": {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"},
|
||
},
|
||
{
|
||
# 측점 하나만 다른 절토 경사(2026-09-07 사용자 지시) — 표준 1:0.4 를 1:1.0 으로 눕힘.
|
||
# 무릎 아래(암)만 바뀌고 위(토사)는 표준 그대로라, 두 쪽이 같은 지점에서 꺾여야 한다.
|
||
"name": "암 절토 경사를 측점에서 바꿈",
|
||
"samples": _ground(-0.5),
|
||
"design_elevation_m": 99.8,
|
||
"options": {
|
||
"ground_type": "ripping_rock",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"rock_boundary_offset_m": -1.0,
|
||
"two_stage_slope": True,
|
||
"cut_slope_ratio": 1.0,
|
||
},
|
||
},
|
||
{
|
||
# 설계선 쉬프트(139-6) — 쪽마다 노견 확장 · 측구 쪽(절토)과 성토 쪽을 같이 넓힘
|
||
"name": "노견 확장 좌 0.7 · 우 1.2",
|
||
"samples": _ground(0.35),
|
||
"design_elevation_m": 100.4,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"shoulder_extend_left_m": 0.7,
|
||
"shoulder_extend_right_m": 1.2,
|
||
},
|
||
},
|
||
{
|
||
# 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮긴 노견에서(370-1)
|
||
"name": "노견 확장 — 판정은 옮긴 노견",
|
||
"samples": _ground(-0.05),
|
||
"design_elevation_m": 100.0,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "right_cut",
|
||
"ditch_side": "right",
|
||
"shoulder_extend_right_m": 3.0,
|
||
"shoulder_extend_left_m": 2.5,
|
||
},
|
||
},
|
||
{
|
||
# 370-1 — 우 절토 마루를 넘겨 옮김 = 그 쪽 성토 1:n 으로 원지반까지(원지반 직선 아님)
|
||
"name": "마루 너머 = 성토 1:n",
|
||
"samples": _ridge_ground(),
|
||
"design_elevation_m": 100.0,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "right_cut",
|
||
"ditch_side": "right",
|
||
"shoulder_extend_right_m": 3.0,
|
||
},
|
||
},
|
||
{
|
||
# 370-1 — 좌 성토를 오르막으로 옮김 = 그 쪽 절토(측구 자동 판정 따름)
|
||
"name": "성 → 절 넘어감",
|
||
"samples": _rise_ground(),
|
||
"design_elevation_m": 100.0,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"shoulder_extend_left_m": 2.5,
|
||
},
|
||
},
|
||
{
|
||
# 151-2 — 곡선부 확폭과 사면 이동이 같은 쪽에 겹침(확폭 출력엔 이동 몫 안 섞임)
|
||
"name": "확폭 좌 + 사면 이동 좌 0.4",
|
||
"samples": _ground(0.28),
|
||
"design_elevation_m": 100.3,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"plan_radius_m": 18.0,
|
||
"curve_outer_side": "left",
|
||
"shoulder_extend_left_m": 0.4,
|
||
},
|
||
},
|
||
{
|
||
# 286-2 — 물넘이포장 자리 = 측구 없음(좌절토라 자동이면 측구가 섬)
|
||
"name": "물넘이포장 자리 측구 없음",
|
||
"samples": _ground(0.35),
|
||
"design_elevation_m": 100.4,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"paved": True,
|
||
"tilted": True,
|
||
},
|
||
},
|
||
{
|
||
# 287-4 — 물넘이포장 자리 노면 = 경사각도로 지대 낮은 쪽 한 경사(지반이 좌로 오름 = 우가 낮음)
|
||
"name": "물넘이포장 자리 노면 기울임",
|
||
"samples": _ground(0.35),
|
||
"design_elevation_m": 100.4,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"paved": True,
|
||
"tilted": True,
|
||
"tilt_pct": 5.2408,
|
||
},
|
||
},
|
||
{
|
||
# 294-3 — 양측 절토 · 낮은 쪽(우) 노면을 늘여 원지반이 내려와 만나는 자리까지
|
||
"name": "물넘이포장 낮은 쪽 노면 연장",
|
||
"samples": _dip_ground(),
|
||
"design_elevation_m": 100.4,
|
||
"options": {
|
||
"ground_type": "soil",
|
||
"section_mode": "left_cut",
|
||
"ditch_side": "left",
|
||
"paved": True,
|
||
"tilted": True,
|
||
"tilt_pct": 5.0,
|
||
},
|
||
},
|
||
]
|
||
|
||
_TS_RUNNER = """
|
||
import { readFileSync, writeFileSync } from "node:fs";
|
||
import { computeCrossDesign } from "./common_util_cross_design.js";
|
||
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||
const camel = {
|
||
ground_type: "groundType",
|
||
section_mode: "sectionMode",
|
||
ditch_side: "ditchSide",
|
||
ditch_type: "ditchType",
|
||
paved: "paved",
|
||
rock_boundary_offset_m: "rockBoundaryOffsetM",
|
||
two_stage_slope: "twoStageSlope",
|
||
cut_slope_ratio: "cutSlopeRatio",
|
||
ditch_enabled: "ditchEnabled",
|
||
ditch_choice: "ditchChoice",
|
||
surface_drop_m: "surfaceDropM",
|
||
plan_radius_m: "planRadiusM",
|
||
curve_outer_side: "curveOuterSide",
|
||
curve_widening_m: "curveWideningM",
|
||
shoulder_extend_left_m: "shoulderExtendLeftM",
|
||
shoulder_extend_right_m: "shoulderExtendRightM",
|
||
tilted: "tilted",
|
||
tilt_pct: "tiltPct",
|
||
};
|
||
const results = input.cases.map((item) => {
|
||
const options = { standard: input.standard };
|
||
for (const [key, value] of Object.entries(item.options)) {
|
||
options[camel[key]] = value;
|
||
}
|
||
return computeCrossDesign(item.samples, item.design_elevation_m, options);
|
||
});
|
||
writeFileSync(process.argv[3], JSON.stringify(results));
|
||
"""
|
||
|
||
|
||
def _python_results() -> list[dict]:
|
||
results = []
|
||
for case in CASES:
|
||
results.append(
|
||
compute_cross_design(
|
||
case["samples"],
|
||
case["design_elevation_m"],
|
||
standard=STANDARD_CROSS_SECTION,
|
||
**case["options"],
|
||
)
|
||
)
|
||
return results
|
||
|
||
|
||
def _ts_results(tmp_path: Path) -> list[dict]:
|
||
"""TS 짝을 프로젝트 tsc 로 옮겨 실제 코드를 그대로 돌린다."""
|
||
out = tmp_path / "js"
|
||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||
[
|
||
"node",
|
||
str(TSC),
|
||
str(PROJECT_ROOT / "common_util" / "common_util_cross_design.ts"),
|
||
"--outDir",
|
||
str(out),
|
||
"--module",
|
||
"esnext",
|
||
"--target",
|
||
"es2022",
|
||
"--moduleResolution",
|
||
"bundler",
|
||
"--ignoreConfig",
|
||
],
|
||
cwd=str(PROJECT_ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
# tsc 가 낸 상대 import 에는 확장자가 없어 Node ESM 이 못 읽는다(번들러가 붙여 주던 몫).
|
||
# 짝 파일이 서로를 부르므로 여기서 `.js` 를 채워 준다.
|
||
for emitted in out.glob("*.js"):
|
||
emitted.write_text(
|
||
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||
encoding="utf-8",
|
||
)
|
||
(out / "runner.mjs").write_text(_TS_RUNNER, encoding="utf-8")
|
||
payload = tmp_path / "input.json"
|
||
result = tmp_path / "output.json"
|
||
payload.write_text(
|
||
json.dumps(
|
||
{
|
||
"cases": [
|
||
{
|
||
"samples": case["samples"],
|
||
"design_elevation_m": case["design_elevation_m"],
|
||
"options": case["options"],
|
||
}
|
||
for case in CASES
|
||
],
|
||
"standard": STANDARD_CROSS_SECTION,
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
subprocess.run( # noqa: S603
|
||
["node", str(out / "runner.mjs"), str(payload), str(result)],
|
||
cwd=str(PROJECT_ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
return json.loads(result.read_text(encoding="utf-8"))
|
||
|
||
|
||
def _diff(left, right, path: str = "") -> list[str]:
|
||
"""두 값이 갈린 자리를 경로와 함께 모은다 — 어느 항목이 틀렸는지 바로 보이게."""
|
||
if isinstance(left, dict) and isinstance(right, dict):
|
||
problems = []
|
||
for key in sorted(set(left) | set(right)):
|
||
if key not in left:
|
||
problems.append(f"{path}.{key}: 파이썬에 없음(TS={right[key]!r})")
|
||
elif key not in right:
|
||
problems.append(f"{path}.{key}: TS 에 없음(파이썬={left[key]!r})")
|
||
else:
|
||
problems.extend(_diff(left[key], right[key], f"{path}.{key}"))
|
||
return problems
|
||
if isinstance(left, list) and isinstance(right, list):
|
||
if len(left) != len(right):
|
||
return [f"{path}: 길이 {len(left)} vs {len(right)}"]
|
||
problems = []
|
||
for index, (one, other) in enumerate(zip(left, right, strict=True)):
|
||
problems.extend(_diff(one, other, f"{path}[{index}]"))
|
||
return problems
|
||
if isinstance(left, bool) or isinstance(right, bool):
|
||
return [] if left == right else [f"{path}: {left!r} vs {right!r}"]
|
||
if isinstance(left, (int, float)) and isinstance(right, (int, float)):
|
||
if abs(float(left) - float(right)) <= _TOLERANCE:
|
||
return []
|
||
return [f"{path}: {left!r} vs {right!r}"]
|
||
return [] if left == right else [f"{path}: {left!r} vs {right!r}"]
|
||
|
||
|
||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||
def test_횡단_설계선과_단면적이_두_쪽에서_같다(tmp_path: Path) -> None:
|
||
expected = _python_results()
|
||
actual = _ts_results(tmp_path)
|
||
assert len(actual) == len(expected)
|
||
problems: list[str] = []
|
||
for case, left, right in zip(CASES, expected, actual, strict=True):
|
||
problems.extend(_diff(left, right, case["name"]))
|
||
assert not problems, "짝이 갈림:\n" + "\n".join(problems[:40])
|
||
|
||
|
||
def test_사토장_단면_칸_없음(tmp_path: Path) -> None:
|
||
"""380-1 — 사토장은 횡단에 안 그림 · 사토 단면 셈 지움 — 두 쪽 결과 모두 `spoil_fill_*` 칸 없음."""
|
||
for result in [*_python_results(), *_ts_results(tmp_path)]:
|
||
assert not [key for key in result if key.startswith("spoil_fill")]
|
||
|
||
|
||
def test_시험이_빈_결과로_통과하지_않는다() -> None:
|
||
"""면적이 실제로 나오는 경우가 섞여 있어야 대조가 뜻을 가진다."""
|
||
results = _python_results()
|
||
assert len(results) == len(CASES)
|
||
assert sum(1 for item in results if item["cut_area_m2"] > 0) >= 4
|
||
assert sum(1 for item in results if item["fill_area_m2"] > 0) >= 1
|
||
assert any(item.get("widening_left_m") or item.get("widening_right_m") for item in results)
|
||
assert any(item["cut_rock_area_m2"] > 0 for item in results)
|
||
|
||
|
||
def test_사면_이동은_노견째_평행이동_늘어난_폭은_차도() -> None:
|
||
"""151-2 — 노견 폭 그대로 · 차도 끝이 이동량만큼 바깥 · 확폭 출력엔 이동 몫 안 섞임."""
|
||
by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True))
|
||
base = compute_cross_design(
|
||
_ground(0.35),
|
||
100.4,
|
||
standard=STANDARD_CROSS_SECTION,
|
||
ground_type="soil",
|
||
section_mode="left_cut",
|
||
ditch_side="left",
|
||
)
|
||
moved = by_name["노견 확장 좌 0.7 · 우 1.2"]
|
||
for side, sign, extend in (("left", 1, 0.7), ("right", -1, 1.2)):
|
||
road = moved["road_edges"][side]["offset_m"]
|
||
lane = moved["carriageway_edges"][side]["offset_m"]
|
||
base_shoulder = (
|
||
base["road_edges"][side]["offset_m"] - base["carriageway_edges"][side]["offset_m"]
|
||
)
|
||
assert abs((road - lane) - base_shoulder) < 1e-9 # 노견 폭 고정
|
||
assert abs(lane - (base["carriageway_edges"][side]["offset_m"] + sign * extend)) < 1e-9
|
||
assert abs(moved["carriageway_width_m"] - (base["carriageway_width_m"] + 1.9)) < 1e-9
|
||
assert moved["widening_left_m"] == 0.0 and moved["widening_right_m"] == 0.0
|
||
|
||
curve = by_name["곡선부 확폭 — 바깥쪽 좌"]
|
||
both = by_name["확폭 좌 + 사면 이동 좌 0.4"]
|
||
assert both["widening_left_m"] == curve["widening_left_m"] > 0
|
||
assert abs(both["carriageway_width_m"] - (curve["carriageway_width_m"] + 0.4)) < 1e-9
|
||
|
||
|
||
def _outer_slope(result: dict, side: str, skip: float = 0.0) -> float:
|
||
"""노면 끝에서 `skip`(측구 폭) 바깥 설계선 한 마디의 기울기(dz / 바깥 거리)."""
|
||
sign = 1.0 if side == "left" else -1.0
|
||
start_offset = result["road_edges"][side]["offset_m"] + sign * skip
|
||
line = sorted(result["design_line"], key=lambda p: sign * p["offset_m"])
|
||
index = next(i for i, p in enumerate(line) if abs(p["offset_m"] - start_offset) < 1e-6)
|
||
start, nxt = line[index], line[index + 1]
|
||
return (nxt["elevation_m"] - start["elevation_m"]) / abs(nxt["offset_m"] - start["offset_m"])
|
||
|
||
|
||
def test_절성_넘어가면_그_쪽_사면_기울기로_원지반까지() -> None:
|
||
"""370-1 — 옮긴 노견에서 절/성 판정 · 마루 너머 = 성토 1:n · 성 → 절 = 절토 1:n + 측구 자동."""
|
||
by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True))
|
||
soil = STANDARD_CROSS_SECTION["soil"]
|
||
ridge = by_name["마루 너머 = 성토 1:n"]
|
||
assert ridge["section_mode"] == "left_cut" # 우 = 성토
|
||
assert ridge["ditch_enabled"] is False # 우 측구 자동 판정 따라 빠짐
|
||
assert ridge["fill_area_m2"] > 0
|
||
assert abs(_outer_slope(ridge, "right") + 1.0 / soil["fill_slope_ratio"]) < 1e-3
|
||
rise = by_name["성 → 절 넘어감"]
|
||
assert rise["section_mode"] == "left_cut" # 좌 = 절토
|
||
assert rise["ditch_enabled"] is True and rise["ditch_side"] == "left"
|
||
assert (
|
||
abs(
|
||
_outer_slope(rise, "left", soil["ditch"]["top_width_m"]) - 1.0 / soil["cut_slope_ratio"]
|
||
)
|
||
< 1e-3
|
||
)
|