/* ============================================================================= * 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; rho_c_phi: number; sources: Record; materials?: Record>>>; } 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; result: ProbResult; computed_at: string; } export interface ProbSetup { station_key: string; side: string; condition: string; role: "cut" | "fill"; required: number | null; fs_deterministic: Record; 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; 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 = { ...(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 { 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; }