시험을 `resources/tester` 로 옮긴 뒤 **한 건이 실패**하고 있었음 — `test_preview_cost` 가 짝 파일을 `tmp.tests.…` 로 불러 못 찾았음(옮긴 폴더에 없음). - `from resources.tester.test_preview_assets import …` 로 고침 - 실행 안내 문구 셋도 새 경로로(`diag_structures` · `helper_b06_fill_slope_length` · `test_common_util_crs`) - 정책 시험 설명은 **사실이 바뀌었으므로** 고쳐 적음 — 「git 밖이라」가 아니라 「옮겨서 함께 건너가되, 까닭은 여전히 값 옆(정본)에 두는 것이 옳다」 `resources/tester/` 1183 통과 / 22 건너뜀 / **실패 0**. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
"""B06 성토사면 경사길이 교차검증 헬퍼 (2026-09-03).
|
|
|
|
화면(`B06_Section_UI_Cross_Fit.fillSlopeLengths`)이 낸 값을 **API 원본으로 다시 계산해**
|
|
대조한다. 프론트 구현과 독립적으로 설계선·지반선만 보고 재는 것이 목적이라 pytest 가
|
|
아니라 실행 스크립트다.
|
|
|
|
1) 공용 브라우저에서 `/api/projects/{pid}/sections/{route}/detail` 응답을 파일로 저장
|
|
2) ./venv/Scripts/python.exe resources/tester/helper_b06_fill_slope_length.py <그 파일> [측점...]
|
|
|
|
「≥」는 사면이 설계선 끝(계산 반폭)까지 원지반을 못 만난 측점 — 거기까지의 하한값이다.
|
|
허용 오차 — 화면은 교차 판정에 2cm 허용오차(`MEET_TOLERANCE_M`)를 쓰고 여기서는 부호
|
|
전환만 보므로, 사면이 지반과 나란히 붙는 자리에서 최대 0.15m 차이가 난다(2026-09-03 실측).
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import pathlib
|
|
import sys
|
|
|
|
|
|
def interp(points):
|
|
"""(offset, elevation) 목록의 선형보간 함수와 정의역 양 끝을 준다."""
|
|
pts = sorted(points, key=lambda t: t[0])
|
|
|
|
def at(x):
|
|
if x <= pts[0][0]:
|
|
return pts[0][1]
|
|
if x >= pts[-1][0]:
|
|
return pts[-1][1]
|
|
lo, hi = 0, len(pts) - 1
|
|
while hi - lo > 1:
|
|
mid = (lo + hi) // 2
|
|
if pts[mid][0] <= x:
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
(x0, z0), (x1, z1) = pts[lo], pts[hi]
|
|
return z0 if x1 == x0 else z0 + (z1 - z0) * (x - x0) / (x1 - x0)
|
|
|
|
return at, pts[0][0], pts[-1][0]
|
|
|
|
|
|
def slope_starts(design):
|
|
"""좌·우 사면 시작 오프셋 — 노견 끝(그 측에 측구가 있으면 측구 바깥)."""
|
|
edges = design["road_edges"]
|
|
left = max(edges["left"]["offset_m"], edges["right"]["offset_m"])
|
|
right = min(edges["left"]["offset_m"], edges["right"]["offset_m"])
|
|
spec = design.get("ditch") or {"type": "none"}
|
|
width = 0.0
|
|
if design.get("ditch_enabled") is not False and spec.get("type") != "none":
|
|
width = spec["top_width_m"] if spec["type"] == "standard" else spec["width_m"]
|
|
return {
|
|
"left": left + (width if design.get("ditch_side") == "left" else 0.0),
|
|
"right": right - (width if design.get("ditch_side") == "right" else 0.0),
|
|
}
|
|
|
|
|
|
def fill_slope_lengths(section, step=0.005):
|
|
"""성토측 사면 경사길이 — {side: (길이 m, 미교차 여부)}."""
|
|
design = section["design"]
|
|
ground_at, _, _ = interp(
|
|
[
|
|
(p["offset_m"], p["elevation_m"])
|
|
for p in section["samples"]
|
|
if p.get("valid") is not False and p.get("elevation_m") is not None
|
|
]
|
|
)
|
|
design_at, line_min, line_max = interp(
|
|
[(p["offset_m"], p["elevation_m"]) for p in design["design_line"]]
|
|
)
|
|
starts = slope_starts(design)
|
|
slant = math.hypot(1, 1 / design["fill_slope_ratio"])
|
|
result = {}
|
|
for side in ("left", "right"):
|
|
if design["section_mode"] in ("both_cut", f"{side}_cut"):
|
|
continue
|
|
outward = 1 if side == "left" else -1
|
|
start = starts[side]
|
|
limit = line_max if side == "left" else line_min
|
|
previous = design_at(start) - ground_at(start)
|
|
offset, meet = start, None
|
|
while (limit - offset) * outward > 0:
|
|
offset = start + outward * min(abs(offset - start) + step, abs(limit - start))
|
|
diff = design_at(offset) - ground_at(offset)
|
|
if previous != 0 and (diff > 0) != (previous > 0):
|
|
# 부호가 뒤집힌 두 걸음 사이를 선형보간한 자리가 사면 끝이다.
|
|
meet = offset - outward * step + outward * step * (previous / (previous - diff))
|
|
break
|
|
previous = diff
|
|
end = limit if meet is None else meet
|
|
result[side] = (abs(end - start) * slant, meet is None)
|
|
return result
|
|
|
|
|
|
def main(argv):
|
|
detail = json.loads(pathlib.Path(argv[1]).read_text(encoding="utf-8"))
|
|
want = {round(float(value), 1) for value in argv[2:]}
|
|
for section in detail["cross_sections"]:
|
|
if not section.get("design"):
|
|
continue
|
|
chainage = round(section["chainage_m"], 1)
|
|
if want and chainage not in want:
|
|
continue
|
|
lengths = fill_slope_lengths(section)
|
|
if not lengths:
|
|
continue
|
|
text = " ".join(
|
|
f"{side}={'≥' if open_ else ''}{length:.2f}m" for side, (length, open_) in lengths.items()
|
|
)
|
|
print(f"ch={chainage:8.1f} {section['design']['section_mode']:10s} {text}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv)
|