⚠ **뿌리** — `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>
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 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)
|