SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,330 @@
|
||||
import type {
|
||||
SimulationResult,
|
||||
ComparisonResult,
|
||||
ComparisonVariable,
|
||||
ToleranceConfig,
|
||||
ToleranceViolation,
|
||||
} from './types';
|
||||
|
||||
export const DEFAULT_TOLERANCES: Record<ComparisonVariable, ToleranceConfig> = {
|
||||
head: { absTol: 1e-4, relTol: 1e-8 },
|
||||
pressure: { absTol: 1e-4, relTol: 1e-8 },
|
||||
demand: { absTol: 1e-3, relTol: 1e-8 },
|
||||
flow: { absTol: 1e-3, relTol: 1e-8 },
|
||||
velocity: { absTol: 1e-5, relTol: 1e-8 },
|
||||
headloss: { absTol: 1e-5, relTol: 1e-8 },
|
||||
quality: { absTol: 1e-3, relTol: 1e-8 },
|
||||
};
|
||||
|
||||
// How many model flow units make up 1 LPS. The default flow/demand absTol
|
||||
// (1e-3) is meant in LPS; applied verbatim to e.g. CMD it was 86x stricter,
|
||||
// so near-zero flows in real CMD networks failed on solver round-off alone.
|
||||
const FLOW_UNITS_PER_LPS: Record<string, number> = {
|
||||
LPS: 1, LPM: 60, CMH: 3.6, CMD: 86.4, MLD: 0.0864, CMS: 0.001,
|
||||
CFS: 0.0353147, GPM: 15.8503, MGD: 0.0228245, IMGD: 0.0190053, AFD: 0.0700456,
|
||||
};
|
||||
|
||||
function flowScaledTolerances(
|
||||
base: Record<ComparisonVariable, ToleranceConfig>,
|
||||
flowUnit: string,
|
||||
maxAbsFlow: number
|
||||
): Record<ComparisonVariable, ToleranceConfig> {
|
||||
const perLps = FLOW_UNITS_PER_LPS[flowUnit.toUpperCase().trim()] ?? 1;
|
||||
// Also never demand more than ~1e-6 of the largest flow in the network.
|
||||
const scaled = (tol: ToleranceConfig): ToleranceConfig => ({
|
||||
...tol,
|
||||
absTol: Math.max(tol.absTol * perLps, 1e-6 * maxAbsFlow),
|
||||
});
|
||||
return { ...base, flow: scaled(base.flow), demand: scaled(base.demand) };
|
||||
}
|
||||
|
||||
function maxAbsSeriesValue(result: SimulationResult, kind: 'flow' | 'demand'): number {
|
||||
let max = 0;
|
||||
const items = kind === 'flow' ? Object.values(result.links) : Object.values(result.nodes);
|
||||
for (const item of items) {
|
||||
const series = (item.timeSeries as Record<string, number[] | undefined> | undefined)?.[kind];
|
||||
const values = series ?? [(item as unknown as Record<string, number>)[kind] ?? 0];
|
||||
for (const v of values) if (Number.isFinite(v) && Math.abs(v) > max) max = Math.abs(v);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function checkPoint(
|
||||
variable: ComparisonVariable,
|
||||
id: string,
|
||||
time: number,
|
||||
valServer: number | undefined | null,
|
||||
valWasm: number | undefined | null,
|
||||
maxDiffs: Record<ComparisonVariable, number>,
|
||||
maxRelativeDiffs: Record<ComparisonVariable, number>,
|
||||
maxSignificantRelativeDiffs: Record<ComparisonVariable, number>,
|
||||
nearZeroCounts: Record<ComparisonVariable, number>,
|
||||
toleranceViolations: ToleranceViolation[],
|
||||
tolerances: Record<ComparisonVariable, ToleranceConfig>,
|
||||
missingIds: string[]
|
||||
) {
|
||||
if (
|
||||
valServer === undefined ||
|
||||
valServer === null ||
|
||||
valWasm === undefined ||
|
||||
valWasm === null
|
||||
) {
|
||||
missingIds.push(`MissingValue:${variable}:${id}@${time}s`);
|
||||
toleranceViolations.push({
|
||||
variable,
|
||||
id,
|
||||
time,
|
||||
diff: Number.POSITIVE_INFINITY,
|
||||
tolerance: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(valServer) || !Number.isFinite(valWasm)) {
|
||||
missingIds.push(`NonFiniteValue:${variable}:${id}@${time}s`);
|
||||
toleranceViolations.push({
|
||||
variable,
|
||||
id,
|
||||
time,
|
||||
diff: Number.NaN,
|
||||
tolerance: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = Math.abs(valServer - valWasm);
|
||||
const scale = Math.max(Math.abs(valServer), Math.abs(valWasm));
|
||||
const relDiff = scale === 0 ? 0 : diff / scale;
|
||||
const tol = tolerances[variable];
|
||||
const allowed = tol.absTol + tol.relTol * scale;
|
||||
|
||||
if (diff > maxDiffs[variable]) {
|
||||
maxDiffs[variable] = diff;
|
||||
}
|
||||
if (relDiff > maxRelativeDiffs[variable]) {
|
||||
maxRelativeDiffs[variable] = relDiff;
|
||||
}
|
||||
|
||||
// Denominator near-zero check: when scale <= absTol, relative error can be
|
||||
// mathematically exaggerated (e.g. 1e-6 diff on a 1e-6 flow gives 100% relative error,
|
||||
// yet is well within hydraulic tolerance).
|
||||
if (scale <= tol.absTol) {
|
||||
nearZeroCounts[variable]++;
|
||||
} else {
|
||||
if (relDiff > maxSignificantRelativeDiffs[variable]) {
|
||||
maxSignificantRelativeDiffs[variable] = relDiff;
|
||||
}
|
||||
}
|
||||
|
||||
if (diff > allowed) {
|
||||
toleranceViolations.push({
|
||||
variable,
|
||||
id,
|
||||
time,
|
||||
diff,
|
||||
tolerance: allowed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Native vs WASM comparison for a simulation result pair.
|
||||
*
|
||||
* Evaluation is strictly based on:
|
||||
* allowedTolerance = absTol + relTol * max(|a|, |b|)
|
||||
*
|
||||
* Relative error is computed across all points, but points where the denominator
|
||||
* is near zero (scale <= absTol) are counted separately so that zero-flow/velocity
|
||||
* mathematical artifacts (e.g. 1.0~1.64 relative diff on 1e-6 flow) do not mislead users.
|
||||
*/
|
||||
export function computeComparison(
|
||||
server: SimulationResult,
|
||||
wasm: SimulationResult,
|
||||
customTolerances?: Partial<Record<ComparisonVariable, ToleranceConfig>>
|
||||
): ComparisonResult {
|
||||
const unitAware = flowScaledTolerances(
|
||||
DEFAULT_TOLERANCES,
|
||||
server.units?.flow || wasm.units?.flow || 'LPS',
|
||||
Math.max(maxAbsSeriesValue(server, 'flow'), maxAbsSeriesValue(server, 'demand'))
|
||||
);
|
||||
const tolerances: Record<ComparisonVariable, ToleranceConfig> = {
|
||||
...unitAware,
|
||||
...customTolerances,
|
||||
};
|
||||
|
||||
// When BOTH engines' quality runs failed there is nothing to compare: report
|
||||
// that once (qualityInfo.status) instead of one MissingValue per node/step.
|
||||
const bothQualityFailed =
|
||||
server.qualityInfo?.status === 'failed' && wasm.qualityInfo?.status === 'failed';
|
||||
const hasQualitySimulation = !bothQualityFailed && Boolean(
|
||||
(server.qualityInfo && server.qualityInfo.qualType > 0) ||
|
||||
(wasm.qualityInfo && wasm.qualityInfo.qualType > 0)
|
||||
);
|
||||
|
||||
const variables: ComparisonVariable[] = hasQualitySimulation
|
||||
? ['demand', 'head', 'pressure', 'flow', 'velocity', 'headloss', 'quality']
|
||||
: ['demand', 'head', 'pressure', 'flow', 'velocity', 'headloss'];
|
||||
const maxDiffs = {} as Record<ComparisonVariable, number>;
|
||||
const maxRelativeDiffs = {} as Record<ComparisonVariable, number>;
|
||||
const maxSignificantRelativeDiffs = {} as Record<ComparisonVariable, number>;
|
||||
const nearZeroCounts = {} as Record<ComparisonVariable, number>;
|
||||
|
||||
for (const v of variables) {
|
||||
maxDiffs[v] = 0;
|
||||
maxRelativeDiffs[v] = 0;
|
||||
maxSignificantRelativeDiffs[v] = 0;
|
||||
nearZeroCounts[v] = 0;
|
||||
}
|
||||
|
||||
const toleranceViolations: ToleranceViolation[] = [];
|
||||
const missingIds: string[] = [];
|
||||
|
||||
// 0. Strict unit metadata match
|
||||
const serverFlow = (server.units?.flow || '').toUpperCase().trim();
|
||||
const wasmFlow = (wasm.units?.flow || '').toUpperCase().trim();
|
||||
if (serverFlow && wasmFlow && serverFlow !== wasmFlow) {
|
||||
missingIds.push(`UnitMismatch:flow(${serverFlow}!=${wasmFlow})`);
|
||||
}
|
||||
|
||||
const serverPress = (server.units?.pressure || '').toUpperCase().trim();
|
||||
const wasmPress = (wasm.units?.pressure || '').toUpperCase().trim();
|
||||
if (serverPress && wasmPress && serverPress !== wasmPress) {
|
||||
missingIds.push(`UnitMismatch:pressure(${serverPress}!=${wasmPress})`);
|
||||
}
|
||||
|
||||
const serverQInfo = server.qualityInfo;
|
||||
const wasmQInfo = wasm.qualityInfo;
|
||||
if (serverQInfo && wasmQInfo) {
|
||||
if (serverQInfo.qualType !== wasmQInfo.qualType) {
|
||||
missingIds.push(`QualityMismatch:qualType(${serverQInfo.qualType}!=${wasmQInfo.qualType})`);
|
||||
}
|
||||
const sqUnits = (serverQInfo.chemUnits || '').trim().toUpperCase();
|
||||
const wqUnits = (wasmQInfo.chemUnits || '').trim().toUpperCase();
|
||||
if (sqUnits && wqUnits && sqUnits !== wqUnits) {
|
||||
missingIds.push(`QualityMismatch:chemUnits(${sqUnits}!=${wqUnits})`);
|
||||
}
|
||||
}
|
||||
|
||||
const serverSteps = server.timeSteps || [0];
|
||||
const wasmSteps = wasm.timeSteps || [0];
|
||||
|
||||
const wasmIndexByTime = new Map<number, number>();
|
||||
wasmSteps.forEach((t, idx) => wasmIndexByTime.set(t, idx));
|
||||
const serverTimeSet = new Set(serverSteps);
|
||||
|
||||
const missingTimestamps = new Set<number>();
|
||||
|
||||
// Compare Nodes
|
||||
for (const id in server.nodes) {
|
||||
if (!wasm.nodes[id]) {
|
||||
missingIds.push(`Node:${id}`);
|
||||
continue;
|
||||
}
|
||||
const s = server.nodes[id];
|
||||
const w = wasm.nodes[id];
|
||||
if (s.timeSeries && w.timeSeries) {
|
||||
for (let si = 0; si < serverSteps.length; si++) {
|
||||
const t = serverSteps[si];
|
||||
const wi = wasmIndexByTime.get(t);
|
||||
if (wi === undefined) {
|
||||
missingTimestamps.add(t);
|
||||
continue;
|
||||
}
|
||||
const sd = si < s.timeSeries.demand.length ? s.timeSeries.demand[si] : undefined;
|
||||
const wd = wi < w.timeSeries.demand.length ? w.timeSeries.demand[wi] : undefined;
|
||||
const sh = si < s.timeSeries.head.length ? s.timeSeries.head[si] : undefined;
|
||||
const wh = wi < w.timeSeries.head.length ? w.timeSeries.head[wi] : undefined;
|
||||
const sp = si < s.timeSeries.pressure.length ? s.timeSeries.pressure[si] : undefined;
|
||||
const wp = wi < w.timeSeries.pressure.length ? w.timeSeries.pressure[wi] : undefined;
|
||||
|
||||
checkPoint('demand', id, t, sd, wd, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('head', id, t, sh, wh, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('pressure', id, t, sp, wp, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
if (hasQualitySimulation) {
|
||||
const sq = s.timeSeries.quality ? (si < s.timeSeries.quality.length ? s.timeSeries.quality[si] : undefined) : s.quality;
|
||||
const wq = w.timeSeries.quality ? (wi < w.timeSeries.quality.length ? w.timeSeries.quality[wi] : undefined) : w.quality;
|
||||
checkPoint('quality', id, t, sq, wq, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkPoint('demand', id, server.timeSeconds, s.demand, w.demand, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('head', id, server.timeSeconds, s.head, w.head, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('pressure', id, server.timeSeconds, s.pressure, w.pressure, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
if (hasQualitySimulation) {
|
||||
checkPoint('quality', id, server.timeSeconds, s.quality, w.quality, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in wasm.nodes) {
|
||||
if (!server.nodes[id] && !missingIds.includes(`Node:${id}`)) missingIds.push(`Node:${id}`);
|
||||
}
|
||||
|
||||
// Compare Links
|
||||
for (const id in server.links) {
|
||||
if (!wasm.links[id]) {
|
||||
missingIds.push(`Link:${id}`);
|
||||
continue;
|
||||
}
|
||||
const s = server.links[id];
|
||||
const w = wasm.links[id];
|
||||
if (s.timeSeries && w.timeSeries) {
|
||||
for (let si = 0; si < serverSteps.length; si++) {
|
||||
const t = serverSteps[si];
|
||||
const wi = wasmIndexByTime.get(t);
|
||||
if (wi === undefined) {
|
||||
missingTimestamps.add(t);
|
||||
continue;
|
||||
}
|
||||
const sf = si < s.timeSeries.flow.length ? s.timeSeries.flow[si] : undefined;
|
||||
const wf = wi < w.timeSeries.flow.length ? w.timeSeries.flow[wi] : undefined;
|
||||
const sv = si < s.timeSeries.velocity.length ? s.timeSeries.velocity[si] : undefined;
|
||||
const wv = wi < w.timeSeries.velocity.length ? w.timeSeries.velocity[wi] : undefined;
|
||||
const shl = si < s.timeSeries.headloss.length ? s.timeSeries.headloss[si] : undefined;
|
||||
const whl = wi < w.timeSeries.headloss.length ? w.timeSeries.headloss[wi] : undefined;
|
||||
|
||||
checkPoint('flow', id, t, sf, wf, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('velocity', id, t, sv, wv, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('headloss', id, t, shl, whl, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
if (hasQualitySimulation) {
|
||||
const slq = s.timeSeries.quality ? (si < s.timeSeries.quality.length ? s.timeSeries.quality[si] : undefined) : s.quality;
|
||||
const wlq = w.timeSeries.quality ? (wi < w.timeSeries.quality.length ? w.timeSeries.quality[wi] : undefined) : w.quality;
|
||||
checkPoint('quality', id, t, slq, wlq, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkPoint('flow', id, server.timeSeconds, s.flow, w.flow, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('velocity', id, server.timeSeconds, s.velocity, w.velocity, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
checkPoint('headloss', id, server.timeSeconds, s.headloss, w.headloss, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
if (hasQualitySimulation) {
|
||||
checkPoint('quality', id, server.timeSeconds, s.quality, w.quality, maxDiffs, maxRelativeDiffs, maxSignificantRelativeDiffs, nearZeroCounts, toleranceViolations, tolerances, missingIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in wasm.links) {
|
||||
if (!server.links[id] && !missingIds.includes(`Link:${id}`)) missingIds.push(`Link:${id}`);
|
||||
}
|
||||
|
||||
// Missing Timestamps check
|
||||
for (const t of wasmSteps) {
|
||||
if (!serverTimeSet.has(t)) missingTimestamps.add(t);
|
||||
}
|
||||
|
||||
for (const t of missingTimestamps) {
|
||||
missingIds.push(`Timestamp:${t}s`);
|
||||
}
|
||||
|
||||
const passed = missingIds.length === 0 && toleranceViolations.length === 0;
|
||||
|
||||
return {
|
||||
passed,
|
||||
maxDiffs,
|
||||
maxRelativeDiffs,
|
||||
maxSignificantRelativeDiffs,
|
||||
nearZeroCounts,
|
||||
toleranceViolations,
|
||||
missingIds,
|
||||
};
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user