122 lines
5.3 KiB
Python
122 lines
5.3 KiB
Python
"""지형 DXF + 교각 + (시추) → 교각별 지반 결과."""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
import math
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from .boreholes import Borehole, read_boreholes
|
||
from .checks import Alert, check_pier
|
||
from .config import GroundConfig
|
||
from .dtm import DTM
|
||
from .dxf_terrain import TerrainData, read_terrain_dxf
|
||
from .piers import Pier, read_piers
|
||
from .strata import SoilColumn, pier_soil_column
|
||
|
||
|
||
@dataclass
|
||
class PierResult:
|
||
pier: Pier
|
||
ground_center: float | None # DTM 중심 지반고
|
||
corner_els: list[float | None] # DTM 네 모서리
|
||
ground_used: float | None # 쓰는 지반고 (DTM 우선, 없으면 엑셀) — G-5
|
||
ground_source: str # 계산(DTM) / 입력(엑셀) / 없음
|
||
cover: float | None # 흙덮개 = 지반 최저 − 확대기초 상면
|
||
column: SoilColumn | None
|
||
alerts: list[Alert] = field(default_factory=list)
|
||
dtm_spacing: float | None = None # 중심 지반고를 만든 지형 점 중 가장 먼 거리 (m)
|
||
|
||
@property
|
||
def ground_min(self) -> float | None:
|
||
v = [z for z in self.corner_els + [self.ground_center] if z is not None]
|
||
return min(v) if v else None
|
||
|
||
@property
|
||
def ground_max(self) -> float | None:
|
||
v = [z for z in self.corner_els + [self.ground_center] if z is not None]
|
||
return max(v) if v else None
|
||
|
||
|
||
@dataclass
|
||
class GroundModel:
|
||
terrain: TerrainData
|
||
dtm: DTM
|
||
boreholes: list[Borehole]
|
||
bore_issues: list[str]
|
||
results: list[PierResult]
|
||
cfg: GroundConfig
|
||
|
||
@property
|
||
def alerts(self) -> list[Alert]:
|
||
return [a for r in self.results for a in r.alerts]
|
||
|
||
|
||
def evaluate_pier(p: Pier, dtm: DTM, bores: list[Borehole], cfg: GroundConfig) -> PierResult:
|
||
zc, spacing = dtm.z_info(p.X, p.Y)
|
||
corners = [dtm.z_at(x, y) for x, y in p.corners()]
|
||
if zc is not None:
|
||
used, src = zc, "계산"
|
||
elif p.excel_ground_el is not None:
|
||
used, src = p.excel_ground_el, "입력"
|
||
else:
|
||
used, src = None, "없음"
|
||
known = [z for z in corners if z is not None] or ([used] if used is not None else [])
|
||
cover = (min(known) - p.footing_top_el) if (known and p.footing_top_el is not None) else None
|
||
column = pier_soil_column(p.X, p.Y, used, bores, cfg) if bores else None
|
||
alerts = check_pier(p, zc, corners, column, cfg, has_bore_data=bool(bores), dtm_spacing=spacing)
|
||
return PierResult(p, zc, corners, used, src, cover, column, alerts, spacing)
|
||
|
||
|
||
def infer_piers_from_plan(terrain: TerrainData) -> list[Pier]:
|
||
"""종평면도에서 교각 번호와 위치를 찾아 치수 없는 교각 목록을 만든다."""
|
||
labels = sorted(terrain.pier_labels, key=lambda item: int(item["id"][1:]) if item["id"][1:].isdigit() else 10**9)
|
||
labels = list({item["id"]: item for item in labels}.values())
|
||
if not labels:
|
||
raise ValueError("종평면도에서 교각 번호(P1, P2...)를 찾지 못했습니다. 교각 입력 JSON을 지정하거나 교각 번호 레이어를 확인하세요.")
|
||
|
||
inferred: list[Pier] = []
|
||
for index, label in enumerate(labels):
|
||
prev_label = labels[index - 1] if index > 0 else None
|
||
next_label = labels[index + 1] if index + 1 < len(labels) else None
|
||
dx = dy = 0.0
|
||
if prev_label and next_label:
|
||
dx = next_label["X"] - prev_label["X"]
|
||
dy = next_label["Y"] - prev_label["Y"]
|
||
elif next_label:
|
||
dx = next_label["X"] - label["X"]
|
||
dy = next_label["Y"] - label["Y"]
|
||
elif prev_label:
|
||
dx = label["X"] - prev_label["X"]
|
||
dy = label["Y"] - prev_label["Y"]
|
||
length = math.hypot(dx, dy)
|
||
axis_dir = (dx / length, dy / length) if length > 1e-9 else (0.0, 1.0)
|
||
inferred.append(Pier(label["id"], float(label["X"]), float(label["Y"]), axis_dir=axis_dir))
|
||
return inferred
|
||
|
||
|
||
def run_ground_model(terrain_path: str | Path, piers: Optional[str | Path | list[Pier]],
|
||
boreholes_xlsx: str | Path | None = None,
|
||
cfg: GroundConfig | None = None, work_dir: str | Path | None = None) -> GroundModel:
|
||
cfg = cfg or GroundConfig()
|
||
terrain = read_terrain_dxf(terrain_path, cfg, work_dir)
|
||
if len(terrain.points) < 3:
|
||
layer_names = ", ".join(list((terrain.stats.get("layers") or {}).keys())[:12]) or "없음"
|
||
raise ValueError(
|
||
"종평면도에서 지형점 3개 이상을 읽지 못했습니다. "
|
||
f"읽은 점 {len(terrain.points)}개입니다. 표고점/등고선/3D 지형 객체와 EL 범위({cfg.z_min}–{cfg.z_max})를 확인하세요. "
|
||
f"발견된 주요 레이어: {layer_names}"
|
||
)
|
||
dtm = DTM(terrain.points, cfg.tin_max_edge)
|
||
if piers is None:
|
||
pier_list = infer_piers_from_plan(terrain)
|
||
else:
|
||
pier_list = read_piers(piers) if isinstance(piers, (str, Path)) else list(piers)
|
||
bores: list[Borehole] = []
|
||
issues: list[str] = []
|
||
if boreholes_xlsx:
|
||
pos = {b["id"]: (b["X"], b["Y"]) for b in terrain.boreholes}
|
||
bores, issues = read_boreholes(boreholes_xlsx, cfg, dxf_positions=pos)
|
||
results = [evaluate_pier(p, dtm, bores, cfg) for p in pier_list]
|
||
return GroundModel(terrain, dtm, bores, issues, results, cfg)
|