Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_2
This commit is contained in:
@@ -9,10 +9,10 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from E07_StabArea.E07_StabArea_Engine import CLASS_KEYS, RISK_CLASS
|
||||
from E07_StabArea.E07_StabArea_Engine import CLASS_KEYS, LEVEL_KEYS
|
||||
from E07_StabArea.E07_StabArea_Router import RESULT, load
|
||||
|
||||
CLASS_NAMES = ("", "안정", "보통 안정", "준안정", "위험 하한", "위험 상한", "불안정")
|
||||
LEVEL_NAMES = {"low": "낮음", "mid": "보통", "high": "높음"}
|
||||
|
||||
|
||||
def scenario_text(s: dict[str, Any]) -> str:
|
||||
@@ -50,10 +50,13 @@ def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) -
|
||||
|
||||
|
||||
def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[float, str]) -> None:
|
||||
"""s7 — 조건 넷 · 등급별 면적 여섯 · 위험 측점(22 행부터 한 줄씩)."""
|
||||
"""s7 — 조건 넷 · 등급별 면적 여섯 · 측점 등급별 수 · 보통 · 높음 측점(22 행부터 한 줄씩)."""
|
||||
if not area:
|
||||
_set(sheet, "B5", "광역 해석 전 — E07 광역에서 [해석] 뒤 [보고서 갱신]")
|
||||
return
|
||||
if "station_levels" not in area["summary"]:
|
||||
_set(sheet, "B5", "옛 판 저장본 — E07 광역에서 [다시 해석] 뒤 [보고서 갱신]")
|
||||
return
|
||||
s, summary = area["settings"], area["summary"]
|
||||
_set(sheet, "B5", f"중심선 ± {s['band_m']:g} m · 격자 {s['cell_m']:g} m")
|
||||
_set(
|
||||
@@ -67,7 +70,17 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f
|
||||
_set(sheet, "B8", str(area["computed_at"]).replace("T", " "))
|
||||
for k in range(1, 7):
|
||||
_set(sheet, f"C{11 + k}", float(summary["areas_m2"][CLASS_KEYS[k]]))
|
||||
rows = [r for r in area["stations"] if r.get("worst_class", 0) >= RISK_CLASS]
|
||||
lo, hi = s["level_lo_pct"], s["level_hi_pct"]
|
||||
bounds = {"low": f"< {lo:g} %", "mid": f"{lo:g} ~ {hi:g} %", "high": f"≥ {hi:g} %"}
|
||||
for col, text in zip("EFG", ("측점 등급", "SI<1 셀 비율", "측점 수")):
|
||||
_set(sheet, f"{col}11", text, 6)
|
||||
for i, key in enumerate(LEVEL_KEYS):
|
||||
_set(sheet, f"E{12 + i}", LEVEL_NAMES[key], 7)
|
||||
_set(sheet, f"F{12 + i}", bounds[key], 7)
|
||||
_set(sheet, f"G{12 + i}", int(summary["station_levels"][key]), 14)
|
||||
_set(sheet, "A20", f"3. 위험 측점 — 보통 · 높음 (영향 구역 안 SI < 1 셀 비율 {lo:g} % 이상)")
|
||||
_set(sheet, "E21", "측점 등급 (SI<1 비율)")
|
||||
rows = [r for r in area["stations"] if r.get("level") in ("mid", "high")]
|
||||
template = {col: dict(sheet["칸"].get(f"{col}22") or {}) for col in "ABCDEFG"}
|
||||
if not rows:
|
||||
_set(sheet, "A22", "위험 측점 없음")
|
||||
@@ -80,6 +93,6 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f
|
||||
_set(sheet, f"B{r}", ch)
|
||||
_set(sheet, f"C{r}", float(row["fs_min"]))
|
||||
_set(sheet, f"D{r}", float(row["si_min"]))
|
||||
_set(sheet, f"E{r}", CLASS_NAMES[int(row["worst_class"])])
|
||||
_set(sheet, f"E{r}", f"{LEVEL_NAMES[row['level']]} ({row['risk_ratio'] * 100:.1f} %)")
|
||||
_set(sheet, f"F{r}", float(row["risk_area_m2"]))
|
||||
_set(sheet, f"G{r}", float(row["slope_max_deg"]))
|
||||
|
||||
@@ -39,6 +39,9 @@ GAMMA_W = 9.81 # kN/m³
|
||||
SI_EDGES = (1.5, 1.25, 1.0, 0.5) # 등급 1~4 아래 경계 · 5 = (0, 0.5) · 6 = 0
|
||||
CLASS_KEYS = ("", "stable", "moderate", "quasi", "lower", "upper", "defended")
|
||||
RISK_CLASS = 4 # 이 등급부터 위험(SI < 1)
|
||||
LEVEL_KEYS = ("low", "mid", "high") # 측점 등급 — 영향 구역 안 위험 셀 비율 기준
|
||||
LEVEL_LO_PCT = 5.0 # 낮음 < 이 % ≤ 보통
|
||||
LEVEL_HI_PCT = 20.0 # 보통 < 이 % ≤ 높음(이 % 이상 = 높음)
|
||||
FLAT_SLOPE_RAD = 1e-4 # 이보다 평평하면 활동 없음(Fs = ∞)
|
||||
X_STEPS = 20 # SI 확률의 x = R/T 중점 적분 마디
|
||||
|
||||
@@ -263,15 +266,28 @@ def band_cells(
|
||||
return (dist <= half_m).reshape(shape), dense[at, 2].reshape(shape)
|
||||
|
||||
|
||||
def station_level(
|
||||
risk_cells: int, cells: int, lo_pct: float = LEVEL_LO_PCT, hi_pct: float = LEVEL_HI_PCT
|
||||
) -> str:
|
||||
"""측점 등급 — 위험(SI < 1) 셀 비율 < lo_pct 낮음 · lo_pct 이상 hi_pct 미만 보통 ·
|
||||
hi_pct 이상 높음.
|
||||
(정수 곱 비교 — 문턱 경계에서 소수 오차 없음)"""
|
||||
if risk_cells * 100 >= hi_pct * cells:
|
||||
return "high"
|
||||
return "mid" if risk_cells * 100 >= lo_pct * cells else "low"
|
||||
|
||||
|
||||
def station_table(
|
||||
chainages: list[float],
|
||||
cell_chain: np.ndarray,
|
||||
inside: np.ndarray,
|
||||
out: dict[str, np.ndarray],
|
||||
cell_m: float,
|
||||
lo_pct: float = LEVEL_LO_PCT,
|
||||
hi_pct: float = LEVEL_HI_PCT,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""측점마다 맡은 띠 셀(이웃 측점과 중간 누가로 나눔) —
|
||||
최소 Fs · SI · 가장 나쁜 등급 · 위험 면적."""
|
||||
"""측점마다 맡은 띠 셀(이웃 측점과 중간 누가로 나눔 = 앞뒤 ½ 간격 × 띠 폭) —
|
||||
최소 Fs · SI · 가장 나쁜 등급 · 위험 면적 · 위험 셀 비율 · 측점 등급."""
|
||||
chs = np.asarray(sorted(chainages), float)
|
||||
if not len(chs):
|
||||
return []
|
||||
@@ -288,6 +304,7 @@ def station_table(
|
||||
cls = out["cls"][sel]
|
||||
fs = out["fs"][sel]
|
||||
si = out["si"][sel]
|
||||
risk = int((cls >= RISK_CLASS).sum())
|
||||
rows.append(
|
||||
{
|
||||
"chainage_m": float(ch),
|
||||
@@ -295,7 +312,9 @@ def station_table(
|
||||
"fs_min": round(float(np.min(fs)), 3),
|
||||
"si_min": round(float(np.min(si)), 3),
|
||||
"worst_class": int(cls.max()),
|
||||
"risk_area_m2": round(float((cls >= RISK_CLASS).sum()) * cell_m**2, 1),
|
||||
"risk_area_m2": round(risk * cell_m**2, 1),
|
||||
"risk_ratio": round(risk / n, 4),
|
||||
"level": station_level(risk, n, lo_pct, hi_pct),
|
||||
"slope_max_deg": round(float(np.degrees(np.nanmax(out["slope"][sel]))), 1),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ from E07_StabArea import E07_StabArea_Engine as engine
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["E07 Stab Area"])
|
||||
|
||||
ENGINE_VERSION = "e07-2" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈
|
||||
ENGINE_VERSION = "e07-3" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈
|
||||
RESULT = Path("E07_StabArea") / "_derived" / "area.json"
|
||||
DEFAULT_MATERIAL = "colluvium" # 표층 붕괴에 가장 취약(지식DB 안정성평가/04 붕적토)
|
||||
RANGE_C_KPA, RANGE_PHI_DEG = (
|
||||
@@ -76,10 +76,15 @@ class AreaRequest(BaseModel):
|
||||
scenario: Literal["sinmap", "saturated"] = "sinmap" # 표준(습윤지수) · 최악(완전 포화)
|
||||
tr_lo: float = Field(2000.0, ge=1.0, le=1e5) # T/R(m) — SINMAP 기본 보정 2000~3000
|
||||
tr_hi: float = Field(3000.0, ge=1.0, le=1e5)
|
||||
# 측점 등급 문턱(%) — 영향 구역 안 SI < 1 셀 비율 · 브레인 정함(사용자 확정 전)
|
||||
level_lo_pct: float = Field(engine.LEVEL_LO_PCT, gt=0.0, le=100.0)
|
||||
level_hi_pct: float = Field(engine.LEVEL_HI_PCT, gt=0.0, le=100.0)
|
||||
force: bool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _ranges(self) -> AreaRequest:
|
||||
if self.level_hi_pct < self.level_lo_pct:
|
||||
raise ValueError("측점 등급 문턱: 높음이 보통보다 작습니다.")
|
||||
if self.c_hi < self.c or self.phi_hi < self.phi or self.tr_hi < self.tr_lo:
|
||||
raise ValueError("범위 위가 아래보다 작습니다(c · φ · T/R).")
|
||||
return self
|
||||
@@ -178,7 +183,15 @@ def run(
|
||||
inside, chain = engine.band_cells(
|
||||
z.shape, x0, y0, cell, _route_line(root), float(settings["band_m"])
|
||||
)
|
||||
rows = engine.station_table([s["chainage_m"] for s in stations], chain, inside, out, cell)
|
||||
rows = engine.station_table(
|
||||
[s["chainage_m"] for s in stations],
|
||||
chain,
|
||||
inside,
|
||||
out,
|
||||
cell,
|
||||
float(settings["level_lo_pct"]),
|
||||
float(settings["level_hi_pct"]),
|
||||
)
|
||||
labels = {round(float(s["chainage_m"]), 3): s.get("label") for s in stations}
|
||||
for row in rows:
|
||||
row["label"] = labels.get(round(row["chainage_m"], 3))
|
||||
@@ -198,9 +211,7 @@ def run(
|
||||
sum(v for k, v in areas.items() if engine.CLASS_KEYS.index(k) >= engine.RISK_CLASS), 1
|
||||
),
|
||||
"fs_min": round(float(np.min(out["fs"][have])), 3) if have.any() else None,
|
||||
"risk_stations": [
|
||||
r["chainage_m"] for r in rows if r.get("worst_class", 0) >= engine.RISK_CLASS
|
||||
],
|
||||
"station_levels": {k: sum(r.get("level") == k for r in rows) for k in engine.LEVEL_KEYS},
|
||||
"stations": len(rows),
|
||||
}
|
||||
doc = {
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
CLASS_COLORS,
|
||||
CLASS_KEYS,
|
||||
FS_STEPS,
|
||||
RISK_CLASS,
|
||||
LEVEL_COLORS,
|
||||
LEVEL_KEYS,
|
||||
decode,
|
||||
type AreaResult,
|
||||
} from "./logic/E07_StabArea_Raster";
|
||||
@@ -71,6 +72,8 @@ const NUMBER_FIELDS: [string, LocaleKey, "soil" | "rain" | "band"][] = [
|
||||
["tr_hi", "E07_TrHi", "rain"],
|
||||
["band_m", "E07_Band", "band"],
|
||||
["cell_m", "E07_Cell", "band"],
|
||||
["level_lo_pct", "E07_LevelLo", "band"],
|
||||
["level_hi_pct", "E07_LevelHi", "band"],
|
||||
];
|
||||
|
||||
const cacheKey = (projectId: string): string => `e07_settings:${projectId}`;
|
||||
@@ -310,7 +313,10 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
["E07_Scenario", scenarioName(result.settings.scenario)],
|
||||
["E07_Sum_FsMin", s.fs_min === null ? "-" : s.fs_min.toFixed(3)],
|
||||
["E07_Sum_Risk", `${Math.round(s.risk_area_m2).toLocaleString()} m²`],
|
||||
["E07_Sum_RiskStations", `${s.risk_stations.length} / ${s.stations}`],
|
||||
[
|
||||
"E07_Sum_Levels",
|
||||
`${LEVEL_KEYS.map((k) => `${t(`E07_Level_${k}` as LocaleKey)} ${s.station_levels[k]}`).join(" · ")} / ${s.stations}`,
|
||||
],
|
||||
[
|
||||
"E07_Sum_Covered",
|
||||
`${Math.round(s.covered_area_m2).toLocaleString()} / ${Math.round(s.band_area_m2).toLocaleString()} m²`,
|
||||
@@ -357,26 +363,28 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
children: [
|
||||
el("th", { text: t("E07_Col_Station") }),
|
||||
el("th", { text: t("E07_Col_FsMin") }),
|
||||
el("th", { text: t("E07_Col_SiMin") }),
|
||||
el("th", { text: t("E07_Col_Worst") }),
|
||||
el("th", { text: t("E07_Col_Ratio") }),
|
||||
el("th", { text: t("E07_Col_Level") }),
|
||||
el("th", { text: t("E07_Col_RiskArea") }),
|
||||
],
|
||||
}),
|
||||
...result.stations.map((r) => {
|
||||
const worst = el("td");
|
||||
if (r.worst_class)
|
||||
worst.append(
|
||||
swatch(CLASS_COLORS[r.worst_class - 1]),
|
||||
el("span", { text: t(`E07_Class_${CLASS_KEYS[r.worst_class - 1]}` as LocaleKey) }),
|
||||
const level = el("td");
|
||||
if (r.level)
|
||||
level.append(
|
||||
swatch(LEVEL_COLORS[r.level]),
|
||||
el("span", { text: t(`E07_Level_${r.level}` as LocaleKey) }),
|
||||
);
|
||||
const tr = el("tr", {
|
||||
className: (r.worst_class ?? 0) >= RISK_CLASS ? "is-risk" : "",
|
||||
className: r.level === "high" ? "is-risk" : "",
|
||||
attrs: { "data-chainage": String(r.chainage_m) },
|
||||
children: [
|
||||
el("td", { text: shortLabel(r.label, r.chainage_m) }),
|
||||
el("td", { text: r.fs_min === undefined ? "-" : r.fs_min.toFixed(2) }),
|
||||
el("td", { text: r.si_min === undefined ? "-" : r.si_min.toFixed(2) }),
|
||||
worst,
|
||||
el("td", {
|
||||
text: r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1),
|
||||
}),
|
||||
level,
|
||||
el("td", {
|
||||
text:
|
||||
r.risk_area_m2 === undefined ? "-" : Math.round(r.risk_area_m2).toLocaleString(),
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface Grid {
|
||||
ny: number;
|
||||
}
|
||||
|
||||
export type LevelKey = "low" | "mid" | "high";
|
||||
|
||||
export interface StationRow {
|
||||
chainage_m: number;
|
||||
label: string | null;
|
||||
@@ -21,6 +23,8 @@ export interface StationRow {
|
||||
si_min?: number;
|
||||
worst_class?: number;
|
||||
risk_area_m2?: number;
|
||||
risk_ratio?: number;
|
||||
level?: LevelKey;
|
||||
slope_max_deg?: number;
|
||||
}
|
||||
|
||||
@@ -30,7 +34,7 @@ export interface AreaSummary {
|
||||
covered_area_m2: number;
|
||||
risk_area_m2: number;
|
||||
fs_min: number | null;
|
||||
risk_stations: number[];
|
||||
station_levels: Record<LevelKey, number>;
|
||||
stations: number;
|
||||
}
|
||||
|
||||
@@ -62,6 +66,13 @@ export interface Decoded {
|
||||
/** 등급 열쇠(서버 CLASS_KEYS 차례 · 1~6) */
|
||||
export const CLASS_KEYS = ["stable", "moderate", "quasi", "lower", "upper", "defended"] as const;
|
||||
export const RISK_CLASS = 4;
|
||||
/** 측점 등급(서버 LEVEL_KEYS 차례) · 색 */
|
||||
export const LEVEL_KEYS: readonly LevelKey[] = ["low", "mid", "high"];
|
||||
export const LEVEL_COLORS: Record<LevelKey, string> = {
|
||||
low: "#1a9850",
|
||||
mid: "#fee08b",
|
||||
high: "#d73027",
|
||||
};
|
||||
|
||||
/** 등급 색 1~6 — SINMAP 관례(초록 → 빨강) */
|
||||
export const CLASS_COLORS = ["#1a9850", "#91cf60", "#d9ef8b", "#fee08b", "#fc8d59", "#d73027"];
|
||||
|
||||
@@ -2926,7 +2926,7 @@
|
||||
"서식": 16
|
||||
},
|
||||
"A20": {
|
||||
"값": "3. 위험 측점 (SI < 1 셀이 있는 측점)",
|
||||
"값": "3. 위험 측점 — 보통 · 높음 (영향 구역 안 SI < 1 셀 비율 5 % 이상)",
|
||||
"서식": 3
|
||||
},
|
||||
"B20": {
|
||||
@@ -2967,7 +2967,7 @@
|
||||
"서식": 6
|
||||
},
|
||||
"E21": {
|
||||
"값": "가장 나쁜 등급",
|
||||
"값": "측점 등급 (SI<1 비율)",
|
||||
"서식": 6
|
||||
},
|
||||
"F21": {
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
| --- | --- |
|
||||
| `B5` `B6` `B7` `B8` (각 B:H 병합 · 서식 5) | 노선 띠 · 토층 물성 · 시나리오(표준 SINMAP 습윤지수 · 최악 완전 포화) · 해석 때 글 |
|
||||
| `C12`~`C17` | 등급 1~6(안정 · 보통 안정 · 준안정 · 위험 하한 · 위험 상한 · 불안정) 면적 m²(서식 14) · `D` 비율 = 양식 수식 · `C18` 합계 = 양식 수식 |
|
||||
| 22 행부터 위험 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 가장 나쁜 등급 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 |
|
||||
| `E11`~`G14` | 측점 등급 표 — 낮음 · 보통 · 높음별 SI<1 셀 비율 기준 글(7) · 측점 수(14) |
|
||||
| 22 행부터 보통 · 높음 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 측점 등급 + SI<1 비율 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 |
|
||||
|
||||
- 22 행 서식을 줄마다 복사. 위험 측점 = 맡은 띠 셀에 SI < 1(등급 4 이상)이 있는 측점.
|
||||
- 22 행 서식을 줄마다 복사. 측점 등급 = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI < 1(등급 4 이상) 셀 비율 — 낮음 < 5 % · 보통 5~20 % · 높음 ≥ 20 %(문턱 = E07 설정).
|
||||
|
||||
@@ -250,6 +250,8 @@ def test_광역요약_E07_저장본(project):
|
||||
"scenario": "sinmap",
|
||||
"tr_lo": 2000.0,
|
||||
"tr_hi": 3000.0,
|
||||
"level_lo_pct": 5.0,
|
||||
"level_hi_pct": 20.0,
|
||||
},
|
||||
"computed_at": "2026-10-25T21:00:00",
|
||||
"stations": [
|
||||
@@ -260,6 +262,8 @@ def test_광역요약_E07_저장본(project):
|
||||
"si_min": 1.6,
|
||||
"worst_class": 1,
|
||||
"risk_area_m2": 0.0,
|
||||
"risk_ratio": 0.0,
|
||||
"level": "low",
|
||||
"slope_max_deg": 12.0,
|
||||
"label": "No.0",
|
||||
},
|
||||
@@ -270,6 +274,8 @@ def test_광역요약_E07_저장본(project):
|
||||
"si_min": 0.0,
|
||||
"worst_class": 6,
|
||||
"risk_area_m2": 36.0,
|
||||
"risk_ratio": 1.0,
|
||||
"level": "high",
|
||||
"slope_max_deg": 41.5,
|
||||
"label": "No.1",
|
||||
},
|
||||
@@ -280,11 +286,16 @@ def test_광역요약_E07_저장본(project):
|
||||
"si_min": 0.7,
|
||||
"worst_class": 4,
|
||||
"risk_area_m2": 8.0,
|
||||
"risk_ratio": 0.0556,
|
||||
"level": "mid",
|
||||
"slope_max_deg": 33.0,
|
||||
"label": "No.2",
|
||||
},
|
||||
],
|
||||
"summary": {"areas_m2": dict(zip(keys, (100.0, 20.0, 12.0, 8.0, 0.0, 36.0)))},
|
||||
"summary": {
|
||||
"areas_m2": dict(zip(keys, (100.0, 20.0, 12.0, 8.0, 0.0, 36.0))),
|
||||
"station_levels": {"low": 1, "mid": 1, "high": 1},
|
||||
},
|
||||
}
|
||||
path = project / "E07_StabArea" / "_derived" / "area.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
@@ -296,8 +307,11 @@ def test_광역요약_E07_저장본(project):
|
||||
assert [s7[f"C{r}"]["값"] for r in range(12, 18)] == [100.0, 20.0, 12.0, 8.0, 0.0, 36.0]
|
||||
assert [s7[f"A{r}"]["값"] for r in (22, 23)] == ["No.1", "No.2"] and "A24" not in s7
|
||||
assert (
|
||||
s7["E22"]["값"] == "불안정"
|
||||
s7["E22"]["값"] == "높음 (100.0 %)"
|
||||
and s7["E23"]["값"] == "보통 (5.6 %)"
|
||||
and s7["F23"]["값"] == 8.0
|
||||
and [s7[f"G{r}"]["값"] for r in (12, 13, 14)] == [1, 1, 1]
|
||||
and s7["F13"]["값"] == "5 ~ 20 %"
|
||||
and s7["B7"]["값"].startswith("표준 — SINMAP")
|
||||
)
|
||||
vals = book["계산값"]["s7"]
|
||||
|
||||
@@ -190,6 +190,48 @@ def project(tmp_path, monkeypatch):
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_측점_등급_문턱_경계():
|
||||
"""위험 셀 비율 < 5 % 낮음 · 5 % 이상 20 % 미만 보통 · 20 % 이상 높음 (경계 포함은 윗등급)."""
|
||||
cases = [(0, 100, "low"), (4, 100, "low"), (5, 100, "mid"), (19, 100, "mid")]
|
||||
cases += [(20, 100, "high"), (100, 100, "high"), (1, 20, "mid"), (1, 21, "low"), (1, 5, "high")]
|
||||
for risk, cells, want in cases:
|
||||
assert engine.station_level(risk, cells) == want, (risk, cells)
|
||||
# 사용자가 바꾼 문턱
|
||||
assert engine.station_level(9, 100, 10, 30) == "low"
|
||||
assert engine.station_level(30, 100, 10, 30) == "high"
|
||||
|
||||
|
||||
def test_측점_영향_구역과_비율():
|
||||
"""측점 0 · 10 · 30 → 영향 구역 = 이웃과 중간 누가로 나눔 · 띠 안 셀만 · 자료 없음 제외."""
|
||||
chain = np.arange(0.0, 40.0, 1.0)[None, :] # 1 × 40 셀 · 셀 i 누가 = i
|
||||
cls = np.ones(chain.shape, np.uint8)
|
||||
cls[0, [0, 1, 2, 3, 4]] = 4 # 측점 0 영역(0~5 · 정확히 중간 5 는 앞 측점) 6 셀 중 5 → 83 %
|
||||
cls[0, [6, 7]] = 5 # 측점 10 영역(6~20) 셀 14(19 는 띠 밖) · 위험 6 · 7 · 20 → 21 %
|
||||
cls[0, [20, 21]] = 6 # 20 은 측점 10 몫 · 측점 30 영역(21~39) 은 21 하나
|
||||
cls[0, 39] = 0 # 자료 없음 → 셀 수에서 빠짐(18 셀) 1/18 → 5.6 %
|
||||
inside = np.ones(chain.shape, bool)
|
||||
inside[0, 19] = False # 띠 밖 → 빠짐
|
||||
out = {k: np.ones(chain.shape) for k in ("fs", "si", "slope")} | {"cls": cls}
|
||||
rows = engine.station_table([30.0, 0.0, 10.0], chain, inside, out, 2.0)
|
||||
assert [r["chainage_m"] for r in rows] == [0.0, 10.0, 30.0]
|
||||
assert [r["cells"] for r in rows] == [6, 14, 18]
|
||||
assert [r["level"] for r in rows] == ["high", "high", "mid"]
|
||||
assert rows[0]["risk_ratio"] == round(5 / 6, 4) and rows[1]["risk_ratio"] == round(3 / 14, 4)
|
||||
assert rows[2]["risk_ratio"] == round(1 / 18, 4) and rows[2]["risk_area_m2"] == 4.0
|
||||
# 문턱을 올리면 등급이 내려감
|
||||
again = engine.station_table([0.0, 10.0, 30.0], chain, inside, out, 2.0, 15.0, 50.0)
|
||||
assert [r["level"] for r in again] == ["high", "mid", "low"]
|
||||
|
||||
|
||||
def test_측점_문턱_설정_검증():
|
||||
base = router_mod.AreaRequest.model_fields
|
||||
assert (base["level_lo_pct"].default, base["level_hi_pct"].default) == (5.0, 20.0)
|
||||
ok = dict(gamma_t=18.0, gamma_sat=20.0, c=0.0, c_hi=0.0, phi=30.0, phi_hi=30.0)
|
||||
router_mod.AreaRequest(**ok, level_lo_pct=10, level_hi_pct=10)
|
||||
with pytest.raises(ValueError):
|
||||
router_mod.AreaRequest(**ok, level_lo_pct=30, level_hi_pct=10)
|
||||
|
||||
|
||||
def test_길_띠_측점표_저장본(project):
|
||||
base = router_mod.defaults(project)
|
||||
assert base["scenario"] == "sinmap" and (base["tr_lo"], base["tr_hi"]) == (2000.0, 3000.0)
|
||||
@@ -216,7 +258,8 @@ def test_길_띠_측점표_저장본(project):
|
||||
rows = doc["stations"]
|
||||
assert [r["chainage_m"] for r in rows] == [0, 20, 40, 60, 80, 100]
|
||||
assert all(r["label"] == f"No.{i}" and r["worst_class"] >= 4 for i, r in enumerate(rows))
|
||||
assert summary["risk_stations"] == [0, 20, 40, 60, 80, 100]
|
||||
assert all(r["level"] == "high" and r["risk_ratio"] > 0.99 for r in rows) # 완전 포화 30° 비탈
|
||||
assert summary["station_levels"] == {"low": 0, "mid": 0, "high": 6}
|
||||
assert sum(r["cells"] for r in rows) == (cls > 0).sum()
|
||||
# 지문 = 설정에 따라 바뀜
|
||||
assert router_mod.fingerprint("s", settings) != router_mod.fingerprint(
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ui_locales_e07 = {
|
||||
E07_ReadOnly: ["보기만 허용된 프로젝트입니다.", "This project is view-only."],
|
||||
E07_Card_Soil: ["토층 물성", "Soil layer"],
|
||||
E07_Card_Rain: ["시나리오 · 포화", "Scenario · saturation"],
|
||||
E07_Card_Band: ["노선 띠 · 격자", "Route band · grid"],
|
||||
E07_Card_Band: ["노선 띠 · 격자 · 측점 등급", "Route band · grid · station level"],
|
||||
E07_Material: ["물성(E02 지반 조건)", "Material (E02 ground)"],
|
||||
E07_GammaT: ["γt (kN/m³)", "γt (kN/m³)"],
|
||||
E07_GammaSat: ["γsat (kN/m³)", "γsat (kN/m³)"],
|
||||
@@ -40,6 +40,8 @@ export const ui_locales_e07 = {
|
||||
E07_TrHi: ["T/R 위 (m · 마른 쪽)", "T/R high (m · drier)"],
|
||||
E07_Band: ["띠 폭 ± (m)", "Band half-width (m)"],
|
||||
E07_Cell: ["격자 (m)", "Cell (m)"],
|
||||
E07_LevelLo: ["측점 보통 문턱 (SI<1 비율 %)", "Station mid threshold (SI<1 share %)"],
|
||||
E07_LevelHi: ["측점 높음 문턱 (SI<1 비율 %)", "Station high threshold (SI<1 share %)"],
|
||||
E07_Defaults: ["기본값", "Defaults"],
|
||||
E07_Analyze: ["해석", "Analyze"],
|
||||
E07_Reanalyze: ["다시 해석", "Re-analyze"],
|
||||
@@ -74,20 +76,23 @@ export const ui_locales_e07 = {
|
||||
],
|
||||
E07_Sum_FsMin: ["띠 안 최소 Fs", "Min Fs in band"],
|
||||
E07_Sum_Risk: ["위험 면적 (SI < 1)", "Hazard area (SI < 1)"],
|
||||
E07_Sum_RiskStations: ["위험 측점", "Hazard stations"],
|
||||
E07_Sum_Levels: ["측점 등급 (낮음 · 보통 · 높음)", "Station levels (low · mid · high)"],
|
||||
E07_Sum_Covered: ["지형 있는 띠 / 띠", "Band with terrain / band"],
|
||||
E07_Sum_Time: ["해석", "Computed"],
|
||||
E07_Areas_Title: ["등급별 면적", "Area by class"],
|
||||
E07_Stations_Title: ["측점별 위험 (누르면 지도로)", "Hazard by station (click to locate)"],
|
||||
E07_Level_low: ["낮음", "Low"],
|
||||
E07_Level_mid: ["보통", "Medium"],
|
||||
E07_Level_high: ["높음", "High"],
|
||||
E07_Col_Class: ["등급", "Class"],
|
||||
E07_Col_Area: ["면적 (m²)", "Area (m²)"],
|
||||
E07_Col_Station: ["측점", "Station"],
|
||||
E07_Col_FsMin: ["최소 Fs", "Min Fs"],
|
||||
E07_Col_SiMin: ["최소 SI", "Min SI"],
|
||||
E07_Col_Worst: ["나쁜 등급", "Worst class"],
|
||||
E07_Col_Ratio: ["SI<1 비율 (%)", "SI<1 share (%)"],
|
||||
E07_Col_Level: ["측점 등급", "Station level"],
|
||||
E07_Col_RiskArea: ["위험 m²", "Hazard m²"],
|
||||
E07_Defaults_Source: [
|
||||
"기본값 = 근거 후보 · 바꿀 수 있음 — 물성 = E02 붕적토 · T/R 2,000~3,000 m = SINMAP 설명서 기본 보정 · 뿌리 점착 1 kPa = 국내 연구(산림 0~5 kPa · 나지 · 절성토면 0).",
|
||||
"Defaults are evidence-based candidates and editable — soil = E02 colluvium · T/R 2,000–3,000 m = SINMAP manual default · root cohesion 1 kPa = Korean studies (forest 0–5 kPa · bare / cut slopes 0).",
|
||||
"기본값 = 근거 후보 · 바꿀 수 있음 — 물성 = E02 붕적토 · T/R 2,000~3,000 m = SINMAP 설명서 기본 보정 · 뿌리 점착 1 kPa = 국내 연구(산림 0~5 kPa · 나지 · 절성토면 0) — 사용자 확정 전 · 측점 등급 문턱 5 % · 20 % = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI<1 셀 비율 — 사용자 확정 전.",
|
||||
"Defaults are evidence-based candidates and editable — soil = E02 colluvium · T/R 2,000–3,000 m = SINMAP manual default · root cohesion 1 kPa = Korean studies (forest 0–5 kPa · bare / cut slopes 0) — not yet confirmed by the user · station level thresholds 5 % · 20 % = share of SI<1 cells in the station zone (half-spacing either side × band width) — not yet confirmed by the user.",
|
||||
],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user