diff --git a/E03_StabSection/E03_StabSection_Engine_Search.py b/E03_StabSection/E03_StabSection_Engine_Search.py index d67fb9de7..a5ba221c4 100644 --- a/E03_StabSection/E03_StabSection_Engine_Search.py +++ b/E03_StabSection/E03_StabSection_Engine_Search.py @@ -13,7 +13,6 @@ import time import numpy as np from E03_StabSection import E03_StabSection_Engine_LEM as lem -from E03_StabSection import E03_StabSection_Engine_Support as support from E03_StabSection.E03_StabSection_Engine_Describe import describe, safety_map from E03_StabSection.E03_StabSection_Engine_Surface import ( # noqa: F401 — 밖에서 Search 로 부르던 이름 METHODS, @@ -46,11 +45,6 @@ FAR_TOL = 0.02 # ---- 원호 --------------------------------------------------------------------- -def _exit_lo(limits) -> float: - """진출 아래 한계 — limits 셋째 칸(보강으로 진입만 넓힌 때 원래 한계) · 없으면 limits[0].""" - return limits[2] if len(limits) > 2 else limits[0] - - def entry_exit_circles(prep, limits, n_points=25, thetas_deg=(10, 20, 35, 50, 65, 80, 100, 120)): xl, xr = limits[:2] pts = np.linspace(xl, xr, n_points) @@ -60,7 +54,6 @@ def entry_exit_circles(prep, limits, n_points=25, thetas_deg=(10, 20, 35, 50, 65 xX = np.repeat(xX, len(thetas_deg)) th = np.tile(np.radians(thetas_deg), len(i)) xc, zc, R, ok = circle_from_chord(prep, xE, xX, th) - ok &= xX >= _exit_lo(limits) - 1e-6 return xc[ok], zc[ok], R[ok], xE[ok], xX[ok] @@ -87,7 +80,7 @@ def grid_radius_circles(prep, limits, gx, gz, n_radii=10): owner = np.repeat(np.arange(len(cx)), n_radii) xc, zc = cx[owner], cz[owner] xE, xX, ok = circle_cross_surface(prep, xc, zc, R) - ok &= (xE >= limits[0] - 1e-6) & (xX <= limits[1] + 1e-6) & (xX >= _exit_lo(limits) - 1e-6) + ok &= (xE >= limits[0] - 1e-6) & (xX <= limits[1] + 1e-6) return xc[ok], zc[ok], R[ok], xE[ok], xX[ok], owner[ok] @@ -106,7 +99,7 @@ def _circle_eval(prep, xc, zc, R, xE, xX, n, min_depth, methods, chunk=4000): def refine_circle(prep, limits, method, seeds, n, min_depth, seed=0, maxiter=None): """원호 다듬기 — (진입 x, 진출 x, 중심각) 차분진화 · 첫 무리 = 거친 탐색의 좋은 면.""" fun = objective(method) - lo = np.array([limits[0], _exit_lo(limits), np.radians(3)]) + lo = np.array([limits[0], limits[0], np.radians(3)]) hi = np.array([limits[1], limits[1], np.radians(170)]) def obj(p): @@ -136,7 +129,7 @@ def _cloud(prep, limits, method, p0, n, min_depth, seed, k=CLOUD_SIZE, keep=10): """원 (진입 x, 진출 x, 중심각) 둘레 구름 k 개를 method 로 한 번에 셈 → 풀리는 좋은 것 keep 개.""" rng = np.random.default_rng(seed) span = limits[1] - limits[0] - lo = np.array([limits[0], _exit_lo(limits), np.radians(3)]) + lo = np.array([limits[0], limits[0], np.radians(3)]) hi = np.array([limits[1], limits[1], np.radians(170)]) p = np.clip(p0 + rng.normal(size=(k, 3)) * [0.03 * span, 0.03 * span, np.radians(4)], lo, hi) xc, zc, R, ok = circle_from_chord(prep, p[:, 0], p[:, 1], p[:, 2]) @@ -310,7 +303,7 @@ def optimize_polyline( fun = objective(method) xl, xr = limits[:2] depth_max = max(prep.surface(np.linspace(xl, xr, 50)).max() - prep.bottom_z, 1.0) - lo = np.array([xl, _exit_lo(limits)] + [0.02] * m + [0.0] * m) + lo = np.array([xl, xl] + [0.02] * m + [0.0] * m) hi = np.array([xr, xr] + [0.98] * m + [depth_max] * m) def obj(p): @@ -393,7 +386,6 @@ def block_seeds(prep, limits, m, n_points=12, angles=((55, 35), (65, 25))): xE = _ray_to_surface(prep, a, za, act, -1.0) xX = _ray_to_surface(prep, b, zb, pas, 1.0) good = np.isfinite(xE) & np.isfinite(xX) & (xE >= xl) & (xX <= xr) - good &= xX >= _exit_lo(limits) for e, x0, x1, xx in zip(xE[good], a[good], b[good], xX[good]): ix = np.linspace(x0, x1, m) d = layer_bottom_depth(prep, ix, mat) @@ -429,13 +421,8 @@ def analyze( lim = (float(prep.surface_x[0]), float(prep.surface_x[-1])) else: lim = tuple(sorted((s * float(limits[0]), s * float(limits[1])))) - if prep.supports is not None: - # 보강 — 임계면이 보강재 끝 뒤(더 깊이 · 더 멀리)로 옮겨 가는 것도 찾게 진입 한계만 끝 뒤까지 넓힘 - # (진출 = 원래 한계 셋째 칸 — 설계 비탈 밖 자연 비탈만 무너지는 면은 보강 전처럼 안 봄) - reach = float(np.min(support.tips(prep.supports))) - max( - 2.0, 0.25 * prep.supports["L"].max() - ) - lim = (max(min(lim[0], reach), float(prep.surface_x[0])), lim[1], lim[0]) + # 보강이 있어도 탐색 한계 그대로 — 보강 없는 해석과 같은 면 무리에서 찾아야 보강 전후가 맞섬 + # (한계를 넓히면 보강 없이 안 본 자연 비탈 면이 들어와 보강 뒤 F 가 오히려 낮아 보임) results: dict = {m: {} for m in methods} counts = {} contour = smap = None diff --git a/E04_Stab3D/E04_Stab3D_Engine_Search.py b/E04_Stab3D/E04_Stab3D_Engine_Search.py index 5a85460b6..c62c0fd7f 100644 --- a/E04_Stab3D/E04_Stab3D_Engine_Search.py +++ b/E04_Stab3D/E04_Stab3D_Engine_Search.py @@ -82,7 +82,7 @@ class SearchResult: f_min: np.ndarray # 셀별 최소 안전율(다스리는 값) · 닿지 않은 셀 = nan volume: np.ndarray # 그 임계 활동체 부피 method: np.ndarray # 0 = Bishop · 1 = Janbu 보정 - best: dict # 전체 최소 활동체(중심 · 반지름 · 비 · 방향 · 방법별 F · 부피) + best: dict # 전체 최소 활동체(중심 · 반지름 · 비 · 방향 · 방법별 F · 부피 · 셀) trials: int = 0 evaluated: int = 0 seconds: float = 0.0 @@ -476,7 +476,7 @@ def search(strata: Strata, params: SearchParams | None = None) -> SearchResult: thr, ref, ) - k = _set_best(best, res, f, meth, xyz[part][trial], r, ratio) + k = _set_best(best, res, f, meth, xyz[part][trial], r, ratio, cols, nx) if k >= 0: at_best.update( node=int(ids[part][trial[k]]), @@ -553,7 +553,9 @@ def search(strata: Strata, params: SearchParams | None = None) -> SearchResult: ) -def _set_best(best: dict, res: dict, f, meth, centers, r: float, ratio: float) -> int: +def _set_best( + best: dict, res: dict, f, meth, centers, r: float, ratio: float, cols, nx: int +) -> int: """덩이들 가운데 best 보다 낮은 것이 있으면 best 를 바꾸고 그 번호 · 없으면 −1.""" if not np.isfinite(f).any(): return -1 @@ -570,6 +572,8 @@ def _set_best(best: dict, res: dict, f, meth, centers, r: float, ratio: float) - center=np.asarray(centers[k], float).tolist(), radius=float(r), ratio=float(ratio), + # 활동체 셀(iy·nx+ix) — 한 구가 자른 여러 덩이 중 이 덩이만 + cells=np.unique(cols.iy[cols.group == k] * nx + cols.ix[cols.group == k]).tolist(), ) return k @@ -627,7 +631,17 @@ def _polish(strata, p, best, at_best, f_min, volume, method, node_map) -> None: return cols, tr, res, f, meth = trial(o.x) _update(f, meth, res, cols, tr, np.array([at_best["node"]]), f_min, volume, method, node_map) - _set_best(best, res, f, meth, np.repeat(o.x[None, :3], len(tr), 0), float(o.x[3]), ratio) + _set_best( + best, + res, + f, + meth, + np.repeat(o.x[None, :3], len(tr), 0), + float(o.x[3]), + ratio, + cols, + strata.z_top.shape[1], + ) def _update( diff --git a/E04_Stab3D/E04_Stab3D_Run.py b/E04_Stab3D/E04_Stab3D_Run.py index 9b646b528..fcd600263 100644 --- a/E04_Stab3D/E04_Stab3D_Run.py +++ b/E04_Stab3D/E04_Stab3D_Run.py @@ -9,7 +9,8 @@ z = 합성 지표(원지형 + 노선면) · f = 셀별 최소 안전율 · v = 그 임계 체적 m³ · m = 방법(0 Bishop · 1 Janbu 보정) · 닿지 않은 셀 = null best = 전체 최소 활동면 {x, y, z(중심), r, ratio, azimuth_deg(활동 방향), f, volume, method, ix, iy, - hovland · bishop · janbu_corrected(같은 면 방법별 F) · truncated_ratio · floor([셀, 못 뚫는 층 윗면])} + hovland · bishop · janbu_corrected(같은 면 방법별 F) · truncated_ratio · floor([셀, 못 뚫는 층 윗면]) + · cells(활동체 덩이 셀 번호 — 한 구가 자른 여러 덩이 중 이 덩이)} 타원체 = 회전축(활동 방향에 수직) 반축만 ratio·r — 533-2 `trial_columns` 와 같은 꼴. """ @@ -33,7 +34,7 @@ from E03_StabSection.E03_StabSection_Types import DEFAULT_ROAD_SURCHARGE_KPA from E04_Stab3D import E04_Stab3D_Engine_Grid as grid_engine from E04_Stab3D import E04_Stab3D_Engine_Search as search_engine -ENGINE_VERSION = "e04-3" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈 +ENGINE_VERSION = "e04-4" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈 STAGE_DIR = "E04_Stab3D" SECTION_LENGTH_M = 100.0 # 자동 구간 길이 MAX_SECTION_M = 500.0 # 한 번에 셀 구간 상한(시간 · 메모리) @@ -167,6 +168,8 @@ def _floor(strata: Any, best: dict[str, Any]) -> list[list[float]]: slip = np.where(rest >= 0, cz - np.sqrt(np.maximum(rest, 0)), np.nan) hard = strata.hard_top hit = np.isfinite(slip) & (slip < strata.z_top) & (hard > slip) & (hard < strata.z_top) + if "cells" in best: # 활동체 덩이 셀만 + hit &= np.isin(np.arange(hit.size), best["cells"]).reshape(hit.shape) return [[int(i), round(float(hard.ravel()[i]), 2)] for i in np.flatnonzero(hit.ravel())] @@ -258,6 +261,7 @@ def run_engine( "janbu_corrected": float(best["janbu_corrected"]), "truncated_ratio": float(best["truncated_ratio"]), "floor": _floor(strata, best), + "cells": [int(i) for i in best["cells"]], }, "trials": int(res.trials), "reference_fs": reference_fs(ground, settings["condition"]), @@ -292,6 +296,16 @@ def pack( best["ix"] = int(round((best["x"] - grid["x0"]) / grid["dx"])) best["iy"] = int(round((best["y"] - grid["y0"]) / grid["dx"])) f = np.asarray(raw["f"], dtype=float) + cells = np.asarray(best.get("cells") or [], int) + # 가까운 측점 = 활동체 평면 중심(없으면 구 중심) — 화면 최소 표시와 같은 자리 + where = ( + ( + grid["x0"] + (cells % grid["nx"]).mean() * grid["dx"], + grid["y0"] + (cells // grid["nx"]).mean() * grid["dx"], + ) + if len(cells) + else (best["x"], best["y"]) + ) return { "fingerprint": finger, "engine": ENGINE_VERSION, @@ -316,7 +330,7 @@ def pack( "by_method": { k: round(float(best[k]), 3) for k in ("hovland", "bishop", "janbu_corrected") }, - "chainage_m": float(_nearest_chainage(stations, best["x"], best["y"])), + "chainage_m": float(_nearest_chainage(stations, *where)), "cells": int(np.isfinite(f).sum()), "trials": int(raw.get("trials") or 0), }, diff --git a/E04_Stab3D/logic/E04_Stab3D_Mesh.ts b/E04_Stab3D/logic/E04_Stab3D_Mesh.ts index 66361007a..b334d60f9 100644 --- a/E04_Stab3D/logic/E04_Stab3D_Mesh.ts +++ b/E04_Stab3D/logic/E04_Stab3D_Mesh.ts @@ -70,22 +70,24 @@ export function slipHeight(best: SlipBest, x: number, y: number): number | null return rest < 0 ? null : best.z - Math.sqrt(rest); } -/** 잘린 셀 → 활동면 높이(활동면이 지표보다 낮은 셀만 · 못 뚫는 층 윗면 아래로는 안 내려감) */ +/** 잘린 셀 → 활동면 높이(활동면이 지표보다 낮은 셀만 · 못 뚫는 층 윗면 아래로는 안 내려감 · 엔진 덩이 셀만) */ export function cutCells( grid: Grid, z: readonly (number | null)[], best: SlipBest, ): Map { const floor = new Map(best.floor ?? []); + const mass = best.cells ? new Set(best.cells) : null; const out = new Map(); z.forEach((top, index) => { - if (top === null) return; + if (top === null || (mass && !mass.has(index))) return; const x = grid.x0 + (index % grid.nx) * grid.dx; const y = grid.y0 + Math.floor(index / grid.nx) * grid.dx; const slip = slipHeight(best, x, y); - if (slip === null) return; - const base = Math.max(slip, floor.get(index) ?? -Infinity); - if (base < top) out.set(index, base); + const base = Math.max(slip ?? top, floor.get(index) ?? -Infinity); + // 엔진 덩이 셀 = 그대로(셀 안 일부만 잘린 가장자리 = 두께 0) · 옛 저장본 = 셀 중심이 잘린 칸만 + if (mass) out.set(index, Math.min(base, top)); + else if (base < top) out.set(index, base); }); return out; } diff --git a/E04_Stab3D/logic/E04_Stab3D_Raster.ts b/E04_Stab3D/logic/E04_Stab3D_Raster.ts index 9a50303c5..7cbb809bf 100644 --- a/E04_Stab3D/logic/E04_Stab3D_Raster.ts +++ b/E04_Stab3D/logic/E04_Stab3D_Raster.ts @@ -28,6 +28,8 @@ export interface SlipBest { iy: number; /** 못 뚫는 층에 잘린 셀 — [셀 번호, 층 윗면] · 활동면을 그 높이에서 자름 */ floor?: [number, number][]; + /** 활동체 덩이 셀 번호(엔진 · 한 구가 자른 여러 덩이 중 이 덩이) — 없으면(옛 저장본) 잘린 셀 전부 */ + cells?: number[]; } export interface Summary3d { diff --git a/resources/tester/test_533_3_e04_engine.py b/resources/tester/test_533_3_e04_engine.py index 102294d2a..25d77abdc 100644 --- a/resources/tester/test_533_3_e04_engine.py +++ b/resources/tester/test_533_3_e04_engine.py @@ -109,6 +109,9 @@ def test_engine_raster_and_best(root): assert best["r"] > 0 and best["ratio"] in (1.0, 2.0) and 0 <= best["azimuth_deg"] < 360 assert best["f"] == best["bishop"] or best["method"] == 1 # 다스림 = Bishop(잘림 없음) assert raw["reference_fs"] == 1.5 # 마스터 기본 건기 + # 활동체 셀 = 엔진 덩이 — 모두 그 셀 최소(= 전체 최소) + cells = np.asarray(best["cells"]) + assert len(cells) and np.allclose(f.ravel()[cells], best["f"]) # 합성 지표 — 노면 위(측점 40 · o=0) = 설계 100 · 깎기 끝 밖(o=+20 · 원지형) = 지반 z = raw["z"] jy0 = round((0.0 - grid["y0"]) / grid["dx"]) diff --git a/resources/tester/test_533_3_e04_logic.py b/resources/tester/test_533_3_e04_logic.py index e9f3d6350..d323e7f1d 100644 --- a/resources/tester/test_533_3_e04_logic.py +++ b/resources/tester/test_533_3_e04_logic.py @@ -63,6 +63,8 @@ writeFileSync(process.argv[3], JSON.stringify({ sphere: mesh(M.slipMesh(c.grid, c.z, c.sphere)), ellipsoid: mesh(M.slipMesh(c.grid, c.z, c.ellipsoid)), cut: Array.from(M.cutCells(c.grid, c.z, c.sphere).keys()), + massCut: Array.from(M.cutCells(c.grid, c.z, { ...c.sphere, cells: [7 * 21 + 10, 7 * 21 + 11, 0] }).keys()), + massBase0: M.cutCells(c.grid, c.z, { ...c.sphere, cells: [0] }).get(0), center: M.footprintCenter(c.grid, M.cutCells(c.grid, c.z, c.sphere), c.sphere), centerEmpty: M.footprintCenter(c.grid, new Map(), c.sphere), floored: Object.fromEntries(M.cutCells(c.grid, c.z, @@ -207,6 +209,8 @@ def test_terrain_hole_matches_cut(ts): """활동체를 걷어낸 지표 = 지표 꼭짓점 − 잘린 셀 · 구멍 테두리는 활동면 메쉬와 같은 꼭짓점.""" cut = [i for i, _ in _py_cut(SPHERE)] assert ts["cut"] == cut + assert ts["massCut"] == [0, 7 * 21 + 10, 7 * 21 + 11] # 엔진 덩이 셀 그대로(0 = 안 잘림 · 두께 0) + assert ts["massBase0"] == Z[0] assert ts["holed"]["n"] == ts["terrain"]["n"] - len(cut) assert not set(ts["holed"]["cells"]) & set(cut) assert _py_rim(set(cut)) <= set(ts["holed"]["cells"]) diff --git a/resources/tester/test_536_3_stab_supports.py b/resources/tester/test_536_3_stab_supports.py index a234dc796..6b32ecc95 100644 --- a/resources/tester/test_536_3_stab_supports.py +++ b/resources/tester/test_536_3_stab_supports.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""536-3 뒷부분 보강재(네일 · 앵커) 2D 한계평형 — 힘 크기 · 능동/수동 · 탐색 넓힘 · 보강 없을 때 불변. +"""536-3 뒷부분 보강재(네일 · 앵커) 2D 한계평형 — 힘 크기 · 능동/수동 · 탐색 한계 그대로 · 보강이 F 를 낮추지 않음 · 보강 없을 때 불변. - 손계산: 평면 쐐기 닫힌 꼴(능동 F = (cL + (W cosα + T sin(α+δ)) tanφ) / (W sinα − T cos(α+δ)) · 수동 T → T/F) · 교점 · 인발 · 벗겨짐 · 정착장 · 프리스트레스. @@ -205,17 +205,64 @@ def test_slide2_86_five_rows(): near(a["methods"]["spencer"]["min"]["F"], 1.620, 0.03) -def test_search_moves_behind_strong_supports(): - """아주 센 보강 — 진입 쪽 한계가 보강재 끝 뒤까지 넓어지고 임계면이 보강재를 안 지나는 깊은 면으로 감.""" +def test_search_keeps_limits_with_strong_supports(): + """아주 센 보강 — 탐색 한계 그대로(보강 없는 해석과 같은 면 무리) · 임계 F 가 보강 없음 이상.""" bare = {k: v for k, v in _p86().items() if k != "supports"} lim = [0, 45] a0 = analyze(bare, direction=-1, methods=("bishop",), min_depth=3.0, limits=lim) a = analyze(_p86(1e5), direction=-1, methods=("bishop",), min_depth=3.0, limits=lim) - assert a0["limits"] == [0, 45] and a["limits"][1] > 45 # 끝 x 45 + 여유 5 - gov = a["methods"]["bishop"]["min"] - assert gov["entry"][0] > 45 and a["support_forces_method"] == "bishop" - assert all(f["mode"] == "not_crossing" for f in a["support_forces"]) - assert gov["F"] > a0["methods"]["bishop"]["min"]["F"] + assert a0["limits"] == a["limits"] == [0, 45] + assert a["support_forces_method"] == "bishop" + assert a["methods"]["bishop"]["min"]["F"] >= a0["methods"]["bishop"]["min"]["F"] + + +# ---- 보강은 F 를 낮추지 않음(요구 351 · sub2 실측 0+720 흠) --------------------------------- +ALL = ("ordinary", "bishop", "janbu", "spencer", MP) +#: 쐐기 비탈 (10, 10) → (20, 0) 위 면 — 얕은 · 깊은 평면 · 두 마디 · 원호(중심 · 반지름) +PLANES = [[[x0, 10.0], [20.0, 0.0]] for x0 in (3.0, 5.0, 7.0, 9.0)] + [ + [[2.0, 10.0], [12.0, 3.0], [20.0, 0.0]], + [[4.0, 10.0], [14.0, -1.0], [22.0, 0.0]], +] +CIRCLES = [[21.0, 19.45, 19.45], [19.0, 13.338, 13.422], [24.0, 27.0, 27.019]] + + +def _sups(kind, L): + heads = [(12.0, 8.0), (15.0, 5.0), (18.0, 2.0)] + if kind == "nail": + return [nail(x, z, 15, L, 1.5, 120, 40) for x, z in heads] + return [anchor(x, z, 15, L, 1.5, 300, 60, min(4.0, L), 80) for x, z in heads] + + +@pytest.mark.parametrize("kind", ["nail", "anchor"]) +@pytest.mark.parametrize("L", [3.0, 8.0, 25.0]) +def test_supports_never_lower_same_surface(kind, L): + """같은 면 — 네일(수동) · 앵커(능동) · 길이 셋 · 다섯 방법 · 면 깊이 여럿: 보강 F ≥ 보강 없음 F.""" + with_s = {**WEDGE, "supports": _sups(kind, L)} + checked = 0 + for spec in [{"polyline": p} for p in PLANES] + [{"circle": c} for c in CIRCLES]: + r0 = evaluate_specified(WEDGE, methods=ALL, **spec) + r1 = evaluate_specified(with_s, methods=ALL, **spec) + for m in ALL: + f0, f1 = r0[m]["F"], r1[m]["F"] + if f0 is None or f1 is None: + continue + assert f1 >= f0 - 1e-9, (kind, L, spec, m, f0, f1) + checked += 1 + assert checked >= 40 + + +def test_station_longer_nails_never_lower(): + """측점 꼴(우 깎기) — 네일을 길게 해도 임계 F 가 보강 없음 아래로 안 감(탐색 한계 그대로).""" + base = {c["side"]: c for c in _station(None)["cases"]}["right"] + f0 = base["min_F"]["bishop"] + prev = f0 + for L in (3.0, 8.0, 15.0, 25.0): + case = {c["side"]: c for c in _station([nail(-4.0, 11.48, 15, L, 1.5, 200, 30)])["cases"]} + right = case["right"] + assert right["result"]["limits"] == base["result"]["limits"] + f = right["min_F"]["bishop"] + assert f >= f0 - 1e-9 and f >= prev - 0.02 * prev, (L, f0, prev, f) + prev = f # ---- 보강 없을 때 불변 · 측점 꼴 ------------------------------------------------------