Files
Aislo/E02_StabGround/logic/E02_StabGround_Check.ts
T
eomsangdonandClaude Opus 5.5 f4077fbcf6 feat(532-3): E02 지하수 갈래 — 우기 규칙 넷(KDS 기본 · 지표 아래 깊이 비율 · r_u · 사용자 수위선 구간) · 엔진 꼴 바꿈 · 지하수 카드
- ground.json groundwater 칸 덧붙임(wet_mode · depth_ratio · ru · lines · 기존 칸 그대로) · 기본값 = KDS · 범위 칸 넷
- check_ground ↔ checkGround 짝 고침(갈래 · 비율 · r_u · 수위선 구간 · 점) · 거울 시험 판 넷 더함
- groundwater_for · station_groundwater — 측점 모형 → {mode: none | ru | line}(좌 · 우 비탈 높이 따로 · 덮지 않은 측점 = KDS)
- GET · [초기화] = 옛 정본 빠진 칸 기본값으로 채움 · 화면 옛 캐시도 서버 값으로 채움
- 화면 = 지하수 카드 따로(「KDS 기본 · 우기 기준안전율 1.2 와 짝」 안내) · 하중 카드 = 노면 하중 · 지진
- 시험 test_532_4_stab_walls_water(벽 · 지하수) · test_532_3 기본값 지하수 칸 대조 고침(칸 늘어남)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:44:00 +09:00

231 lines
8.9 KiB
TypeScript

/* =============================================================================
* E02_StabGround_Check.ts
* E02 지반 조건 꼴 · 입력 검사 · 측점 적용(요구 347-2 · 3 · 4 · PLAN 532-3) — 순수 함수만.
*
* ⚠ 파이썬·TS 짝 — `checkGround` ↔ `E02_StabGround_Model.py` `check_ground` ·
* `resolveStations` ↔ `resolve_stations`. 한쪽을 고치면 다른 쪽도 · 거울 시험
* `resources/tester/test_532_3_e02_ground.py`.
* ========================================================================== */
export type WaterGroup = "soil" | "rock";
export type WaterRule = "none" | "surface" | "half_height";
/** 우기 지하수 갈래 — kds = wet_soil · wet_rock 규칙(기본) · depth_ratio · ru · line(파이썬 `WET_MODES`) */
export type WetMode = "kds" | "depth_ratio" | "ru" | "line";
/** 사용자 수위선 한 구간 — 점 = [offset m(좌 +), 공사 뒤 지표 아래 깊이 m] · offset 오름차순 */
export interface WaterLine {
from_m: number;
to_m: number;
points: [number, number][];
}
export interface Groundwater {
dry: WaterRule;
wet_soil: WaterRule;
wet_rock: WaterRule;
wet_mode: WetMode;
depth_ratio: number;
ru: number;
lines: WaterLine[];
}
/** 물성 한 줄 = E03_StabSection_Types.Material + 지하수 무리 */
export interface Material {
id: string;
name: string;
gamma_t: number;
gamma_sat: number;
c: number;
phi: number;
water: WaterGroup;
}
export interface Station {
chainage_m: number;
ground_type: string;
soil_thickness_m: number | null;
}
export interface Zone {
from_m: number;
to_m: number;
soil: string | null;
rock: string | null;
soil_thickness_m: number | null;
}
export type SlopeFactors = Record<(typeof SLOPES)[number], number>;
export interface Ground {
version: number;
materials: Material[];
label_map: Record<(typeof LABELS)[number], string>;
stations: Station[];
zones: Zone[];
safety_factors: Record<(typeof CASES)[number], SlopeFactors>;
road_surcharge_kpa: number;
groundwater: Groundwater;
seismic: { enabled: boolean; kh: number };
}
export type Limits = Record<string, [number, number]>;
export interface GroundDefaults extends Ground {
note: string;
limits: Limits;
sources: Record<string, string>;
}
export interface CheckError {
path: string;
code: string;
}
export interface ResolvedStation {
chainage_m: number;
ground_type: string;
upper: string | null;
lower: string | null;
soil_thickness_m: number | null;
zoned: boolean;
}
export const MATERIAL_FIELDS = ["gamma_t", "gamma_sat", "c", "phi"] as const;
export const WATER_GROUPS: readonly WaterGroup[] = ["soil", "rock"];
export const LABELS = ["soil", "ripping_rock", "blasting_rock", "fill"] as const;
const ROCK_LABELS = ["ripping_rock", "blasting_rock"];
export const CASES = ["dry", "wet", "seismic", "short_term"] as const;
export const SLOPES = ["cut", "fill", "soft"] as const;
export const WATER_KEYS = ["dry", "wet_soil", "wet_rock"] as const;
export const WATER_RULES: readonly WaterRule[] = ["none", "surface", "half_height"];
export const WET_MODES: readonly WetMode[] = ["kds", "depth_ratio", "ru", "line"];
type Loose = Record<string, unknown>;
const isNum = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
const asList = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
const isDict = (v: unknown): v is Loose => typeof v === "object" && v !== null && !Array.isArray(v);
const asDict = (v: unknown): Loose => (isDict(v) ? v : {});
/** 틀린 칸 목록 — 빈 목록 = 통과(code 는 파이썬과 같은 낱말) */
export function checkGround(ground: unknown, limits: Limits): CheckError[] {
const errors: CheckError[] = [];
const bad = (path: string, code: string): void => void errors.push({ path, code });
const ranged = (path: string, value: unknown, key: string): void => {
const [low, high] = limits[key];
if (!isNum(value)) bad(path, "number");
else if (!(low <= value && value <= high)) bad(path, "range");
};
const doc = asDict(ground);
const materials = asList(doc.materials);
if (!materials.length) bad("materials", "empty");
const ids: string[] = [];
const names: string[] = [];
materials.forEach((item, i) => {
const base = `materials.${i}`;
if (!isDict(item)) return bad(base, "type");
const mid = item.id;
if (typeof mid !== "string" || !mid || ids.includes(mid)) bad(`${base}.id`, "id_dup");
ids.push(typeof mid === "string" ? mid : "");
const name = typeof item.name === "string" ? item.name.trim() : "";
const [low, high] = limits.name_length;
if (!(low <= name.length && name.length <= high)) bad(`${base}.name`, "name");
else if (names.includes(name)) bad(`${base}.name`, "name_dup");
names.push(name);
for (const field of MATERIAL_FIELDS) ranged(`${base}.${field}`, item[field], field);
if (isNum(item.gamma_t) && isNum(item.gamma_sat) && item.gamma_sat < item.gamma_t)
bad(`${base}.gamma_sat`, "sat_lt_t");
if (!WATER_GROUPS.includes(item.water as WaterGroup)) bad(`${base}.water`, "enum");
});
const labelMap = asDict(doc.label_map);
for (const label of LABELS) {
const ref = labelMap[label];
if (!ref || !ids.includes(ref as string)) bad(`label_map.${label}`, "ref");
}
asList(doc.zones).forEach((zone, i) => {
const base = `zones.${i}`;
if (!isDict(zone)) return bad(base, "type");
ranged(`${base}.from_m`, zone.from_m, "chainage_m");
ranged(`${base}.to_m`, zone.to_m, "chainage_m");
if (isNum(zone.from_m) && isNum(zone.to_m) && zone.from_m >= zone.to_m)
bad(`${base}.to_m`, "order");
for (const layer of ["soil", "rock"]) {
const ref = zone[layer];
if (ref !== null && ref !== undefined && !ids.includes(ref as string))
bad(`${base}.${layer}`, "ref");
}
if (zone.soil_thickness_m !== null && zone.soil_thickness_m !== undefined)
ranged(`${base}.soil_thickness_m`, zone.soil_thickness_m, "soil_thickness_m");
});
const factors = asDict(doc.safety_factors);
for (const c of CASES)
for (const s of SLOPES)
ranged(`safety_factors.${c}.${s}`, asDict(factors[c])[s], "safety_factor");
ranged("road_surcharge_kpa", doc.road_surcharge_kpa, "road_surcharge_kpa");
const water = asDict(doc.groundwater);
for (const key of WATER_KEYS)
if (!WATER_RULES.includes(water[key] as WaterRule)) bad(`groundwater.${key}`, "enum");
if (!WET_MODES.includes(water.wet_mode as WetMode)) bad("groundwater.wet_mode", "enum");
ranged("groundwater.depth_ratio", water.depth_ratio, "depth_ratio");
ranged("groundwater.ru", water.ru, "ru");
if (!Array.isArray(water.lines)) bad("groundwater.lines", "type");
asList(water.lines).forEach((line, i) => {
const base = `groundwater.lines.${i}`;
if (!isDict(line)) return bad(base, "type");
ranged(`${base}.from_m`, line.from_m, "chainage_m");
ranged(`${base}.to_m`, line.to_m, "chainage_m");
if (isNum(line.from_m) && isNum(line.to_m) && line.from_m >= line.to_m)
bad(`${base}.to_m`, "order");
const points = asList(line.points);
if (points.length < 2) bad(`${base}.points`, "few_points");
let last: number | null = null;
points.forEach((point, k) => {
if (!Array.isArray(point) || point.length !== 2) return bad(`${base}.points.${k}`, "type");
ranged(`${base}.points.${k}.0`, point[0], "offset_m");
ranged(`${base}.points.${k}.1`, point[1], "water_depth_m");
if (isNum(point[0])) {
if (last !== null && point[0] <= last) bad(`${base}.points.${k}.0`, "order");
last = point[0];
}
});
});
const seismic = asDict(doc.seismic);
if (typeof seismic.enabled !== "boolean") bad("seismic.enabled", "enum");
ranged("seismic.kh", seismic.kh, "kh");
return errors;
}
/** 측점마다 적용 지층 — 위 = 토사 연결 · 아래 = 암 측점의 암 연결 · 두께 = B06 · 구간이 덮어씀
* (뒤 구간이 이김 · 양 끝 포함 · 빈 칸은 안 바꿈) · 두께가 없으면 아래 지층 없음 */
export function resolveStations(ground: Ground): ResolvedStation[] {
return ground.stations.map((station) => {
const chainage = station.chainage_m;
let upper: string | null = ground.label_map.soil ?? null;
let lower: string | null = ROCK_LABELS.includes(station.ground_type)
? (ground.label_map[station.ground_type as (typeof LABELS)[number]] ?? null)
: null;
let thickness = station.soil_thickness_m;
let zoned = false;
for (const zone of ground.zones) {
if (!(zone.from_m <= chainage && chainage <= zone.to_m)) continue;
zoned = true;
if (zone.soil != null) upper = zone.soil;
if (zone.rock != null) lower = zone.rock;
if (zone.soil_thickness_m != null) thickness = zone.soil_thickness_m;
}
if (thickness === null) lower = null; // 경계 없음 = 전부 위 지층
return {
chainage_m: chainage,
ground_type: station.ground_type,
upper,
lower,
soil_thickness_m: lower !== null ? thickness : null,
zoned,
};
});
}