完善AMESim组件界面与仿真求解稳定性

This commit is contained in:
ljz committed 2026-08-02 00:57:48 +08:00
1 parent e7177ab03e
commit 410ef535e8
34 files changed
+3340 -251

No files matched your search

+316 -47
View File
@@ -89,6 +89,10 @@ import {
type ParameterColumnLayout,
type ParameterTableRow,
} from "./ParameterTable";
import {
evaluateParameterExpression,
isParameterExpressionValue,
} from "./parameterExpression";
import {
WorkspaceViewTabs,
type WorkspaceView,
@@ -219,7 +223,17 @@ type SimulationNodeData = {
type SimulationNode = Node<SimulationNodeData, "simulationComponent">;
type SimulationEdge = Edge;
type SimulationNumericKey = "t_start" | "t_stop" | "step" | "max_step";
type SimulationConfig = {
t_start: ParameterValue;
t_stop: ParameterValue;
step: ParameterValue;
max_step: ParameterValue;
method: string;
};
type ResolvedSimulationConfig = {
t_start: number;
t_stop: number;
step: number;
@@ -255,6 +269,10 @@ type ReactFlowProjectPayload = {
simulation: SimulationConfig;
};
type ExecutableProjectPayload = Omit<ReactFlowProjectPayload, "simulation"> & {
simulation: ResolvedSimulationConfig;
};
type SimulationResult = {
success: boolean;
status: "completed" | "stopped" | "stalled" | "failed";
@@ -534,7 +552,7 @@ const unitOptions: Record<UnitQuantity, UnitOption[]> = {
],
};
const defaultSimulationConfig: SimulationConfig = {
const defaultSimulationConfig: ResolvedSimulationConfig = {
t_start: 0,
t_stop: 2,
step: 0.1,
@@ -625,6 +643,12 @@ type SimulationStreamEvent =
totalTime?: number;
};
const SUPPORTED_SOLVER_METHODS = ["BDF", "Radau", "LSODA", "RK45", "RK23", "DOP853"];
const simulationConfigLabels: Record<SimulationNumericKey, string> = {
t_start: "起始时间",
t_stop: "结束时间",
step: "采样步长",
max_step: "最大积分步长",
};
function physicalPort(
name: string,
@@ -1527,7 +1551,7 @@ type NormalizedSimulationProgress = {
function normalizeSimulationProgressEvent(
event: Extract<SimulationStreamEvent, { event: "progress" }>,
config: SimulationConfig,
config: ResolvedSimulationConfig,
): NormalizedSimulationProgress {
const totalTime = Number.isFinite(event.totalTime)
? Number(event.totalTime)
@@ -2184,7 +2208,7 @@ function DeferredNumberInput({
<input
aria-invalid={invalid}
aria-label={ariaLabel}
inputMode="decimal"
inputMode="text"
onBlur={() => {
editingRef.current = false;
if (cancelCommitRef.current) {
@@ -3013,10 +3037,11 @@ function FlowWorkbench() {
const selectedUnit =
selectedNode?.data.parameterUnits[key] ?? definition.unit ?? "";
const unit = findUnitOption(definition, selectedUnit);
const numericValue = Number(displayValue);
const normalizedDisplayValue = displayValue.trim();
const numericValue = Number(normalizedDisplayValue);
const baseValue =
displayValue.trim() === "" || !Number.isFinite(numericValue)
? displayValue
normalizedDisplayValue === "" || !Number.isFinite(numericValue)
? normalizedDisplayValue
: unit.toBase(numericValue);
updateSelectedNode((node) => ({
@@ -3216,15 +3241,22 @@ function FlowWorkbench() {
]);
const updateSimulationConfig = (key: keyof SimulationConfig, value: string) => {
const normalizedValue = value.trim();
const numericValue = Number(normalizedValue);
setSimulationConfig((current) => ({
...current,
[key]: key === "method" ? value : Number(value),
[key]:
key === "method"
? value
: normalizedValue !== "" && Number.isFinite(numericValue)
? numericValue
: normalizedValue,
}));
};
const generateXml = () => {
try {
const xml = buildSystemXml(buildCurrentProject());
const xml = buildSystemXml(buildCurrentProject(), componentDefinitions);
changeConsoleMode("normal");
appendConsoleEntry(
"success",
@@ -3239,7 +3271,7 @@ function FlowWorkbench() {
const downloadXml = () => {
try {
const xml = buildSystemXml(buildCurrentProject());
const xml = buildSystemXml(buildCurrentProject(), componentDefinitions);
downloadText(`${safeFilename(projectName)}.xml`, xml);
appendConsoleEntry("success", `XML 已下载:${safeFilename(projectName)}.xml`);
} catch (error) {
@@ -3402,7 +3434,7 @@ function FlowWorkbench() {
const publishSimulationResult = (
result: SimulationResult,
project: ReactFlowProjectPayload,
project: ExecutableProjectPayload,
) => {
const resultStatus = result.status ?? (result.success ? "completed" : "failed");
const sampleCount = Math.min(
@@ -3510,34 +3542,48 @@ function FlowWorkbench() {
const runSimulation = async () => {
const issues = checkModel();
const modelErrors = issues.filter((issue) => issue.severity === "error");
const simulationResolution = resolveSimulationConfig(
simulationConfigRef.current,
);
const progressConfig = simulationResolution.ok
? simulationResolution.value
: defaultSimulationConfig;
if (modelErrors.length > 0) {
changeConsoleMode("normal");
setSimulationProgress({
state: "error",
percent: 0,
message: "模型检查未通过",
startTime: simulationConfigRef.current.t_start,
simulatedTime: simulationConfigRef.current.t_start,
totalTime: simulationConfigRef.current.t_stop,
startTime: progressConfig.t_start,
simulatedTime: progressConfig.t_start,
totalTime: progressConfig.t_stop,
});
return;
}
if (!simulationResolution.ok) {
changeConsoleMode("normal");
appendConsoleEntry("error", simulationResolution.message);
return;
}
const project: ExecutableProjectPayload = {
...buildCurrentProject(),
simulation: simulationResolution.value,
};
changeConsoleMode("normal");
setSimulationProgress({
state: "running",
percent: 0,
message: "正在提交仿真任务",
startTime: simulationConfigRef.current.t_start,
simulatedTime: simulationConfigRef.current.t_start,
totalTime: simulationConfigRef.current.t_stop,
startTime: project.simulation.t_start,
simulatedTime: project.simulation.t_start,
totalTime: project.simulation.t_stop,
});
appendConsoleEntry(
"info",
`开始仿真:${projectNameRef.current || "untitled"},时间范围 ${simulationConfigRef.current.t_start} - ${simulationConfigRef.current.t_stop} s`,
`开始仿真:${projectNameRef.current || "untitled"},时间范围 ${project.simulation.t_start} - ${project.simulation.t_stop} s`,
);
await runBusyAction("正在编译模型并运行仿真...", async () => {
const project = buildCurrentProject();
const xml = buildSystemXml(project);
const xml = buildSystemXml(project, componentDefinitions);
const simulationId = createSimulationId();
activeSimulationIdRef.current = simulationId;
setSimulationStopPending(false);
@@ -3988,7 +4034,11 @@ function FlowWorkbench() {
const error =
gasReference && !gasReferenceAvailable
? "引用的介质定义不存在"
: parameterValidationMessage(definition, baseValue);
: parameterValidationMessage(
definition,
baseValue,
selectedUnit,
);
let valueControl: ReactNode;
if (mediumDefinitionIndex) {
valueControl = (
@@ -4103,12 +4153,20 @@ function FlowWorkbench() {
definition,
selectedUnit,
),
calculatedValue: parameterCalculatedValueMetadata(
baseValue,
definition,
selectedUnit,
),
};
},
),
]
: [];
const simulationSettingErrors = simulationConfigValidationMessages(
simulationConfig,
);
const simulationSettingRows: ParameterTableRow[] = [
{
id: "simulation-t-start",
@@ -4116,6 +4174,7 @@ function FlowWorkbench() {
value: (
<DeferredNumberInput
ariaLabel="起始时间"
invalid={Boolean(simulationSettingErrors.t_start)}
value={simulationConfig.t_start}
onCommit={(value) => updateSimulationConfig("t_start", value)}
onEditEnd={finishHistoryEdit}
@@ -4127,6 +4186,11 @@ function FlowWorkbench() {
defaultValue: `${defaultSimulationConfig.t_start} s`,
range: "有限数值",
currentUnit: "s",
error: simulationSettingErrors.t_start,
calculatedValue: simulationCalculatedValueMetadata(
simulationConfig.t_start,
"s",
),
},
{
id: "simulation-t-stop",
@@ -4134,6 +4198,7 @@ function FlowWorkbench() {
value: (
<DeferredNumberInput
ariaLabel="结束时间"
invalid={Boolean(simulationSettingErrors.t_stop)}
value={simulationConfig.t_stop}
onCommit={(value) => updateSimulationConfig("t_stop", value)}
onEditEnd={finishHistoryEdit}
@@ -4145,6 +4210,11 @@ function FlowWorkbench() {
defaultValue: `${defaultSimulationConfig.t_stop} s`,
range: "> 起始时间",
currentUnit: "s",
error: simulationSettingErrors.t_stop,
calculatedValue: simulationCalculatedValueMetadata(
simulationConfig.t_stop,
"s",
),
},
{
id: "simulation-step",
@@ -4152,6 +4222,7 @@ function FlowWorkbench() {
value: (
<DeferredNumberInput
ariaLabel="采样步长"
invalid={Boolean(simulationSettingErrors.step)}
value={simulationConfig.step}
onCommit={(value) => updateSimulationConfig("step", value)}
onEditEnd={finishHistoryEdit}
@@ -4163,6 +4234,11 @@ function FlowWorkbench() {
defaultValue: `${defaultSimulationConfig.step} s`,
range: "> 0",
currentUnit: "s",
error: simulationSettingErrors.step,
calculatedValue: simulationCalculatedValueMetadata(
simulationConfig.step,
"s",
),
},
{
id: "simulation-max-step",
@@ -4170,6 +4246,7 @@ function FlowWorkbench() {
value: (
<DeferredNumberInput
ariaLabel="最大积分步长"
invalid={Boolean(simulationSettingErrors.max_step)}
value={simulationConfig.max_step}
onCommit={(value) => updateSimulationConfig("max_step", value)}
onEditEnd={finishHistoryEdit}
@@ -4181,6 +4258,11 @@ function FlowWorkbench() {
defaultValue: `${defaultSimulationConfig.max_step} s`,
range: "> 0",
currentUnit: "s",
error: simulationSettingErrors.max_step,
calculatedValue: simulationCalculatedValueMetadata(
simulationConfig.max_step,
"s",
),
},
{
id: "simulation-method",
@@ -4838,7 +4920,9 @@ function normalizeLoadedNode(
label: node.data.label || definition?.label || node.id,
componentType: node.data.componentType || definition?.type || "component",
modelType: node.data.modelType || definition?.modelType || node.data.componentType,
symbol: node.data.symbol || definition?.symbol || node.data.componentType,
// Catalog symbols are versioned presentation metadata. Prefer the current
// definition so projects saved with an old generic icon are upgraded on load.
symbol: definition?.symbol || node.data.symbol || node.data.componentType,
ports: normalizeLoadedPorts(node.data.ports, definition),
parameters,
parameterUnits,
@@ -5122,17 +5206,95 @@ function parameterCurrentUnitMetadata(
: undefined;
}
type NumericInputResolution =
| { ok: true; value: number; expression: boolean }
| { ok: false; message: string; expression: boolean };
function resolveNumericInput(value: ParameterValue): NumericInputResolution {
if (typeof value === "number") {
return Number.isFinite(value)
? { ok: true, value, expression: false }
: { ok: false, message: "请输入有限数值", expression: false };
}
const normalizedValue = value.trim();
if (!normalizedValue) {
return { ok: false, message: "参数不能为空", expression: false };
}
const numericValue = Number(normalizedValue);
if (Number.isFinite(numericValue)) {
return { ok: true, value: numericValue, expression: false };
}
const evaluated = evaluateParameterExpression(normalizedValue);
return evaluated.ok
? { ok: true, value: evaluated.value, expression: true }
: {
ok: false,
message: `等式无法计算:${evaluated.message}`,
expression: true,
};
}
function resolveParameterValue(
value: ParameterValue,
definition: ParameterDefinition,
selectedUnit: string,
): NumericInputResolution {
const resolved = resolveNumericInput(value);
if (!resolved.ok) {
return resolved;
}
if (resolved.expression && definition.editor) {
return {
ok: false,
message: "该离散参数只能通过下拉栏选择",
expression: true,
};
}
if (!resolved.expression) {
return resolved;
}
const baseValue = findUnitOption(definition, selectedUnit).toBase(
resolved.value,
);
return Number.isFinite(baseValue)
? { ...resolved, value: baseValue }
: {
ok: false,
message: "等式换算到基础单位后不是有限数值",
expression: true,
};
}
function parameterCalculatedValueMetadata(
value: ParameterValue,
definition: ParameterDefinition,
selectedUnit: string,
) {
if (!isParameterExpressionValue(value)) {
return undefined;
}
const resolved = resolveParameterValue(value, definition, selectedUnit);
if (!resolved.ok || !resolved.expression) {
return undefined;
}
const unit = findUnitOption(definition, selectedUnit);
const displayValue = unit.fromBase(resolved.value);
return `${formatEditableNumber(displayValue)}${unit.label ? ` ${unit.label}` : ""}`;
}
function parameterValidationMessage(
definition: ParameterDefinition,
value: ParameterValue,
selectedUnit: string,
) {
if (typeof value === "string" && value.trim() === "") {
return "参数不能为空";
}
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
return "请输入有效数字";
const resolved = resolveParameterValue(value, definition, selectedUnit);
if (!resolved.ok) {
return resolved.message;
}
const numericValue = resolved.value;
if (
definition.options &&
!definition.options.some((option) => option.value === numericValue)
@@ -5153,6 +5315,85 @@ function parameterValidationMessage(
return "";
}
function simulationCalculatedValueMetadata(
value: ParameterValue,
unit: string,
) {
if (!isParameterExpressionValue(value)) {
return undefined;
}
const resolved = resolveNumericInput(value);
if (!resolved.ok || !resolved.expression) {
return undefined;
}
return `${formatEditableNumber(resolved.value)}${unit ? ` ${unit}` : ""}`;
}
function simulationConfigValidationMessages(
simulation: SimulationConfig,
): Partial<Record<SimulationNumericKey, string>> {
const resolutions = {
t_start: resolveNumericInput(simulation.t_start),
t_stop: resolveNumericInput(simulation.t_stop),
step: resolveNumericInput(simulation.step),
max_step: resolveNumericInput(simulation.max_step),
} satisfies Record<SimulationNumericKey, NumericInputResolution>;
const messages: Partial<Record<SimulationNumericKey, string>> = {};
(Object.keys(resolutions) as SimulationNumericKey[]).forEach((key) => {
const resolved = resolutions[key];
if (!resolved.ok) {
messages[key] = resolved.message;
}
});
if (
resolutions.t_start.ok &&
resolutions.t_stop.ok &&
resolutions.t_stop.value <= resolutions.t_start.value
) {
messages.t_stop = "必须大于起始时间";
}
if (resolutions.step.ok && resolutions.step.value <= 0) {
messages.step = "必须大于 0";
}
if (resolutions.max_step.ok && resolutions.max_step.value <= 0) {
messages.max_step = "必须大于 0";
}
return messages;
}
type SimulationConfigResolution =
| { ok: true; value: ResolvedSimulationConfig }
| { ok: false; message: string };
function resolveSimulationConfig(
simulation: SimulationConfig,
): SimulationConfigResolution {
const messages = simulationConfigValidationMessages(simulation);
const firstInvalidKey = (Object.keys(messages) as SimulationNumericKey[])[0];
if (firstInvalidKey) {
return {
ok: false,
message: `仿真设置“${simulationConfigLabels[firstInvalidKey]}”:${messages[firstInvalidKey]}`,
};
}
return {
ok: true,
value: {
t_start: (resolveNumericInput(simulation.t_start) as { ok: true; value: number })
.value,
t_stop: (resolveNumericInput(simulation.t_stop) as { ok: true; value: number })
.value,
step: (resolveNumericInput(simulation.step) as { ok: true; value: number })
.value,
max_step: (
resolveNumericInput(simulation.max_step) as { ok: true; value: number }
).value,
method: simulation.method,
},
};
}
function findDuplicateNames(nodes: SimulationNode[]) {
const counts = new Map<string, number>();
nodes.forEach((node) => {
@@ -5236,9 +5477,16 @@ function validateModel(
return;
}
Object.entries(definition.parameters).forEach(([key, parameter]) => {
const message = parameterValidationMessage(parameter, node.data.parameters[key] ?? "");
const selectedUnit =
node.data.parameterUnits[key] ?? parameter.unit ?? "";
const message = parameterValidationMessage(
parameter,
node.data.parameters[key] ?? "",
selectedUnit,
);
if (message) {
addIssue("error", `${node.data.label}:${parameter.label}${message}`, {
const messageSeparator = message.startsWith("等式") ? ":" : "";
addIssue("error", `${node.data.label}:${parameter.label}${messageSeparator}${message}`, {
nodeId: node.id,
});
}
@@ -5416,17 +5664,13 @@ function validateModel(
});
});
if (!Number.isFinite(simulation.t_start) || !Number.isFinite(simulation.t_stop)) {
addIssue("error", "仿真起止时间必须是有效数字");
} else if (simulation.t_stop <= simulation.t_start) {
addIssue("error", "仿真结束时间必须大于起始时间");
}
if (!Number.isFinite(simulation.step) || simulation.step <= 0) {
addIssue("error", "采样步长必须大于 0");
}
if (!Number.isFinite(simulation.max_step) || simulation.max_step <= 0) {
addIssue("error", "最大积分步长必须大于 0");
}
const simulationErrors = simulationConfigValidationMessages(simulation);
(Object.keys(simulationErrors) as SimulationNumericKey[]).forEach((key) => {
const message = simulationErrors[key];
if (message) {
addIssue("error", `仿真设置:${simulationConfigLabels[key]}:${message}`);
}
});
if (!SUPPORTED_SOLVER_METHODS.includes(simulation.method)) {
addIssue("error", "求解器不受支持");
}
@@ -5548,7 +5792,15 @@ function formatTime(date: Date) {
return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}
function buildSystemXml(project: ReactFlowProjectPayload) {
function buildSystemXml(
project: ReactFlowProjectPayload,
componentDefinitions: ComponentDefinition[],
) {
const simulationResolution = resolveSimulationConfig(project.simulation);
if (!simulationResolution.ok) {
throw new Error(simulationResolution.message);
}
const resolvedSimulation = simulationResolution.value;
const xmlDocument = document.implementation.createDocument("", "System", null);
const system = xmlDocument.documentElement;
system.setAttribute("name", project.name);
@@ -5557,16 +5809,22 @@ function buildSystemXml(project: ReactFlowProjectPayload) {
system.setAttribute("unitSystem", "SI");
const simulation = xmlDocument.createElement("Simulation");
simulation.setAttribute("tStart", String(project.simulation.t_start));
simulation.setAttribute("tStop", String(project.simulation.t_stop));
simulation.setAttribute("step", String(project.simulation.step));
simulation.setAttribute("maxStep", String(project.simulation.max_step));
simulation.setAttribute("method", project.simulation.method);
simulation.setAttribute("tStart", String(resolvedSimulation.t_start));
simulation.setAttribute("tStop", String(resolvedSimulation.t_stop));
simulation.setAttribute("step", String(resolvedSimulation.step));
simulation.setAttribute("maxStep", String(resolvedSimulation.max_step));
simulation.setAttribute("method", resolvedSimulation.method);
const components = xmlDocument.createElement("Components");
const connections = xmlDocument.createElement("Connections");
system.append(simulation, components, connections);
project.nodes.forEach((node) => {
const definition = componentDefinitions.find(
(candidate) => candidate.type === node.data.componentType,
);
if (!definition) {
throw new Error(`组件 ${node.data.label || node.id} 的类型不受支持`);
}
const component = xmlDocument.createElement("Component");
component.setAttribute("id", node.id);
component.setAttribute("name", node.data.label);
@@ -5590,9 +5848,20 @@ function buildSystemXml(project: ReactFlowProjectPayload) {
component.appendChild(port);
});
Object.entries(node.data.parameters).forEach(([name, value]) => {
const parameterDefinition = definition.parameters[name];
const selectedUnit =
node.data.parameterUnits?.[name] ?? parameterDefinition?.unit ?? "";
const resolved = parameterDefinition
? resolveParameterValue(value, parameterDefinition, selectedUnit)
: resolveNumericInput(value);
if (!resolved.ok) {
throw new Error(
`组件“${node.data.label || node.id}”的参数“${parameterDefinition?.label ?? name}”:${resolved.message}`,
);
}
const parameter = xmlDocument.createElement("Parameter");
parameter.setAttribute("name", name);
parameter.setAttribute("value", String(value));
parameter.setAttribute("value", formatEditableNumber(resolved.value));
component.appendChild(parameter);
});
components.appendChild(component);