- 서버 재계산([저장]·[확정]·자동설계 체인) 맨 앞에서 미교차 측점만 열린 쪽을 +5m 씩 확정 지표면에서 다시 떠 계산 · 닫히면 멈춤 · 지표면 밖 샘플이 섞이면 그 걸음은 안 붙이고 미교차로 남김 - 넓힌 샘플은 횡단 파일에 남겨 화면·Node·B08 이 같은 지반을 봄 · 계산식은 안 바뀜(입력만 넓어짐) · 뒤 보정(포장·세월교·사토장)은 넓힌 지반 위에서 - 종전엔 반폭 20m 끝에서 잘려 성토가 작게 섰음 - 936be972 서버 재계산 전→후: 미교차 12 → 1(840 지표면 끝) · 성토 단면 460 19.87→35.11 · 480 48.04→84.35 · 500 37.15→52.47 · 700 24.74→54.84 · 1000 56.30→80.40 · 840 41.60→110.35 · 258.12 25.11→31.44 · 유토곡선 성토 13,350.64 → 17,248.52㎥ · 토취 7,259.84 → 11,100.86㎥ · 내역 본체 129,498,518 → 154,857,626원 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
191 lines
8.1 KiB
Python
191 lines
8.1 KiB
Python
"""미교차 측점의 지반 샘플을 **지표면 끝까지** 넓힘 — ㉳ (가) (2026-09-14 브레인 판정).
|
|
|
|
반폭(기본 20m) 샘플 끝에서 사면이 원지반과 안 만나면 면적이 거기서 잘려 성토가 **작게** 섰다
|
|
(936be972 미교차 12곳 중 11곳이 넓히면 닫힘 · 성토 +2,559.9㎥). ⇒ **열린 쪽만** +5m 씩 확정 지표면에서
|
|
다시 떠 계산하고, 닫히면 멈춘다. 새 걸음에 지표면 밖(무효) 샘플이 섞이면 그 걸음은 안 붙이고 멈춘다 —
|
|
그 측점은 「지표면 끝까지 넓혀도 안 만남」으로 미교차가 남는다.
|
|
⚠ 상한 숫자를 두지 않는다 — 지표면이 실제로 있는 끝이라 근거가 필요 없음(인위 기본값 금지).
|
|
⚠ 716 판정 「미교차 측점만 반폭 +5m 한 번」을 갈음(그것으론 4곳만 닫혔음).
|
|
⚠ 계산식은 안 바뀐다 — 샘플(입력)만 넓어진다. 넓힌 샘플은 횡단 파일에 남겨 화면(TS)·Node·B08 이
|
|
같은 지반을 본다(한 벌).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
import numpy as np
|
|
|
|
from B05_Profile.B05_Profile_Engine_Sections import cross_filename
|
|
from B06_Section.B06_Section_Engine_Design import (
|
|
_SLOPE_CLOSE_TOLERANCE_M,
|
|
compute_cross_design,
|
|
curve_widening_args,
|
|
)
|
|
from common_util.common_util_json import atomic_write_json
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#: 한 걸음 — 브레인 판정 「+5m 씩」.
|
|
EXTEND_STEP_M = 5.0
|
|
#: 지표면 끝까지 넓혀도 안 닫힌 측점의 사유(경고가 그대로 씀).
|
|
SURFACE_END_REASON = "지표면 끝까지 넓혀도 성토 비탈이 원지반과 안 만남"
|
|
|
|
|
|
def _valid_sorted(samples: list[dict[str, Any]]) -> list[tuple[float, float]]:
|
|
return sorted(
|
|
(float(s["offset_m"]), float(s["elevation_m"]))
|
|
for s in samples
|
|
if s.get("valid") is not False and s.get("elevation_m") is not None
|
|
)
|
|
|
|
|
|
def _open_ends(samples: list[dict[str, Any]], design: dict[str, Any]) -> tuple[bool, bool]:
|
|
"""(우 끝 열림, 좌 끝 열림) — 샘플 끝의 지반과 설계선 높이차가 허용오차를 넘나."""
|
|
ground = _valid_sorted(samples)
|
|
line = sorted(
|
|
(float(p["offset_m"]), float(p["elevation_m"])) for p in design.get("design_line") or []
|
|
)
|
|
if not ground or not line:
|
|
return False, False
|
|
right = abs(ground[0][1] - line[0][1]) > _SLOPE_CLOSE_TOLERANCE_M
|
|
left = abs(ground[-1][1] - line[-1][1]) > _SLOPE_CLOSE_TOLERANCE_M
|
|
return right, left
|
|
|
|
|
|
def extend_unclosed(
|
|
samples: list[dict[str, Any]],
|
|
frame: dict[str, Any],
|
|
compute: Callable[[list[dict[str, Any]]], dict[str, Any]],
|
|
sampler: Any,
|
|
step_m: float = EXTEND_STEP_M,
|
|
) -> tuple[list[dict[str, Any]], dict[str, Any]] | None:
|
|
"""넓힌 샘플과 `{left_m, right_m, surface_end}` — 이미 닫혔으면 `None`."""
|
|
design = compute(samples)
|
|
if not design.get("slope_unclosed"):
|
|
return None
|
|
offsets = [offset for offset, _z in _valid_sorted(samples)]
|
|
spacing = min(b - a for a, b in zip(offsets, offsets[1:]) if b - a > 1e-9)
|
|
count = max(int(round(step_m / spacing)), 1)
|
|
origin = np.array([float(frame["origin"]["x"]), float(frame["origin"]["y"])])
|
|
left_axis = np.array([float(v) for v in frame["left_xy"]])
|
|
info: dict[str, Any] = {"left_m": 0.0, "right_m": 0.0, "surface_end": False}
|
|
samples = list(samples)
|
|
while design.get("slope_unclosed"):
|
|
right_open, left_open = _open_ends(samples, design)
|
|
added: list[dict[str, Any]] = []
|
|
for side, is_open, sign in (("right", right_open, -1.0), ("left", left_open, 1.0)):
|
|
if not is_open:
|
|
continue
|
|
edge = min(offsets) if sign < 0 else max(offsets)
|
|
new_offsets = np.array([edge + sign * spacing * k for k in range(1, count + 1)])
|
|
xy = origin[None, :] + left_axis[None, :] * new_offsets[:, None]
|
|
z, valid = sampler.sample_xy(xy)
|
|
if not np.all(valid):
|
|
info["surface_end"] = True
|
|
continue
|
|
added.extend(
|
|
{
|
|
"offset_m": round(float(o), 6),
|
|
"x": round(float(p[0]), 6),
|
|
"y": round(float(p[1]), 6),
|
|
"z": round(float(e), 6),
|
|
"elevation_m": round(float(e), 6),
|
|
"valid": True,
|
|
}
|
|
for o, p, e in zip(new_offsets, xy, z)
|
|
)
|
|
info[f"{side}_m"] += step_m
|
|
if not added:
|
|
break
|
|
samples = sorted(samples + added, key=lambda s: float(s["offset_m"]))
|
|
offsets = [offset for offset, _z in _valid_sorted(samples)]
|
|
design = compute(samples)
|
|
return samples, info
|
|
|
|
|
|
def extend_unclosed_sections(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
project_root: Path,
|
|
standard: dict[str, Any] | None,
|
|
sampler: Any,
|
|
) -> int:
|
|
"""저장 설계가 미교차인 측점을 넓혀 샘플·설계를 자리에서 갈고 횡단 파일에 남긴다. 넓힌 수."""
|
|
from B06_Section.B06_Section_Router_Design import (
|
|
USER_TOUCHED_KEYS,
|
|
ford_drop_at,
|
|
ford_surface_drops,
|
|
stored_berm,
|
|
stored_cut_slope,
|
|
)
|
|
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
|
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
|
|
if sampler is None:
|
|
return 0
|
|
drops = ford_surface_drops(project_root)
|
|
cross_dir = project_root / "B06_Section" / "cross_sections"
|
|
changed = 0
|
|
for section in cross_sections:
|
|
design = section.get("design")
|
|
if not isinstance(design, dict) or not design.get("slope_unclosed"):
|
|
continue
|
|
chainage = float(section.get("chainage_m", 0.0))
|
|
|
|
def compute(samples: list[dict[str, Any]], design=design, chainage=chainage) -> dict:
|
|
return compute_cross_design(
|
|
samples,
|
|
design_elevation_from_longitudinal(longitudinal, chainage),
|
|
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
|
section_mode=str(design.get("section_mode") or "left_cut"),
|
|
ditch_side=design.get("ditch_side"),
|
|
ditch_type=str(design.get("ditch_type") or "standard"),
|
|
paved=bool(design.get("paved", False)),
|
|
standard=standard,
|
|
rock_boundary_offset_m=design.get(
|
|
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
),
|
|
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
|
cut_slope_ratio=stored_cut_slope(design),
|
|
ditch_enabled=design.get("ditch_enabled"),
|
|
ditch_choice=design.get("ditch_choice"),
|
|
surface_drop_m=ford_drop_at(chainage, drops),
|
|
berm=stored_berm(design),
|
|
**curve_widening_args(section),
|
|
)
|
|
|
|
try:
|
|
result = extend_unclosed(
|
|
section.get("samples") or [], section["frame"], compute, sampler
|
|
)
|
|
except (ValueError, KeyError):
|
|
continue
|
|
if result is None:
|
|
continue
|
|
samples, info = result
|
|
if not info["left_m"] and not info["right_m"]:
|
|
continue # 첫 걸음부터 지표면 밖 — 샘플이 그대로라 갈 것이 없음
|
|
recomputed = compute(samples)
|
|
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
|
if design.get(key) is not None:
|
|
recomputed[key] = design[key]
|
|
section["samples"] = samples
|
|
section["design"] = recomputed
|
|
cross_path = cross_dir / cross_filename(chainage)
|
|
if cross_path.is_file():
|
|
stored = json.loads(cross_path.read_text(encoding="utf-8"))
|
|
atomic_write_json(cross_path, {**stored, "samples": samples})
|
|
logger.info(
|
|
"미교차 샘플 넓힘: 측점 %.3f 좌 +%sm 우 +%sm 지표면 끝 %s",
|
|
chainage,
|
|
info["left_m"],
|
|
info["right_m"],
|
|
info["surface_end"],
|
|
)
|
|
changed += 1
|
|
return changed
|