diff --git a/E03_StabSection/E03_StabSection_Engine_Station.py b/E03_StabSection/E03_StabSection_Engine_Station.py index dbfdae283..79da300be 100644 --- a/E03_StabSection/E03_StabSection_Engine_Station.py +++ b/E03_StabSection/E03_StabSection_Engine_Station.py @@ -8,6 +8,7 @@ import time from E03_StabSection.E03_StabSection_Engine_Model import section_model_from_b06 from E03_StabSection.E03_StabSection_Engine_Search import METHODS, analyze +from E03_StabSection.E03_StabSection_Types import groundwater as norm_groundwater #: 활동면 최소 깊이(m) — 지표를 얇게 긁는 면(표토 박리)은 사면 붕괴로 보지 않음. MIN_DEPTH_M = 0.3 @@ -26,10 +27,14 @@ def analyze_station( noncircular: bool = True, min_depth: float = MIN_DEPTH_M, case_workers: int = 1, + groundwater_for=None, **model_kw, ) -> dict: """측점 하나 — case_workers > 1 이면 비탈 × 조건 경우를 프로세스로 나눠 돎(측점 하나만 다시 셀 때). + groundwater_for(condition, model) → 엔진 지하수 꼴 — 주면 조건마다 모형 지하수를 갈아 끼움 + (E02 `station_groundwater` 꼴) · None 이면 모형 기본(우기 = 지표 수위). + 서버 프로세스 안에서는 1(차례로) 그대로 둠 — 자식이 main.py 를 다시 읽음(`Worker` 머리글). """ t0 = time.perf_counter() @@ -46,6 +51,8 @@ def analyze_station( seismic_kh=seismic_kh if cond == "seismic" else 0.0, **model_kw, ) + if groundwater_for is not None: + model["groundwater"] = norm_groundwater(groundwater_for(cond, model)) out.setdefault("areas", model["areas"]) jobs += [(cond, model, side) for side in model["sides"]] kw = {"methods": methods, "noncircular": noncircular, "min_depth": min_depth} diff --git a/E04_Stab3D/E04_Stab3D_Engine_Search.py b/E04_Stab3D/E04_Stab3D_Engine_Search.py index ef4562e56..3ef40f964 100644 --- a/E04_Stab3D/E04_Stab3D_Engine_Search.py +++ b/E04_Stab3D/E04_Stab3D_Engine_Search.py @@ -485,52 +485,57 @@ def search(strata: Strata, params: SearchParams | None = None) -> SearchResult: ) return improved - step = max(1, int(p.coarse)) - grid = np.stack( - np.meshgrid( - np.arange(0, nhs, step), np.arange(0, nys, step), np.arange(0, nxs, step), indexing="ij" - ), - -1, - ).reshape(-1, 3) - ids = (grid[:, 0] * nys + grid[:, 1]) * nxs + grid[:, 2] - seen.update(ids.tolist()) - seeds = run(ids) - rounds = passes = 0 - while seeds and (step > 1 or rounds < p.refine_rounds): - if step == 1: - rounds += 1 - step = max(1, step // 2) - passes += 1 - stage[:] = [min(40 + 8 * (passes - 1), 77), 8, f"고운 탐색 {passes}"] - cand = set() - for nid in seeds: - ih, rem = divmod(nid, nys * nxs) - iy, ix = divmod(rem, nxs) - for dh in (-step, 0, step): - for dy in (-step, 0, step): - for dxs in (-step, 0, step): - h2, y2, x2 = ih + dh, iy + dy, ix + dxs - if 0 <= h2 < nhs and 0 <= y2 < nys and 0 <= x2 < nxs: - cid = (h2 * nys + y2) * nxs + x2 - if cid not in seen: - cand.add(cid) - if not cand: + try: + step = max(1, int(p.coarse)) + grid = np.stack( + np.meshgrid( + np.arange(0, nhs, step), + np.arange(0, nys, step), + np.arange(0, nxs, step), + indexing="ij", + ), + -1, + ).reshape(-1, 3) + ids = (grid[:, 0] * nys + grid[:, 1]) * nxs + grid[:, 2] + seen.update(ids.tolist()) + seeds = run(ids) + rounds = passes = 0 + while seeds and (step > 1 or rounds < p.refine_rounds): if step == 1: - break - continue - seen.update(cand) - seeds = run(np.fromiter(cand, np.int64)) + rounds += 1 + step = max(1, step // 2) + passes += 1 + stage[:] = [min(40 + 8 * (passes - 1), 77), 8, f"고운 탐색 {passes}"] + cand = set() + for nid in seeds: + ih, rem = divmod(nid, nys * nxs) + iy, ix = divmod(rem, nxs) + for dh in (-step, 0, step): + for dy in (-step, 0, step): + for dxs in (-step, 0, step): + h2, y2, x2 = ih + dh, iy + dy, ix + dxs + if 0 <= h2 < nhs and 0 <= y2 < nys and 0 <= x2 < nxs: + cid = (h2 * nys + y2) * nxs + x2 + if cid not in seen: + cand.add(cid) + if not cand: + if step == 1: + break + continue + seen.update(cand) + seeds = run(np.fromiter(cand, np.int64)) - # 타원체 — 구 탐색이 찾은 임계 노드(F < refine_below)에서만(시간 1/3 · 노선 1 km 실측) - ell = tuple(r for r in p.ratios if r != 1.0) - limit = max(p.refine_below, 1.2 * best["governing"]) - crit = np.unique(node_map[(node_map >= 0) & (f_min < limit)]) - if ell and len(crit): - stage[:] = [85, 14, "타원체"] - run(crit, ell) + # 타원체 — 구 탐색이 찾은 임계 노드(F < refine_below)에서만(시간 1/3 · 노선 1 km 실측) + ell = tuple(r for r in p.ratios if r != 1.0) + limit = max(p.refine_below, 1.2 * best["governing"]) + crit = np.unique(node_map[(node_map >= 0) & (f_min < limit)]) + if ell and len(crit): + stage[:] = [85, 14, "타원체"] + run(crit, ell) - if pool: - pool.shutdown() + finally: + if pool: # 끊기(InterruptedError) · 오류에도 일꾼을 닫음 + pool.shutdown(cancel_futures=True) f_out = np.where(np.isfinite(f_min), f_min, np.nan) return SearchResult( f_min=f_out, diff --git a/resources/tester/test_532_4_stab_walls_water.py b/resources/tester/test_532_4_stab_walls_water.py index 4b688811b..86bb68eb5 100644 --- a/resources/tester/test_532_4_stab_walls_water.py +++ b/resources/tester/test_532_4_stab_walls_water.py @@ -238,6 +238,28 @@ def test_지하수_측점_모형에_끼움(): assert res["methods"]["bishop"]["min"]["F"] > 0 # 끼운 모형이 엔진에서 돎 +def test_측점_해석_groundwater_for_인자(): + ground = ground_model.build_initial(ground_model.load_defaults(), []) + ground["groundwater"].update(wet_mode="ru", ru=0.3) + seen = [] + + def gw_for(cond, model): + seen.append(cond) + return ground_model.station_groundwater(ground, cond, model) + + kw = {"conditions": ("dry", "wet"), "methods": ("bishop",), "noncircular": False} + base = analyze_station(CROSS, DESIGN, **kw) + out = analyze_station(CROSS, DESIGN, groundwater_for=gw_for, **kw) + assert seen == ["dry", "wet"] + F = {(c["condition"], c["side"]): c["min_F"]["bishop"] for c in out["cases"]} + F0 = {(c["condition"], c["side"]): c["min_F"]["bishop"] for c in base["cases"]} + for (cond, side), f in F.items(): + if cond == "dry": + assert f == pytest.approx(F0[cond, side]) # 건기 = 수위 없음 그대로 + else: + assert f > F0[cond, side] # r_u 0.3 < 지표 포화 + + @pytest.fixture def project(tmp_path, monkeypatch): async def fake_project(_project_id):