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(); 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, ) { 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, ) { 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, ) { 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(" "); }