Files
SystemSimulationApp/frontend/src/projectCompatibility.ts
T

93 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import unitTable from "../../schemas/parameter-units.json" with { type: "json" };
export const DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
export const sharedUnitTable = unitTable as unknown as Record<string, Record<string, [number, number, string]>>;
type Parameter = { unit?: string; quantity?: string };
type Definition = { type: string; modelType: string; modelVersion: string; parameters: Record<string, Parameter> };
type Node = { id: string; data: { label: string; componentType: string; modelType: string; modelVersion?: string;
parameters: Record<string, number | string>; parameterUnits?: Record<string, string> } };
type Project = { projectSchemaVersion: 1 | 2; nodes: Node[] };
const indices = new WeakMap<readonly Definition[], Map<string, Definition>>();
export function componentIndex<D extends Definition>(definitions: readonly D[]): Map<string, D> {
let index = indices.get(definitions);
if (!index) {
index = new Map(definitions.map(definition => [definition.type, definition]));
indices.set(definitions, index);
}
return index as Map<string, D>;
}
export function componentVersionNotices(nodes: readonly Node[], definitions: readonly Definition[]) {
const index = componentIndex(definitions);
return nodes.flatMap(node => {
const definition = index.get(node.data.componentType);
if (!definition || definition.modelType !== node.data.modelType ||
definition.modelVersion === node.data.modelVersion) return [];
return [{ componentId: node.id, label: node.data.label || node.id,
storedVersion: node.data.modelVersion, currentVersion: definition.modelVersion }];
});
}
export function componentVersionWarning(nodes: readonly Node[], definitions: readonly Definition[], running = false) {
const notices = componentVersionNotices(nodes, definitions);
if (!notices.length) return null;
return `${running ? "正在使用旧版或版本未知的工程组件,将按当前模型执行,可能出现仿真失败或仿真结果与实际不符" : "工程包含旧版或版本未知的组件"}:` +
notices.map(n => `${n.label}(${n.storedVersion || "版本未知"} → ${n.currentVersion})`).join(";");
}
export function parameterUnitConversion(definition: Parameter, unit: string) {
const conversion = definition.unit && definition.quantity ? sharedUnitTable[definition.quantity]?.[unit] : undefined;
if (conversion) return { scale: conversion[0], offset: conversion[1] };
if (unit === (definition.unit ?? "")) return { scale: 1, offset: 0 };
throw new Error(`不支持的参数单位“${unit}”(${definition.quantity ?? definition.unit})`);
}
/** Editor numbers stay SI. v2 wire numbers and expressions both use selected units. */
export function decodeProjectParameters<P extends Project>(project: P, definitions: readonly Definition[]): P {
if (project.projectSchemaVersion === 1) return project;
const index = componentIndex(definitions);
return { ...project, projectSchemaVersion: 1, nodes: project.nodes.map(node => {
const definition = index.get(node.data.componentType);
if (!definition) throw new Error(`无法换算未知组件 ${node.data.label} 的单位`);
const parameters = Object.fromEntries(Object.entries(node.data.parameters).map(([name, value]) => {
const parameter = definition.parameters[name];
if (!parameter) throw new Error(`组件 ${node.data.label} 包含未知参数 ${name}`);
const { scale, offset } = parameterUnitConversion(parameter, node.data.parameterUnits?.[name] ?? parameter.unit ?? "");
const isNumber = typeof value === "number" || DECIMAL_LITERAL.test(value.trim());
const si = isNumber ? Number(value) * scale + offset : value;
if (typeof si === "number" && !Number.isFinite(si)) throw new Error(`${node.id}.${name} 必须为有限数值`);
return [name, si];
}));
return { ...node, data: { ...node.data, parameters } };
}) } as P;
}
/** Call only for editor SI data, never on an already-encoded external project. */
export function encodeProjectParameters<P extends Project>(project: P, definitions: readonly Definition[]): P {
if (project.projectSchemaVersion !== 1) throw new Error("工程已按显示单位编码,不能重复换算");
const index = componentIndex(definitions);
return { ...project, projectSchemaVersion: 2, nodes: project.nodes.map(node => {
const definition = index.get(node.data.componentType);
if (!definition) throw new Error(`无法导出未知组件 ${node.data.label} 的新版单位格式`);
const parameterUnits = { ...node.data.parameterUnits };
const parameters = Object.fromEntries(Object.entries(node.data.parameters).map(([name, value]) => {
const parameter = definition.parameters[name];
if (!parameter) throw new Error(`组件 ${node.data.label} 包含未知参数 ${name}`);
const { scale, offset } = parameterUnitConversion(parameter, node.data.parameterUnits?.[name] ?? parameter.unit ?? "");
const isNumber = typeof value === "number" || DECIMAL_LITERAL.test(value.trim());
let display = isNumber ? (Number(value) - offset) / scale : value;
// Some SI doubles have no exact representation after a unit round trip.
// Preserve the numerical input by writing that parameter in its SI unit.
// Never round the physical value or add a hidden second source of truth.
if (typeof display === "number" && display * scale + offset !== Number(value)) {
display = Number(value);
parameterUnits[name] = parameter.unit ?? "";
}
if (typeof display === "number" && !Number.isFinite(display)) throw new Error(`${node.id}.${name} 必须为有限数值`);
return [name, display];
}));
return { ...node, data: { ...node.data, parameters, parameterUnits } };
}) } as P;
}