fix(E04): 3D/2D 비 = 3D 와 같은 조건 2D 안전율(fs_by_condition → min) · 해석 물성 = E02 작업본 → 초기값 → 마스터 기본값 · 층 물성 = E02 label_map 짝 (533-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ ROCK_KINDS = ("ripping_rock", "blasting_rock")
|
||||
#: 층 → 물성 id(E03 기본표) · 못 뚫는 층
|
||||
LAYER_MATERIALS = ("fill", "weathered_soil", "weathered_rock", "soft_rock")
|
||||
LAYER_HARD = (False, False, False, True)
|
||||
LAYER_LABELS = ("fill", "soil", "ripping_rock", "blasting_rock") # E02 label_map 열쇠
|
||||
|
||||
|
||||
def station_from_b06(cross: dict, design: dict) -> dict:
|
||||
@@ -255,12 +256,15 @@ def build_grid(
|
||||
def strata_from_grid(
|
||||
grid: dict,
|
||||
materials: list[dict] | None = None,
|
||||
label_map: dict[str, str] | None = None,
|
||||
*,
|
||||
water_depth_m: float | None = None,
|
||||
with_surcharge: bool = True,
|
||||
) -> Strata:
|
||||
"""격자 → 기둥 솔버 지층(층 차례 = 머리 주석) · 물성은 id 로 고름(E03 꼴 · 빠지면 기본표).
|
||||
|
||||
label_map — E02 층 이름 → 물성 id(soil · ripping_rock · blasting_rock · fill) · 빠지면 기본 짝.
|
||||
|
||||
water_depth_m — 지하수위면 = 합성 지표 − 깊이(0 = 지표까지 포화 · KDS 우기 규칙)
|
||||
· None = 지하수 없음.
|
||||
"""
|
||||
@@ -276,7 +280,7 @@ def strata_from_grid(
|
||||
grid["y"],
|
||||
z,
|
||||
np.stack([fill_bot, soil_bot, rip_bot]),
|
||||
[by_id[k] for k in LAYER_MATERIALS],
|
||||
[by_id[(label_map or {}).get(lab, k)] for lab, k in zip(LAYER_LABELS, LAYER_MATERIALS)],
|
||||
hard=list(LAYER_HARD),
|
||||
z_water=None if water_depth_m is None else z - water_depth_m,
|
||||
q=grid["q"] if with_surcharge else None,
|
||||
|
||||
@@ -28,14 +28,13 @@ import numpy as np
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
from E01_StabRegister import E01_StabRegister_Source as source
|
||||
from E02_StabGround.E02_StabGround_Model import load_defaults as load_ground_defaults
|
||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||
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" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈
|
||||
STAGE_DIR = "E04_Stab3D"
|
||||
GROUND_FILE = Path("E02_StabGround") / "ground.json"
|
||||
SECTION_LENGTH_M = 100.0 # 자동 구간 길이
|
||||
MAX_SECTION_M = 500.0 # 한 번에 셀 구간 상한(시간 · 메모리)
|
||||
METHOD_NAMES = ("bishop", "janbu_corrected")
|
||||
@@ -69,10 +68,12 @@ def auto_sections(stations: list[dict[str, Any]]) -> list[dict[str, float]]:
|
||||
return out
|
||||
|
||||
|
||||
def read_ground(project_root: Path) -> dict[str, Any] | None:
|
||||
"""E02 작업본(없으면 None = 기본값)."""
|
||||
path = Path(project_root) / GROUND_FILE
|
||||
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
|
||||
def read_ground(project_root: Path) -> dict[str, Any]:
|
||||
"""E02 작업본 → 없으면 초기값 → 없으면 마스터 기본값(E05 와 같은 차례)."""
|
||||
for path in (ground_model.ground_path(project_root), ground_model.initial_path(project_root)):
|
||||
if path.is_file():
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return ground_model.pick(ground_model.load_defaults())
|
||||
|
||||
|
||||
def fingerprint(
|
||||
@@ -171,7 +172,7 @@ def _floor(strata: Any, best: dict[str, Any]) -> list[list[float]]:
|
||||
|
||||
def reference_fs(ground: dict[str, Any] | None, condition: str) -> float:
|
||||
"""기준안전율(E02 · 없으면 마스터 기본) — 깎기 · 쌓기 중 큰 값(보수)."""
|
||||
factors = (ground or load_ground_defaults()).get("safety_factors") or {}
|
||||
factors = (ground or ground_model.load_defaults()).get("safety_factors") or {}
|
||||
row = factors.get(condition) or {}
|
||||
return float(max(row.get("cut", 1.5), row.get("fill", 1.5)))
|
||||
|
||||
@@ -202,7 +203,10 @@ def run_engine(
|
||||
)
|
||||
wet = settings["condition"] == "wet"
|
||||
strata = grid_engine.strata_from_grid(
|
||||
grid, (ground or {}).get("materials"), water_depth_m=0.0 if wet else None
|
||||
grid,
|
||||
(ground or {}).get("materials"),
|
||||
(ground or {}).get("label_map"),
|
||||
water_depth_m=0.0 if wet else None,
|
||||
)
|
||||
mask = _node_mask(stations, grid, settings["start_m"], settings["end_m"], half)
|
||||
ratios = (1.0,) if settings["shape"] == "sphere" else (1.0, float(settings["ellipsoid_ratio"]))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* E04 안정성평가 3D 해석(요구 347-1 · 요구서 4-2 · 4-3 · PLAN 533-3) — B05~B09 틀(group_workflow).
|
||||
* 좌 = 구간 목록 · 탐색 설정(기본 · 고급 접힘) · [해석] · 진행률 · [끊기]
|
||||
* 가운데 = 평면 지도(셀별 최소 안전율 · 임계 체적) · [3D 보기](지표 + 노선면 + 임계 활동면)
|
||||
* 우 = 구간 결과(3D 최소 · 2D 최소(같은 구간 E03 측점 fs_min 최소) · 3D/2D 비 · 체적 · 방법)
|
||||
* 우 = 구간 결과(3D 최소 · 2D 최소(같은 구간 E03 측점 · 3D 와 같은 조건 안전율 최소) · 3D/2D 비 · 체적 · 방법)
|
||||
* 계산 = 서버 단독(POST …/stab/analyze3d · 뒤에서) · 화면은 값 · 그림만. 탐색 설정 = sessionStorage.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
FS_STEPS,
|
||||
VOLUME_STEPS,
|
||||
legendRows,
|
||||
fs2dOf,
|
||||
min2d,
|
||||
ratio3d2d,
|
||||
stepColor,
|
||||
@@ -73,18 +74,13 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return body;
|
||||
}
|
||||
|
||||
/** E03 2D 측점 요약(GET …/stab/result2d · stations[].fs_min · 측점 하나에 깎기 · 쌓기 두 줄) — 없으면 빈 목록 */
|
||||
async function fetch2d(
|
||||
projectId: string,
|
||||
): Promise<{ chainage_m: number; fs_min: number | null }[]> {
|
||||
/** E03 2D 측점 줄(GET …/stab/result2d · fs_by_condition · min = 조건별) — 없으면 빈 목록 */
|
||||
type Station2d = Parameters<typeof fs2dOf>[0] & { chainage_m: number };
|
||||
|
||||
async function fetch2d(projectId: string): Promise<Station2d[]> {
|
||||
try {
|
||||
const body = await api<{ stations?: Record<string, unknown>[] }>(
|
||||
`/projects/${projectId}/stab/result2d`,
|
||||
);
|
||||
return (body.stations ?? []).map((s) => ({
|
||||
chainage_m: Number(s.chainage_m),
|
||||
fs_min: typeof s.fs_min === "number" ? s.fs_min : null,
|
||||
}));
|
||||
const body = await api<{ stations?: Station2d[] }>(`/projects/${projectId}/stab/result2d`);
|
||||
return (body.stations ?? []).map((s) => ({ ...s, chainage_m: Number(s.chainage_m) }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -227,7 +223,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
// ---- 상태 ----
|
||||
let overview: Overview | null = null;
|
||||
let result: Result3d | null = null;
|
||||
let stations2d: { chainage_m: number; fs_min: number | null }[] = [];
|
||||
let stations2d: Station2d[] = [];
|
||||
let running = false;
|
||||
|
||||
const remember = (): void =>
|
||||
@@ -290,7 +286,11 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
return;
|
||||
}
|
||||
const s = result.summary;
|
||||
const f2d = min2d(stations2d, s.start_m, s.end_m);
|
||||
const f2d = min2d(
|
||||
stations2d.map((x) => ({ chainage_m: x.chainage_m, fs_min: fs2dOf(x, s.condition) })),
|
||||
s.start_m,
|
||||
s.end_m,
|
||||
);
|
||||
const ratio = ratio3d2d(s.f_min, f2d);
|
||||
const station = overview?.stations.reduce<StationPoint | null>(
|
||||
(best, x) =>
|
||||
|
||||
@@ -180,6 +180,18 @@ export function rasterRgba(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 2D 측점 한 줄의 3D 조건 안전율 — 지배 비탈 fs_by_condition · 없으면 조건별 최소 min · 없으면 null */
|
||||
export function fs2dOf(
|
||||
row: {
|
||||
fs_by_condition?: Record<string, number | null>;
|
||||
min?: Record<string, number | null>;
|
||||
},
|
||||
condition: string,
|
||||
): number | null {
|
||||
const v = row.fs_by_condition?.[condition] ?? row.min?.[condition];
|
||||
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
/** 구간 안 2D 측점 최소 안전율(없으면 null) */
|
||||
export function min2d(
|
||||
stations: readonly { chainage_m: number; fs_min: number | null }[],
|
||||
|
||||
@@ -182,3 +182,35 @@ def test_node_mask_section_and_route_ends(root):
|
||||
assert mask[(np.abs(yy) <= 12.0) & (xx >= 1) & (xx <= 99)].all()
|
||||
part = run._node_mask(STATIONS, grid, 20.0, 60.0, 12.0)
|
||||
assert not part[(xx < 19.5) | (xx > 60.5)].any() and part[(yy == 0) & (xx == 40)].all()
|
||||
|
||||
|
||||
def test_ground_order_and_label_map(tmp_path):
|
||||
"""물성 = E02 작업본 → 초기값 → 마스터 기본값(성토 φ 28) · 층 물성 = label_map 짝."""
|
||||
import json
|
||||
|
||||
from E04_Stab3D import E04_Stab3D_Engine_Grid as grid_engine
|
||||
|
||||
assert {m["id"]: m["phi"] for m in run.read_ground(tmp_path)["materials"]}["fill"] == 28.0
|
||||
init = tmp_path / "initial_snapshot" / "E02_StabGround" / "ground.json"
|
||||
init.parent.mkdir(parents=True)
|
||||
init.write_text(json.dumps({"tag": "initial"}), encoding="utf-8")
|
||||
assert run.read_ground(tmp_path) == {"tag": "initial"}
|
||||
work = tmp_path / "E02_StabGround" / "ground.json"
|
||||
work.parent.mkdir(parents=True)
|
||||
work.write_text(json.dumps({"tag": "work"}), encoding="utf-8")
|
||||
assert run.read_ground(tmp_path) == {"tag": "work"}
|
||||
|
||||
z = np.full((2, 2), 10.0)
|
||||
grid = {
|
||||
"x": np.arange(2.0),
|
||||
"y": np.arange(2.0),
|
||||
"z": z,
|
||||
"z_orig": z,
|
||||
"rock_top": z - 3,
|
||||
"rock_kind": np.ones((2, 2), int),
|
||||
"q": np.zeros((2, 2)),
|
||||
}
|
||||
ids = [m["id"] for m in grid_engine.strata_from_grid(grid).materials]
|
||||
assert ids == ["fill", "weathered_soil", "weathered_rock", "soft_rock"]
|
||||
mapped = grid_engine.strata_from_grid(grid, label_map={"soil": "colluvium", "fill": "fill"})
|
||||
assert [m["id"] for m in mapped.materials][:2] == ["fill", "colluvium"]
|
||||
|
||||
@@ -56,6 +56,9 @@ writeFileSync(process.argv[3], JSON.stringify({
|
||||
min2d: R.min2d([{ chainage_m: 0, fs_min: 1.1 }, { chainage_m: 20, fs_min: 1.4 },
|
||||
{ chainage_m: 40, fs_min: null }, { chainage_m: 60, fs_min: 0.9 }], 10, 50),
|
||||
ratio: [R.ratio3d2d(1.68, 1.4), R.ratio3d2d(1.5, null), R.ratio3d2d(1.5, 0)],
|
||||
fs2d: [R.fs2dOf({ fs_by_condition: { wet: 1.2 }, min: { wet: 1.1 } }, "wet"),
|
||||
R.fs2dOf({ fs_by_condition: { dry: 1.6 }, min: { wet: 1.1 } }, "wet"),
|
||||
R.fs2dOf({ fs_by_condition: { wet: null }, min: {} }, "wet"), R.fs2dOf({}, "dry")],
|
||||
terrain: mesh(M.terrainMesh(c.grid, c.z)),
|
||||
sphere: mesh(M.slipMesh(c.grid, c.z, c.sphere)),
|
||||
ellipsoid: mesh(M.slipMesh(c.grid, c.z, c.ellipsoid)),
|
||||
@@ -120,6 +123,7 @@ def test_raster_min_pixels_ratio(ts):
|
||||
assert ts["pixelEmpty"] == 0 # 빈 셀 = 투명
|
||||
assert ts["min2d"] == 1.4 # 10~50 m 안 = 20 m(1.4) · null 뺌
|
||||
assert ts["ratio"][0] == pytest.approx(1.2) and ts["ratio"][1:] == [None, None]
|
||||
assert ts["fs2d"] == [1.2, 1.1, None, None] # 지배 비탈 → 조건별 최소 → 없음
|
||||
|
||||
|
||||
def _py_slip(best, x, y):
|
||||
|
||||
Reference in New Issue
Block a user