修复机械事件力曲线并对齐气动孔口端口显示
This commit is contained in:
1 parent
e18399c022
commit
408b4ecb22
11 files changed
+697
-43
No files matched your search
@@ -142,6 +142,7 @@ import {
|
||||
isParameterExpressionValue,
|
||||
isScientificNotationValue,
|
||||
} from "./parameterExpression";
|
||||
import { migratedPnvo001Mirrored } from "./pnvoPortDisplayMigration";
|
||||
import {
|
||||
WorkspaceViewTabs,
|
||||
type WorkspaceView,
|
||||
@@ -9473,7 +9474,12 @@ function normalizeLoadedNode(
|
||||
parameterUnits,
|
||||
parameterScientificNotation,
|
||||
rotation: normalizeNodeRotation(node.data.rotation),
|
||||
mirrored: Boolean(node.data.mirrored),
|
||||
mirrored: migratedPnvo001Mirrored(
|
||||
definition?.modelType ?? node.data.modelType,
|
||||
node.data.ports,
|
||||
definition?.ports,
|
||||
Boolean(node.data.mirrored),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ import {
|
||||
orthogonalEdgePoints,
|
||||
type EdgeRouteData,
|
||||
} from "./edgeRouting";
|
||||
import {
|
||||
continuousMechanicalForceChartValues,
|
||||
resultStateTransitionSampleIndices,
|
||||
segmentedChartPath,
|
||||
} from "./resultEventSeries";
|
||||
|
||||
type ResultPortDefinition = {
|
||||
name: string;
|
||||
@@ -136,6 +141,11 @@ type ResultPayload = {
|
||||
final: Record<string, number>;
|
||||
series: Record<string, number[]>;
|
||||
diagnostics: {
|
||||
integration?: {
|
||||
segments?: Array<{
|
||||
stateTransitionTimes?: number[];
|
||||
}>;
|
||||
};
|
||||
pressureFlow: {
|
||||
solveCount: number;
|
||||
maxScaledResidual: number;
|
||||
@@ -383,6 +393,14 @@ export function SimulationResultsView({
|
||||
}: SimulationResultsViewProps) {
|
||||
const reactFlow = useReactFlow();
|
||||
const resultStatus = simulationResultStatus(snapshot.result);
|
||||
const stateTransitionSampleIndices = useMemo(
|
||||
() =>
|
||||
resultStateTransitionSampleIndices(
|
||||
snapshot.result.series.time ?? [],
|
||||
snapshot.result.diagnostics,
|
||||
),
|
||||
[snapshot.result.diagnostics, snapshot.result.series.time],
|
||||
);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(
|
||||
snapshot.project.nodes[0]?.id ?? null,
|
||||
);
|
||||
@@ -1613,9 +1631,16 @@ export function SimulationResultsView({
|
||||
variable,
|
||||
chartWindow.unit,
|
||||
);
|
||||
const displayValues = (
|
||||
snapshot.result.series[chartWindow.variableKey] ?? []
|
||||
).map(selectedUnit.fromBase);
|
||||
const chartTime = snapshot.result.series.time ?? [];
|
||||
const primaryChartValues = continuousMechanicalForceChartValues(
|
||||
variable,
|
||||
component,
|
||||
snapshot.result.series[chartWindow.variableKey] ?? [],
|
||||
stateTransitionSampleIndices,
|
||||
);
|
||||
const displayValues = primaryChartValues.values.map(
|
||||
selectedUnit.fromBase,
|
||||
);
|
||||
const combinedVariables = windowVariables.filter(
|
||||
(item) => isMixed || item.unit === variable.unit,
|
||||
);
|
||||
@@ -1632,6 +1657,13 @@ export function SimulationResultsView({
|
||||
chartWindow.variableUnits[item.key] ?? item.unit,
|
||||
)
|
||||
: selectedUnit;
|
||||
const itemChartValues =
|
||||
continuousMechanicalForceChartValues(
|
||||
item,
|
||||
itemComponent,
|
||||
snapshot.result.series[item.key] ?? [],
|
||||
stateTransitionSampleIndices,
|
||||
);
|
||||
return {
|
||||
key: item.key,
|
||||
label: resultVariableTitle(
|
||||
@@ -1640,12 +1672,18 @@ export function SimulationResultsView({
|
||||
),
|
||||
color: colorForVariable(item.key),
|
||||
unit: itemUnit.label,
|
||||
values: (snapshot.result.series[item.key] ?? []).map(
|
||||
itemUnit.fromBase,
|
||||
),
|
||||
values: itemChartValues.values.map(itemUnit.fromBase),
|
||||
separatedEventSampleCount:
|
||||
itemChartValues.separatedEventSampleCount,
|
||||
};
|
||||
});
|
||||
const chartTime = snapshot.result.series.time ?? [];
|
||||
const separatedEventSampleCount = isCombined
|
||||
? multiSeries.reduce(
|
||||
(count, item) =>
|
||||
count + item.separatedEventSampleCount,
|
||||
0,
|
||||
)
|
||||
: primaryChartValues.separatedEventSampleCount;
|
||||
const visibilitySeries = isCombined
|
||||
? multiSeries
|
||||
: [
|
||||
@@ -1671,6 +1709,7 @@ export function SimulationResultsView({
|
||||
}`}
|
||||
data-chart-kind={chartWindow.kind}
|
||||
data-pending="false"
|
||||
data-separated-event-samples={separatedEventSampleCount}
|
||||
key={chartWindow.id}
|
||||
onDragLeave={(event) => {
|
||||
if (
|
||||
@@ -1727,6 +1766,9 @@ export function SimulationResultsView({
|
||||
{isCombined
|
||||
? `${visibleCombinedVariables.length}/${combinedVariables.length} 条显示 · ${isMixed ? "上下同步图" : variable.unit || "无量纲"}`
|
||||
: componentName}
|
||||
{separatedEventSampleCount > 0
|
||||
? ` · 已隐藏 ${separatedEventSampleCount} 个孤立事件力尖峰`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="result-chart-window-actions">
|
||||
@@ -2026,7 +2068,7 @@ export function SimulationResultsView({
|
||||
}
|
||||
onZoomPrevious={() => undoChartWindowZoom(chartWindow.id)}
|
||||
series={multiSeries}
|
||||
time={snapshot.result.series.time ?? []}
|
||||
time={chartTime}
|
||||
viewport={chartWindow.viewport}
|
||||
zoomEnabled={chartWindow.zoomEnabled}
|
||||
/>
|
||||
@@ -2038,7 +2080,7 @@ export function SimulationResultsView({
|
||||
}
|
||||
onZoomPrevious={() => undoChartWindowZoom(chartWindow.id)}
|
||||
series={multiSeries}
|
||||
time={snapshot.result.series.time ?? []}
|
||||
time={chartTime}
|
||||
title="多曲线对比"
|
||||
unit={selectedUnit.label}
|
||||
viewport={chartWindow.viewport}
|
||||
@@ -2052,7 +2094,7 @@ export function SimulationResultsView({
|
||||
updateChartWindowViewport(chartWindow.id, viewport)
|
||||
}
|
||||
onZoomPrevious={() => undoChartWindowZoom(chartWindow.id)}
|
||||
time={snapshot.result.series.time ?? []}
|
||||
time={chartTime}
|
||||
title={chartTitle}
|
||||
unit={selectedUnit.label}
|
||||
values={displayValues}
|
||||
@@ -2268,6 +2310,7 @@ type MultiCurveSeries = {
|
||||
color: string;
|
||||
unit: string;
|
||||
values: number[];
|
||||
separatedEventSampleCount: number;
|
||||
};
|
||||
|
||||
type ChartVisibilitySeries = {
|
||||
@@ -3598,12 +3641,12 @@ function MultiCurvePlot({
|
||||
/>
|
||||
<g clipPath={`url(#${clipPathId})`} data-chart-clipped-series="true">
|
||||
{plottedSeries.map((curve) => {
|
||||
const path = curve.samples
|
||||
.map(
|
||||
(sample, index) =>
|
||||
`${index === 0 ? "M" : "L"} ${xPosition(sample.x).toFixed(2)} ${yPosition(sample.y).toFixed(2)}`,
|
||||
)
|
||||
.join(" ");
|
||||
const path = segmentedChartPath(
|
||||
time,
|
||||
curve.values,
|
||||
xPosition,
|
||||
yPosition,
|
||||
);
|
||||
return (
|
||||
<path
|
||||
d={path}
|
||||
@@ -4157,12 +4200,12 @@ function StackedCurvePlot({
|
||||
</defs>
|
||||
{plotBands.map((band) => {
|
||||
const isLast = band.index === plotBands.length - 1;
|
||||
const path = band.curve.samples
|
||||
.map(
|
||||
(sample, sampleIndex) =>
|
||||
`${sampleIndex === 0 ? "M" : "L"} ${xPosition(sample.x).toFixed(2)} ${band.yPosition(sample.y).toFixed(2)}`,
|
||||
)
|
||||
.join(" ");
|
||||
const path = segmentedChartPath(
|
||||
time,
|
||||
band.curve.values,
|
||||
xPosition,
|
||||
band.yPosition,
|
||||
);
|
||||
return (
|
||||
<g className="result-chart-stack-band" key={band.curve.key}>
|
||||
{band.index > 0 ? (
|
||||
@@ -4614,14 +4657,12 @@ function CurvePlot({
|
||||
margin.top + (1 - (value - yMin) / yRange) * plotHeight;
|
||||
const xTicks = chartTicks(xMin, xMax, 5);
|
||||
const yTicks = chartTicks(yMin, yMax, 5);
|
||||
const path = samples
|
||||
.map(
|
||||
(sample, index) =>
|
||||
`${index === 0 ? "M" : "L"} ${xPosition(sample.x).toFixed(2)} ${yPosition(
|
||||
sample.y,
|
||||
).toFixed(2)}`,
|
||||
)
|
||||
.join(" ");
|
||||
const path = segmentedChartPath(
|
||||
time,
|
||||
values,
|
||||
xPosition,
|
||||
yPosition,
|
||||
);
|
||||
const requestedCursorIndex =
|
||||
cursorIndex === null
|
||||
? Math.floor(samples.length / 2)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
type PortSideSnapshot = {
|
||||
name: string;
|
||||
side: string;
|
||||
};
|
||||
|
||||
const PNVO001_MODEL_TYPES = new Set([
|
||||
"amesim_pnvo001_fixed",
|
||||
"amesim_pnvo001",
|
||||
]);
|
||||
|
||||
function portUsesSide(
|
||||
ports: readonly PortSideSnapshot[],
|
||||
portName: string,
|
||||
side: string,
|
||||
) {
|
||||
return ports.some((port) => port.name === portName && port.side === side);
|
||||
}
|
||||
|
||||
export function migratedPnvo001Mirrored(
|
||||
modelType: string,
|
||||
savedPorts: readonly PortSideSnapshot[],
|
||||
currentPorts: readonly PortSideSnapshot[] | undefined,
|
||||
mirrored: boolean,
|
||||
) {
|
||||
if (!PNVO001_MODEL_TYPES.has(modelType) || !currentPorts) {
|
||||
return mirrored;
|
||||
}
|
||||
|
||||
const savedUsesLegacySides =
|
||||
portUsesSide(savedPorts, "port_2", "left") &&
|
||||
portUsesSide(savedPorts, "port_3", "right");
|
||||
const catalogUsesAmesimSides =
|
||||
portUsesSide(currentPorts, "port_2", "right") &&
|
||||
portUsesSide(currentPorts, "port_3", "left");
|
||||
|
||||
return savedUsesLegacySides && catalogUsesAmesimSides
|
||||
? !mirrored
|
||||
: mirrored;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
export type StateTransitionDiagnostics = {
|
||||
integration?: {
|
||||
segments?: Array<{
|
||||
stateTransitionTimes?: number[];
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type EventSeriesVariable = {
|
||||
scope: "component" | "port";
|
||||
portName: string | null;
|
||||
quantity: string;
|
||||
category: string;
|
||||
};
|
||||
|
||||
type EventSeriesComponent = {
|
||||
data: {
|
||||
ports: Array<{
|
||||
name: string;
|
||||
domain: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
const ISOLATED_EVENT_FORCE_RATIO = 100;
|
||||
|
||||
export function resultStateTransitionSampleIndices(
|
||||
time: number[],
|
||||
diagnostics: StateTransitionDiagnostics,
|
||||
) {
|
||||
const indices = new Set<number>();
|
||||
for (const segment of diagnostics.integration?.segments ?? []) {
|
||||
for (const eventTime of segment.stateTransitionTimes ?? []) {
|
||||
if (!Number.isFinite(eventTime)) {
|
||||
continue;
|
||||
}
|
||||
let index = time.indexOf(eventTime);
|
||||
if (index < 0) {
|
||||
const tolerance = 1e-12 * Math.max(Math.abs(eventTime), 1);
|
||||
index = time.findIndex(
|
||||
(sampleTime) => Math.abs(sampleTime - eventTime) <= tolerance,
|
||||
);
|
||||
}
|
||||
if (index >= 0) {
|
||||
indices.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
export function continuousMechanicalForceChartValues(
|
||||
variable: EventSeriesVariable,
|
||||
component: EventSeriesComponent | undefined,
|
||||
values: number[],
|
||||
stateTransitionSampleIndices: ReadonlySet<number>,
|
||||
) {
|
||||
const componentHasMechanicalPort = Boolean(
|
||||
component?.data.ports.some((port) => port.domain === "mechanical"),
|
||||
);
|
||||
const forceBelongsToMechanicalDomain =
|
||||
variable.scope === "component"
|
||||
? variable.category === "derived" && componentHasMechanicalPort
|
||||
: variable.portName !== null &&
|
||||
Boolean(
|
||||
component?.data.ports.some(
|
||||
(port) =>
|
||||
port.name === variable.portName &&
|
||||
port.domain === "mechanical",
|
||||
),
|
||||
);
|
||||
if (variable.quantity !== "force" || !forceBelongsToMechanicalDomain) {
|
||||
return { values, separatedEventSampleCount: 0 };
|
||||
}
|
||||
|
||||
const isolatedEventIndices: number[] = [];
|
||||
for (const index of stateTransitionSampleIndices) {
|
||||
if (
|
||||
index < 0 ||
|
||||
index >= values.length ||
|
||||
!isolatedEventForceSample(
|
||||
values,
|
||||
index,
|
||||
stateTransitionSampleIndices,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
isolatedEventIndices.push(index);
|
||||
}
|
||||
if (isolatedEventIndices.length === 0) {
|
||||
return { values, separatedEventSampleCount: 0 };
|
||||
}
|
||||
|
||||
const displayValues = values.slice();
|
||||
for (const index of isolatedEventIndices) {
|
||||
displayValues[index] = Number.NaN;
|
||||
}
|
||||
return {
|
||||
values: displayValues,
|
||||
separatedEventSampleCount: isolatedEventIndices.length,
|
||||
};
|
||||
}
|
||||
|
||||
function isolatedEventForceSample(
|
||||
values: number[],
|
||||
index: number,
|
||||
stateTransitionSampleIndices: ReadonlySet<number>,
|
||||
) {
|
||||
const value = Number(values[index]);
|
||||
if (!Number.isFinite(value)) {
|
||||
return false;
|
||||
}
|
||||
const previous = finiteNonEventNeighbor(
|
||||
values,
|
||||
index - 1,
|
||||
-1,
|
||||
stateTransitionSampleIndices,
|
||||
);
|
||||
const next = finiteNonEventNeighbor(
|
||||
values,
|
||||
index + 1,
|
||||
1,
|
||||
stateTransitionSampleIndices,
|
||||
);
|
||||
if (previous === null || next === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const localScale = Math.max(
|
||||
Math.abs(previous),
|
||||
Math.abs(next),
|
||||
Math.abs(next - previous),
|
||||
1e-9,
|
||||
);
|
||||
const localEstimate = (previous + next) / 2;
|
||||
return (
|
||||
Math.abs(value) > ISOLATED_EVENT_FORCE_RATIO * localScale &&
|
||||
Math.abs(value - localEstimate) >
|
||||
ISOLATED_EVENT_FORCE_RATIO * localScale
|
||||
);
|
||||
}
|
||||
|
||||
function finiteNonEventNeighbor(
|
||||
values: number[],
|
||||
startIndex: number,
|
||||
direction: -1 | 1,
|
||||
stateTransitionSampleIndices: ReadonlySet<number>,
|
||||
) {
|
||||
for (
|
||||
let index = startIndex;
|
||||
index >= 0 && index < values.length;
|
||||
index += direction
|
||||
) {
|
||||
if (stateTransitionSampleIndices.has(index)) {
|
||||
continue;
|
||||
}
|
||||
const value = Number(values[index]);
|
||||
if (Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function segmentedChartPath(
|
||||
time: number[],
|
||||
values: number[],
|
||||
xPosition: (value: number) => number,
|
||||
yPosition: (value: number) => number,
|
||||
) {
|
||||
const commands: string[] = [];
|
||||
let continuesSegment = false;
|
||||
for (let index = 0; index < time.length; index += 1) {
|
||||
const x = Number(time[index]);
|
||||
const y = Number(values[index]);
|
||||
if (!Number.isFinite(x)) {
|
||||
continuesSegment = false;
|
||||
continue;
|
||||
}
|
||||
if (!Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
commands.push(
|
||||
`${continuesSegment ? "L" : "M"} ${xPosition(x).toFixed(2)} ${yPosition(y).toFixed(2)}`,
|
||||
);
|
||||
continuesSegment = true;
|
||||
}
|
||||
return commands.join(" ");
|
||||
}
|
||||
Reference in new issue
Block a user