Files
Pier_v1/pierzainer_ground/pierzainer_ground/strata.py
T

121 lines
5.0 KiB
Python

"""G4 — 교각별 지층 기둥 (1D).
- 교각에서 가까운 두 공(bore_max 이내)의 지층 경계 EL을 거리 역수 가중으로 평균한다.
- 두 공의 지층 순서가 다르거나 한 공만 있으면 가까운 공 하나만 쓴다 (순서 불일치는 W12).
- 지층 경계는 교각 지반고보다 높을 수 없게 자른다.
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from .boreholes import Borehole
from .config import GroundConfig
@dataclass
class Layer:
name: str
top_el: float
bottom_el: float
@dataclass
class SoilColumn:
method: str # idw2 / nearest / none
layers: list[Layer] = field(default_factory=list)
used: list[dict] = field(default_factory=list) # {"id", "dist", "weight"}
nearest_dist: float | None = None
order_mismatch: bool = False
support_top_el: float | None = None # 지지층 상단 EL (없으면 None)
support_name: str | None = None
gwl_el: float | None = None
bottom_el: float | None = None # 자료가 끝나는 EL (시추 종료 깊이)
top_shift: float | None = None # 지반고 − 시추 공구EL (꼭대기를 지반고로 바꾼 양, m)
def _support_index(names: list[str], cfg: GroundConfig) -> int | None:
if cfg.support_layer not in cfg.strata_order:
return names.index(cfg.support_layer) if cfg.support_layer in names else None
rank = cfg.strata_order.index(cfg.support_layer)
for i, n in enumerate(names):
if n in cfg.strata_order and cfg.strata_order.index(n) >= rank:
return i
return None
def _order_conflict(a: list[str], b: list[str]) -> bool:
"""두 공에 공통으로 있는 지층끼리의 순서가 다른지 본다. 한쪽에만 있는 층은 불일치로 보지 않는다."""
common = set(a) & set(b)
return [n for n in a if n in common] != [n for n in b if n in common]
def _layer_bottoms(ref: Borehole, sel: list[tuple[float, Borehole]]) -> list[float]:
"""기준 공(가장 가까운 공)의 층마다 하단 EL을 가중 평균한다. 다른 공에 같은 층이 없으면 그 공은 빼고 평균한다."""
if all(b.names == ref.names for _, (_, b) in sel):
return [sum(w * b.strata[i].bottom_el for w, (_, b) in sel) for i in range(len(ref.strata))]
out = []
for name in ref.names:
vals = [(w, next(s.bottom_el for s in b.strata if s.name == name))
for w, (_, b) in sel if name in b.names]
total = sum(w for w, _ in vals)
out.append(sum(w * v for w, v in vals) / total)
return out
def pier_soil_column(X: float, Y: float, ground_el: float | None, bores: list[Borehole],
cfg: GroundConfig | None = None) -> SoilColumn:
cfg = cfg or GroundConfig()
cand = [(math.hypot(b.X - X, b.Y - Y), b) for b in bores
if b.X is not None and b.Y is not None and b.top_el is not None and b.strata]
cand.sort(key=lambda t: t[0])
nearest = cand[0][0] if cand else None
cand = [c for c in cand if c[0] <= cfg.bore_max][:2]
if not cand:
return SoilColumn(method="none", nearest_dist=nearest)
mismatch = len(cand) == 2 and _order_conflict(cand[0][1].names, cand[1][1].names)
if len(cand) == 1 or mismatch:
sel = cand[:1]
weights = [1.0]
else:
if cand[0][0] < 1e-6:
sel, weights = cand[:1], [1.0]
else:
inv = [1 / d for d, _ in cand]
sel, weights = cand, [w / sum(inv) for w in inv]
ref = sel[0][1]
top = sum(w * b.top_el for w, (_, b) in zip(weights, sel))
top_shift = None
if ground_el is not None:
top_shift = ground_el - top # 경계 EL은 시추 기준이라 차이만큼 맨 위 층 두께가 달라짐
top = ground_el # 기둥 꼭대기는 교각 위치의 지반고
bottoms = _layer_bottoms(ref, list(zip(weights, sel)))
layers: list[Layer] = []
cur = top
for name, bot in zip(ref.names, bottoms):
bot = min(bot, top)
if bot < cur - 1e-9:
layers.append(Layer(name, cur, bot))
cur = bot
si = _support_index([ly.name for ly in layers], cfg)
gw = [(w, b.gwl_el) for w, (_, b) in zip(weights, sel) if b.gwl_el is not None]
gwl = sum(w * g for w, g in gw) / sum(w for w, _ in gw) if gw else None
return SoilColumn(
method="idw2" if len(sel) == 2 else "nearest",
layers=layers,
used=[{"id": b.id, "dist": round(d, 2), "weight": round(w, 3)} for w, (d, b) in zip(weights, sel)]
+ ([{"id": cand[1][1].id, "dist": round(cand[1][0], 2), "weight": 0.0}] if mismatch else []),
nearest_dist=nearest,
order_mismatch=mismatch,
support_top_el=layers[si].top_el if si is not None else None,
support_name=layers[si].name if si is not None else None,
gwl_el=gwl,
bottom_el=layers[-1].bottom_el if layers else None,
top_shift=top_shift,
)