Files
Aislo/resources/tester/helper_b06_fill_slope_length.py
T
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt`
가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는
시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음.

- `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침**
- `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시)
- 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q`
  옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0

⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:12:30 +09:00

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 tmp/tests/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)