Files
Aislo/E03_StabSection/logic/E03_StabSection_Prob.ts
T
eomsangdonandClaude Opus 5.5 210109e189 feat(E03): 「확률 · 민감도」 탭 — 분포 입력 · 기본값 후보(근거 · 사용자 확정 전) · 카드 안 진행률 · 히스토그램 · 누적분포 · 민감도 · 토네이도 SVG (537-1 · 요구 351)
- UI_Prob(새 파일): 대표 단면 · 고른 비탈 · 조건 · 단면 물성 × c · φ · γ(+ 수위선 = 지하수위 이동 · r_u 갈래 = r_u) ·
  쓰기 · 분포 · 변동계수/σ · 절단 ±kσ · 임계면 고정 / 매회 탐색 · 방법 · LHS / MC · N · 시드 · c–φ 상관 · [기본값] ·
  입력 = sessionStorage 캐시(자동저장 없음) · 계산 = 서버(잠금판 없이 카드 안 진행률)
- UI_ProbChart(새 파일): F < 1 빨강 히스토그램 · F = 1 · 기준 · 결정론 선 · 누적분포(Pf) · 민감도(범위 %) · 토네이도
- logic/E03_StabSection_Prob.ts: 변수 줄 · 기본값 후보(물성 덮어쓰기) · 요청 꼴 · 바꾼 줄 수 · 눈금
- Page: 보기 고름에 「확률 · 민감도」 · 문구 locale_e03

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 05:04:55 +09:00

267 lines
7.4 KiB
TypeScript

/* =============================================================================
* E03_StabSection_Prob.ts
* E03 「확률 · 민감도」 탭 계산(PLAN 537-1) — 변수 줄(단면 물성 × c · φ · γ + 지하수위 · r_u) ·
* 기본값 후보(서버 `prob_defaults.json`) · 입력 캐시 → 요청 꼴 · 그래프 눈금.
* 화면 · fetch · sessionStorage 없음(페이지가 부름).
* ========================================================================== */
export type ProbProp = "c" | "phi" | "gamma" | "water" | "ru";
export type ProbDist = "normal" | "lognormal";
export type ProbMethod = "ordinary" | "bishop" | "janbu" | "spencer" | "morgenstern_price";
export interface ProbPropDefault {
dist: ProbDist;
cov?: number;
std?: number;
trunc_sigma: number | null;
}
export interface ProbDefaults {
run: {
n: number;
sampling: "lhs" | "mc";
seed: number;
mode: "fixed" | "search";
method: ProbMethod;
};
properties: Record<ProbProp, ProbPropDefault>;
rho_c_phi: number;
sources: Record<string, string>;
materials?: Record<string, Partial<Record<ProbProp, Partial<ProbPropDefault>>>>;
}
export interface ProbMaterial {
id: string;
name: string | null;
c: number;
phi: number;
gamma_t: number;
}
export interface ProbStats {
n: number;
n_valid: number;
mean: number | null;
std: number | null;
cov?: number | null;
min?: number;
max?: number;
pf: number | null;
n_fail?: number;
beta_normal: number | null;
beta_lognormal?: number | null;
required?: number | null;
p_below_required?: number | null;
histogram?: { edges: number[]; counts: number[] };
cdf?: { p: number[]; F: number[] };
}
export interface ProbCurve {
name: string;
x: (number | null)[];
F: (number | null)[];
F_lo: number | null;
F_hi: number | null;
swing: number | null;
}
export interface ProbVariable {
name: string;
material: string | null;
prop: ProbProp;
dist: ProbDist;
mean: number;
std: number;
lo: number | null;
hi: number | null;
}
export interface ProbResult {
mode: "fixed" | "search";
method: ProbMethod;
n: number;
sampling: string;
seed: number;
variables: ProbVariable[];
stats: ProbStats;
sensitivity: {
F_mean: number | null;
curves: ProbCurve[];
tornado: { name: string; F_lo: number | null; F_hi: number | null; swing: number }[];
};
elapsed_s: { total: number; samples: number };
}
export interface ProbRecord {
station_key: string;
side: string;
condition: string;
required: number | null;
fs_deterministic: number | null;
request: Record<string, unknown>;
result: ProbResult;
computed_at: string;
}
export interface ProbSetup {
station_key: string;
side: string;
condition: string;
role: "cut" | "fill";
required: number | null;
fs_deterministic: Record<string, number | null>;
materials: ProbMaterial[];
groundwater: { mode: "none" | "line" | "ru"; ru: number | null };
defaults: ProbDefaults;
search_max_n: number;
last: ProbRecord | null;
}
/** 변수 줄 하나 — kind cov = 변동계수(평균 곱) · std = 표준편차(m · r_u) */
export interface ProbRow {
key: string;
material: string | null;
prop: ProbProp;
name: string;
mean: number;
kind: "cov" | "std";
}
export interface ProbInput {
use: boolean;
dist: ProbDist;
value: number;
trunc: number | null;
}
export interface ProbRunInput {
n: number;
sampling: "lhs" | "mc";
seed: number;
mode: "fixed" | "search";
method: ProbMethod;
rho: number;
}
export interface ProbCache {
vars: Record<string, ProbInput>;
run: ProbRunInput;
}
export const varKey = (material: string | null, prop: ProbProp): string =>
`${material ?? "*"}|${prop}`;
/** 단면 물성마다 c · φ · γ + 지하수 갈래가 수위선이면 지하수위 · r_u 면 r_u */
export function probRows(setup: ProbSetup): ProbRow[] {
const rows: ProbRow[] = [];
for (const m of setup.materials) {
const name = m.name ?? m.id;
rows.push(
{ key: varKey(m.id, "c"), material: m.id, prop: "c", name, mean: m.c, kind: "cov" },
{ key: varKey(m.id, "phi"), material: m.id, prop: "phi", name, mean: m.phi, kind: "cov" },
{
key: varKey(m.id, "gamma"),
material: m.id,
prop: "gamma",
name,
mean: m.gamma_t,
kind: "cov",
},
);
}
const gw = setup.groundwater;
if (gw.mode === "line")
rows.push({
key: varKey(null, "water"),
material: null,
prop: "water",
name: "",
mean: 0,
kind: "std",
});
if (gw.mode === "ru")
rows.push({
key: varKey(null, "ru"),
material: null,
prop: "ru",
name: "",
mean: gw.ru ?? 0,
kind: "std",
});
return rows;
}
/** 기본값 후보 — 성질 기본 위에 물성별 덮어쓰기 · 0 이면 끔 */
export function defaultInput(defaults: ProbDefaults, row: ProbRow): ProbInput {
const base = defaults.properties[row.prop];
const own = (row.material && defaults.materials?.[row.material]?.[row.prop]) || {};
const merged = { ...base, ...own };
const value = Number((row.kind === "cov" ? merged.cov : merged.std) ?? 0);
return {
use: value > 0,
dist: row.prop === "water" ? "normal" : merged.dist,
value,
trunc: merged.trunc_sigma ?? null,
};
}
/** 캐시가 없는 줄만 기본값 후보로 채움 */
export function fillCache(setup: ProbSetup, cache: ProbCache | null): ProbCache {
const d = setup.defaults;
const vars: Record<string, ProbInput> = { ...(cache?.vars ?? {}) };
for (const row of probRows(setup)) vars[row.key] ??= defaultInput(d, row);
const run = cache?.run ?? { ...d.run, rho: d.rho_c_phi };
return { vars, run };
}
/** 기본값과 다른 줄 수(「사용자 입력」 표기) */
export function changedCount(setup: ProbSetup, cache: ProbCache): number {
return probRows(setup).filter((row) => {
const a = defaultInput(setup.defaults, row);
const b = cache.vars[row.key];
return (
b && (a.use !== b.use || a.dist !== b.dist || a.value !== b.value || a.trunc !== b.trunc)
);
}).length;
}
/** POST …/stab/prob/{측점} 몸 — 켠 줄만 · 값 0 은 뺌 */
export function probRequest(setup: ProbSetup, cache: ProbCache): Record<string, unknown> {
const variables = probRows(setup)
.map((row) => ({ row, v: cache.vars[row.key] }))
.filter(({ v }) => v?.use && v.value > 0)
.map(({ row, v }) => ({
material: row.material,
prop: row.prop,
dist: v.dist,
...(row.kind === "cov" ? { cov: v.value } : { std: v.value }),
trunc_sigma: v.trunc,
}));
const r = cache.run;
return {
side: setup.side,
condition: setup.condition,
method: r.method,
mode: r.mode,
n: Math.round(r.n),
sampling: r.sampling,
seed: Math.round(r.seed),
rho_c_phi: r.rho,
variables,
};
}
/** 그래프 눈금 — 0 을 억지로 넣지 않음(안전율 축) */
export function axisTicks(min: number, max: number, count = 5): number[] {
const lo = Math.min(min, max);
const hi = Math.max(lo + 1e-9, max);
const raw = (hi - lo) / Math.max(count, 1);
const mag = 10 ** Math.floor(Math.log10(raw));
const step = [1, 2, 5, 10].map((m) => m * mag).find((v) => v >= raw) ?? raw;
const out: number[] = [];
for (let v = Math.floor(lo / step) * step; v <= hi + step * 1e-6; v += step)
out.push(Math.round(v * 1e6) / 1e6);
if (out[out.length - 1] < hi) out.push(out[out.length - 1] + step);
return out;
}