- 모델: B06 횡단(지반선 · 설계선 · 암 경계 · 성토체 · 벽) → 지층 폴리곤 · 지하수 · 노면 하중 · 비탈별 방향 · 탐색 한계 - 솔버: 한 벌 절편 식(λ = 0 Bishop · Janbu / Spencer 모멘트 · 힘 동시) · 간극수압 none · r_u · 지하수위선 · kh · 상재하중 · 인장균열 - 탐색: Entry & Exit · Grid & Radius · 차분진화 다듬기 · 블록 씨앗 + 꼭짓점 국부 최적화 + Nelder-Mead - 출력: 방법별 최소 안전율 · 임계면 좌표 · 절편표 · 등안전율 격자 · 수렴 정보 - 시험: Slide2 검증 예제 12 문제(ACADS 1a~4 · F&K · Loukidis · Arai & Tagyo · Yamagami & Ueta) ±3 % · 면적 = B06 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
294 lines
12 KiB
Python
294 lines
12 KiB
Python
"""E03 2D 지층 모델 빌더 — 측점 횡단(지반선 · 설계선 · 소단 · 측구 · 암 경계 · 성토체 · 벽) → 지층 폴리곤.
|
|
|
|
`build_section_model` = storage 를 모르는 순수 함수(좌표 · 값만) · `section_model_from_b06` = B06 횡단 자료 꼴
|
|
(분석 345-D ②)을 읽는 얇은 어댑터. 좌표 x = B06 offset(좌 +) · z = 표고.
|
|
설계선(design_line)이 소단 · 측구 · 노면을 이미 담음 — 여기서 다시 그리지 않음. 설계선 밖은 원지반.
|
|
암 경계 = 원지반을 연직으로 |offset| 내린 선(B06 면적 가름과 같은 정의 · `Engine_Design.py` 363-377).
|
|
"""
|
|
|
|
import numpy as np
|
|
from shapely.geometry import LineString, MultiPolygon, Polygon, box
|
|
from shapely.ops import unary_union
|
|
from shapely.validation import make_valid
|
|
|
|
from E03_StabSection.E03_StabSection_Types import (
|
|
DEFAULT_MATERIALS,
|
|
DEFAULT_ROAD_SURCHARGE_KPA,
|
|
)
|
|
from E03_StabSection.E03_StabSection_Types import (
|
|
groundwater as norm_groundwater,
|
|
)
|
|
from E03_StabSection.E03_StabSection_Types import (
|
|
surcharges as norm_surcharges,
|
|
)
|
|
|
|
#: B06 지반 라벨 → 암 경계 아래 지층(요구서 345 6장 · 분석 D 5-2). 경계 위는 풍화토.
|
|
ROCK_LAYER_OF = {"ripping_rock": "weathered_rock", "blasting_rock": "soft_rock"}
|
|
SOIL_ID = "weathered_soil"
|
|
FILL_ID = "fill"
|
|
#: 벽(C군) — 활동면이 못 지나게 강한 물체로(콘크리트 · 석축 대표값). 물성 표에 없으면 이 값을 덧붙임.
|
|
WALL_MATERIAL = {
|
|
"id": "wall",
|
|
"name": "벽체",
|
|
"gamma_t": 23.0,
|
|
"gamma_sat": 23.0,
|
|
"c": 1000.0,
|
|
"phi": 45.0,
|
|
}
|
|
DEFAULT_HALF_WIDTH_M = 40.0
|
|
|
|
|
|
def _polys(geom) -> list[Polygon]:
|
|
geom = make_valid(geom)
|
|
if isinstance(geom, Polygon):
|
|
parts = [geom]
|
|
elif isinstance(geom, MultiPolygon):
|
|
parts = list(geom.geoms)
|
|
else:
|
|
parts = [g for g in getattr(geom, "geoms", []) if isinstance(g, Polygon)]
|
|
return [p for p in parts if p.area > 1e-6]
|
|
|
|
|
|
def _region(material_id: str, poly: Polygon) -> dict:
|
|
out = {"material_id": material_id, "polygon": [list(c) for c in poly.exterior.coords]}
|
|
if poly.interiors:
|
|
out["holes"] = [[list(c) for c in ring.coords] for ring in poly.interiors]
|
|
return out
|
|
|
|
|
|
def final_surface(ground: list, design_line: list) -> list[list[float]]:
|
|
"""공사 뒤 지표 = 설계선 범위는 설계선 · 밖은 원지반."""
|
|
line = sorted((float(x), float(z)) for x, z in design_line)
|
|
if not line:
|
|
return [list(p) for p in ground]
|
|
x0, x1 = line[0][0], line[-1][0]
|
|
left = [p for p in ground if p[0] < x0 - 1e-9]
|
|
right = [p for p in ground if p[0] > x1 + 1e-9]
|
|
return [list(map(float, p)) for p in left + line + right]
|
|
|
|
|
|
def build_section_model(
|
|
ground: list,
|
|
design_line: list,
|
|
*,
|
|
layers: list,
|
|
materials: list[dict] | None = None,
|
|
fill_material: str = FILL_ID,
|
|
walls: list[dict] | None = None,
|
|
groundwater: dict | None = None,
|
|
seismic_kh: float = 0.0,
|
|
surcharge: list[dict] | None = None,
|
|
bottom_z: float | None = None,
|
|
area_range: tuple[float, float] | None = None,
|
|
) -> dict:
|
|
"""원지반 · 설계선 · 지층(위부터 [(material_id, 원지반 아래 바닥 깊이 m | None=모형 바닥)]) → 해석 모형.
|
|
|
|
돌려주는 값 = LEM 모형(materials · regions · surface · groundwater · seismic_kh · surcharge)
|
|
+ `areas`(area_range 안 절토 · 성토 · 절토 토사 · 절토 암 ㎡ — B06 면적 대조용).
|
|
"""
|
|
ground = sorted((float(x), float(z)) for x, z in ground)
|
|
surface = final_surface(ground, design_line)
|
|
xs = [p[0] for p in ground]
|
|
zs = [p[1] for p in ground] + [p[1] for p in surface]
|
|
relief = max(zs) - min(zs)
|
|
zb = bottom_z if bottom_z is not None else min(zs) - max(5.0, 0.5 * relief)
|
|
x0, x1 = xs[0], xs[-1]
|
|
bottom = [(x1, zb), (x0, zb)]
|
|
below_final = make_valid(Polygon(surface + bottom))
|
|
ground_body = make_valid(Polygon(ground + bottom))
|
|
natural = below_final.intersection(ground_body)
|
|
fill = below_final.difference(ground_body)
|
|
cut = ground_body.difference(below_final)
|
|
|
|
def band(d_top: float, d_bot: float | None) -> Polygon:
|
|
top = [(x, z - d_top) for x, z in ground]
|
|
if d_bot is None:
|
|
low = [(x1, zb - 1.0), (x0, zb - 1.0)]
|
|
else:
|
|
low = [(x, z - d_bot) for x, z in reversed(ground)]
|
|
return make_valid(Polygon(top + low))
|
|
|
|
wall_geom = unary_union([Polygon(w["polygon"]) for w in walls or []]) if walls else None
|
|
regions: list[dict] = []
|
|
bands: list = []
|
|
depth = 0.0
|
|
for material_id, d_bot in layers:
|
|
b = band(depth, None if d_bot is None else float(d_bot))
|
|
bands.append(b)
|
|
part = natural.intersection(b)
|
|
if wall_geom is not None:
|
|
part = part.difference(wall_geom)
|
|
regions += [_region(material_id, p) for p in _polys(part)]
|
|
if d_bot is None:
|
|
break
|
|
depth = float(d_bot)
|
|
fill_part = fill.difference(wall_geom) if wall_geom is not None else fill
|
|
regions += [_region(fill_material, p) for p in _polys(fill_part)]
|
|
mats = [dict(m) for m in (materials or DEFAULT_MATERIALS)]
|
|
if wall_geom is not None:
|
|
for w in walls:
|
|
regions += [
|
|
_region(w.get("material_id", "wall"), p) for p in _polys(Polygon(w["polygon"]))
|
|
]
|
|
if not any(m["id"] == "wall" for m in mats):
|
|
mats.append(dict(WALL_MATERIAL))
|
|
a0, a1 = area_range or (x0, x1)
|
|
clip = box(a0, zb - 1.0, a1, max(zs) + 100.0)
|
|
cut_in = cut.intersection(clip)
|
|
cut_soil = cut_in.intersection(bands[0]).area if len(bands) > 1 else cut_in.area
|
|
return {
|
|
"materials": mats,
|
|
"regions": regions,
|
|
"surface": surface,
|
|
"groundwater": norm_groundwater(groundwater),
|
|
"seismic_kh": float(seismic_kh or 0.0),
|
|
"surcharge": norm_surcharges(surcharge),
|
|
"bottom_z": zb,
|
|
"areas": {
|
|
"cut_m2": cut_in.area,
|
|
"fill_m2": fill.intersection(clip).area,
|
|
"cut_soil_m2": cut_soil,
|
|
"cut_rock_m2": cut_in.area - cut_soil,
|
|
},
|
|
}
|
|
|
|
|
|
# ---- B06 어댑터 -----------------------------------------------------------------
|
|
def ground_from_cross(cross: dict) -> list[tuple[float, float]]:
|
|
return sorted(
|
|
(float(s["offset_m"]), float(s["elevation_m"]))
|
|
for s in cross.get("samples") or []
|
|
if s.get("valid") is not False and s.get("elevation_m") is not None
|
|
)
|
|
|
|
|
|
def widen_ground(cross: dict, sampler, half_width: float = DEFAULT_HALF_WIDTH_M, step: float = 0.5):
|
|
"""해석 폭 — 횡단 표본 바깥을 지형에서 다시 떠 ±half_width 까지 넓힘(지형 밖이 나오면 거기서 멈춤)."""
|
|
ground = ground_from_cross(cross)
|
|
frame = cross["frame"]
|
|
origin = np.array([float(frame["origin"]["x"]), float(frame["origin"]["y"])])
|
|
axis = np.array([float(v) for v in frame["left_xy"]])
|
|
added = []
|
|
for sign, edge in ((-1.0, ground[0][0]), (1.0, ground[-1][0])):
|
|
n = int(max(0.0, half_width - abs(edge)) / step)
|
|
if n <= 0:
|
|
continue
|
|
offs = edge + sign * step * np.arange(1, n + 1)
|
|
z, valid = sampler.sample_xy(origin[None, :] + axis[None, :] * offs[:, None])
|
|
stop = int(np.argmin(valid)) if not valid.all() else len(valid)
|
|
added += [(float(o), float(e)) for o, e in zip(offs[:stop], z[:stop])]
|
|
return sorted(ground + added)
|
|
|
|
|
|
def side_cases(design: dict, ground: list, line: list) -> list[dict]:
|
|
"""좌 · 우 비탈마다 역할(절토 · 성토) · 활동 방향 · 탐색 한계. 좌 = +offset.
|
|
|
|
탐색 한계 = 노면 반대쪽 끝 ~ 비탈 끝(설계선이 원지반과 만나는 점) 바깥 max(5 m, 2H) — 설계 비탈을
|
|
지나는 면만 봄(바깥 자연 비탈 전체 붕괴는 E 광역 · 2차 몫). H = 노면 끝 ~ 비탈 끝 높이차.
|
|
"""
|
|
mode = design.get("section_mode") or "right_cut"
|
|
roles = {
|
|
"left_cut": ("cut", "fill"),
|
|
"right_cut": ("fill", "cut"),
|
|
"both_cut": ("cut", "cut"),
|
|
"both_fill": ("fill", "fill"),
|
|
}[mode]
|
|
edges = design.get("road_edges") or {}
|
|
left_edge = float((edges.get("left") or {}).get("offset_m", 2.0))
|
|
right_edge = float((edges.get("right") or {}).get("offset_m", -2.0))
|
|
gx = np.array([p[0] for p in ground])
|
|
gz = np.array([p[1] for p in ground])
|
|
lx = np.array([p[0] for p in line])
|
|
lz = np.array([p[1] for p in line])
|
|
off = np.abs(lz - np.interp(lx, gx, gz)) > 0.005
|
|
out = []
|
|
for side, role in zip(("left", "right"), roles):
|
|
inward = role == "cut" # 절토 = 산 쪽에서 노면으로 · 성토 = 노면에서 바깥으로
|
|
sign = 1.0 if side == "left" else -1.0
|
|
edge = left_edge if side == "left" else right_edge
|
|
# 비탈 끝 = 원지반에서 떨어진 가장 바깥 꼭짓점의 바로 바깥 꼭짓점(다시 원지반에 붙는 점)
|
|
idx = [k for k, (x, o) in enumerate(zip(lx, off)) if o and sign * x >= sign * edge - 1e-9]
|
|
if idx:
|
|
k = max(idx) if sign > 0 else min(idx)
|
|
k = min(max(k + int(sign), 0), len(lx) - 1)
|
|
day = float(lx[k])
|
|
else:
|
|
day = edge
|
|
H = abs(float(np.interp(day, lx, lz)) - float(np.interp(edge, lx, lz)))
|
|
reach = day + sign * max(5.0, 2.0 * H)
|
|
reach = min(max(reach, gx[0]), gx[-1])
|
|
inner = right_edge if side == "left" else left_edge
|
|
limits = tuple(sorted((inner, reach)))
|
|
direction = int(-sign) if inward else int(sign)
|
|
out.append(
|
|
{"side": side, "role": role, "direction": direction, "limits": limits, "height_m": H}
|
|
)
|
|
return out
|
|
|
|
|
|
def section_model_from_b06(
|
|
cross: dict,
|
|
design: dict,
|
|
*,
|
|
materials: list[dict] | None = None,
|
|
layers: list | None = None,
|
|
ground: list | None = None,
|
|
wet: bool = False,
|
|
seismic_kh: float = 0.0,
|
|
road_surcharge_kpa: float = DEFAULT_ROAD_SURCHARGE_KPA,
|
|
walls: list[dict] | None = None,
|
|
) -> dict:
|
|
"""B06 횡단 파일(samples · frame) + 설계(design) → 해석 모형 + 비탈 목록(`sides`).
|
|
|
|
ground = 넓혀 다시 뜬 지반선(`widen_ground`) · 없으면 횡단 표본 그대로.
|
|
layers 기본 = 토사 측점 풍화토 한 층 · 암 측점 풍화토(암 경계까지) + 풍화암 / 연암.
|
|
wet = 우기 — 지하수위 = 공사 뒤 지표(KDS 11 70 05 표 4.3-3 · 토사 · 풍화암 지표 포화).
|
|
노면 하중 = 노면 끝 사이 등분포. 벽 도형(walls)은 B06 화면 산식(TS)이 그리므로 부르는 쪽이 넘김.
|
|
"""
|
|
ground = ground or ground_from_cross(cross)
|
|
line = [
|
|
(float(p["offset_m"]), float(p["elevation_m"])) for p in design.get("design_line") or []
|
|
]
|
|
if layers is None:
|
|
rock = ROCK_LAYER_OF.get(design.get("ground_type"))
|
|
offset = design.get("rock_boundary_offset_m")
|
|
if rock and offset is not None:
|
|
layers = [(SOIL_ID, abs(float(offset))), (rock, None)]
|
|
elif rock:
|
|
layers = [(rock, None)]
|
|
else:
|
|
layers = [(SOIL_ID, None)]
|
|
edges = design.get("road_edges") or {}
|
|
loads = []
|
|
if road_surcharge_kpa and edges.get("left") and edges.get("right"):
|
|
loads = [
|
|
{
|
|
"x0": edges["right"]["offset_m"],
|
|
"x1": edges["left"]["offset_m"],
|
|
"q": road_surcharge_kpa,
|
|
}
|
|
]
|
|
surface = final_surface(ground, line)
|
|
gw = {"mode": "line", "line": surface} if wet else {"mode": "none"}
|
|
model = build_section_model(
|
|
ground,
|
|
line,
|
|
layers=layers,
|
|
materials=materials,
|
|
walls=walls,
|
|
groundwater=gw,
|
|
seismic_kh=seismic_kh,
|
|
surcharge=loads,
|
|
area_range=(line[0][0], line[-1][0]) if line else None,
|
|
)
|
|
model["sides"] = side_cases(design, ground, line or ground)
|
|
model["station"] = {"chainage_m": cross.get("chainage_m"), "label": cross.get("label")}
|
|
return model
|
|
|
|
|
|
def polygon_closed(region: dict) -> bool:
|
|
"""폴리곤 닫힘 · 자기 교차 없음 · 넓이 > 0 (시험용)."""
|
|
ring = region["polygon"]
|
|
poly = Polygon(ring, region.get("holes"))
|
|
return bool(poly.is_valid and poly.area > 0 and LineString(ring).is_closed)
|