- 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>
231 lines
8.9 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|