Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-10-26 03:28:31 +09:00
6 changed files with 79 additions and 23 deletions
+5 -1
View File
@@ -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,
+12 -8
View File
@@ -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"]))
+14 -14
View File
@@ -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) =>
+12
View File
@@ -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 }[],
+32
View File
@@ -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"]
+4
View File
@@ -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):