Skip to content

Commit 2f206d0

Browse files
committed
Enhance simulation context and provider: Introduce SimulationFrameState and SimulationFrameStateDiff types, update currently viewed frame handling, and refactor related components for improved state management and clarity.
1 parent 8f8852e commit 2f206d0

File tree

9 files changed

+233
-60
lines changed

9 files changed

+233
-60
lines changed

libs/@hashintel/petrinaut/src/state/simulation-context.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,53 @@ export type SimulationState =
99
| "Error"
1010
| "Paused";
1111

12+
/**
13+
* State of a simulation frame.
14+
*/
15+
export type SimulationFrameState = {
16+
number: number;
17+
time: number;
18+
places: {
19+
[placeId: string]:
20+
| {
21+
/** Number of tokens in the place at the time of the frame. */
22+
tokenCount: number;
23+
}
24+
| undefined;
25+
};
26+
transitions: {
27+
[transitionId: string]:
28+
| {
29+
/** Time since last firing of the transition at the time of the frame. */
30+
timeSinceLastFiring: number;
31+
}
32+
| undefined;
33+
};
34+
};
35+
36+
/**
37+
* Difference between two simulation frame states.
38+
*/
39+
export type SimulationFrameStateDiff = {
40+
currentFrame: SimulationFrameState;
41+
comparedFrame: SimulationFrameState;
42+
places: {
43+
[placeId: string]:
44+
| {
45+
tokenCount: number;
46+
}
47+
| undefined;
48+
};
49+
transitions: {
50+
[transitionId: string]:
51+
| {
52+
/** Number of times this transition fired since the compared frame. */
53+
firingCount: number;
54+
}
55+
| undefined;
56+
};
57+
};
58+
1259
export type InitialMarking = Map<
1360
string,
1461
{ values: Float64Array; count: number }
@@ -25,7 +72,16 @@ export type SimulationContextValue = {
2572
errorItemId: string | null;
2673
parameterValues: Record<string, string>;
2774
initialMarking: InitialMarking;
28-
currentlyViewedFrame: number;
75+
/**
76+
* The currently viewed simulation frame state.
77+
* Null when no simulation is running or no frames exist.
78+
*/
79+
currentViewedFrame: SimulationFrameState | null;
80+
/**
81+
* The difference between the currently viewed frame and the previous frame.
82+
* Null when no simulation is running, no frames exist, or viewing frame 0.
83+
*/
84+
currentViewedFrameDiff: SimulationFrameStateDiff | null;
2985
dt: number;
3086

3187
// Actions
@@ -41,7 +97,7 @@ export type SimulationContextValue = {
4197
run: () => void;
4298
pause: () => void;
4399
reset: () => void;
44-
setCurrentlyViewedFrame: (frameIndex: number) => void;
100+
setCurrentViewedFrame: (frameIndex: number) => void;
45101
};
46102

47103
const DEFAULT_CONTEXT_VALUE: SimulationContextValue = {
@@ -51,7 +107,8 @@ const DEFAULT_CONTEXT_VALUE: SimulationContextValue = {
51107
errorItemId: null,
52108
parameterValues: {},
53109
initialMarking: new Map(),
54-
currentlyViewedFrame: 0,
110+
currentViewedFrame: null,
111+
currentViewedFrameDiff: null,
55112
dt: 0.01,
56113
setInitialMarking: () => {},
57114
setParameterValue: () => {},
@@ -62,7 +119,7 @@ const DEFAULT_CONTEXT_VALUE: SimulationContextValue = {
62119
run: () => {},
63120
pause: () => {},
64121
reset: () => {},
65-
setCurrentlyViewedFrame: () => {},
122+
setCurrentViewedFrame: () => {},
66123
};
67124

68125
export const SimulationContext = createContext<SimulationContextValue>(

libs/@hashintel/petrinaut/src/state/simulation-provider.tsx

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
type InitialMarking,
1515
SimulationContext,
1616
type SimulationContextValue,
17+
type SimulationFrameState,
18+
type SimulationFrameStateDiff,
1719
type SimulationState,
1820
} from "./simulation-context";
1921

@@ -24,7 +26,10 @@ type SimulationStateValues = {
2426
errorItemId: string | null;
2527
parameterValues: Record<string, string>;
2628
initialMarking: InitialMarking;
27-
currentlyViewedFrame: number;
29+
/** Internal frame index for tracking which frame is being viewed */
30+
currentViewedFrameIndex: number | null;
31+
/** Internal frame index for tracking which frame was previously viewed */
32+
previousViewedFrameIndex: number | null;
2833
dt: number;
2934
};
3035

@@ -35,10 +40,87 @@ const initialStateValues: SimulationStateValues = {
3540
errorItemId: null,
3641
parameterValues: {},
3742
initialMarking: new Map(),
38-
currentlyViewedFrame: 0,
43+
currentViewedFrameIndex: null,
44+
previousViewedFrameIndex: null,
3945
dt: 0.01,
4046
};
4147

48+
/**
49+
* Converts a simulation frame to a SimulationFrameState.
50+
*/
51+
function buildFrameState(
52+
simulation: SimulationContextValue["simulation"],
53+
frameIndex: number,
54+
): SimulationFrameState | null {
55+
if (!simulation || simulation.frames.length === 0) {
56+
return null;
57+
}
58+
59+
const frame = simulation.frames[frameIndex];
60+
if (!frame) {
61+
return null;
62+
}
63+
64+
const places: SimulationFrameState["places"] = {};
65+
for (const [placeId, placeData] of frame.places) {
66+
places[placeId] = {
67+
tokenCount: placeData.count,
68+
};
69+
}
70+
71+
const transitions: SimulationFrameState["transitions"] = {};
72+
for (const [transitionId, transitionData] of frame.transitions) {
73+
transitions[transitionId] = {
74+
timeSinceLastFiring: transitionData.timeSinceLastFiring,
75+
};
76+
}
77+
78+
return {
79+
number: frameIndex,
80+
time: frame.time,
81+
places,
82+
transitions,
83+
};
84+
}
85+
86+
/**
87+
* Computes the difference between two simulation frames.
88+
*/
89+
function buildFrameStateDiff(
90+
currentFrame: SimulationFrameState,
91+
comparedFrame: SimulationFrameState,
92+
): SimulationFrameStateDiff {
93+
const places: SimulationFrameStateDiff["places"] = {};
94+
for (const placeId of Object.keys(currentFrame.places)) {
95+
const currentTokenCount = currentFrame.places[placeId]?.tokenCount ?? 0;
96+
const comparedTokenCount = comparedFrame.places[placeId]?.tokenCount ?? 0;
97+
places[placeId] = {
98+
tokenCount: currentTokenCount - comparedTokenCount,
99+
};
100+
}
101+
102+
const transitions: SimulationFrameStateDiff["transitions"] = {};
103+
for (const transitionId of Object.keys(currentFrame.transitions)) {
104+
const currentTimeSinceLastFiring =
105+
currentFrame.transitions[transitionId]?.timeSinceLastFiring ?? 0;
106+
// Count firings: if timeSinceLastFiring is 0, the transition just fired
107+
// We need to count how many times it fired between the two frames
108+
// For simplicity, we count 1 if it fired in current frame (timeSinceLastFiring === 0)
109+
// and the compared frame had a non-zero timeSinceLastFiring
110+
const justFired = currentTimeSinceLastFiring === 0;
111+
transitions[transitionId] = {
112+
firingCount: justFired ? 1 : 0,
113+
};
114+
}
115+
116+
return {
117+
currentFrame,
118+
comparedFrame,
119+
places,
120+
transitions,
121+
};
122+
}
123+
42124
/**
43125
* Internal component that subscribes to simulation state changes
44126
* and shows notifications when appropriate.
@@ -190,7 +272,8 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
190272
state: "Paused",
191273
error: null,
192274
errorItemId: null,
193-
currentlyViewedFrame: 0,
275+
currentViewedFrameIndex: 0,
276+
previousViewedFrameIndex: null,
194277
};
195278
} catch (error) {
196279
// eslint-disable-next-line no-console
@@ -253,7 +336,8 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
253336
state: newState,
254337
error: null,
255338
errorItemId: null,
256-
currentlyViewedFrame: updatedSimulation.currentFrameNumber,
339+
previousViewedFrameIndex: prev.currentViewedFrameIndex,
340+
currentViewedFrameIndex: updatedSimulation.currentFrameNumber,
257341
};
258342
} catch (error) {
259343
// eslint-disable-next-line no-console
@@ -358,7 +442,8 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
358442
error: null,
359443
errorItemId: null,
360444
parameterValues,
361-
currentlyViewedFrame: 0,
445+
currentViewedFrameIndex: null,
446+
previousViewedFrameIndex: null,
362447
// Keep initialMarking when resetting - it's configuration, not simulation state
363448
}));
364449
},
@@ -384,7 +469,7 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
384469
return { ...prev, state: newState };
385470
}),
386471

387-
setCurrentlyViewedFrame: (frameIndex: number) =>
472+
setCurrentViewedFrame: (frameIndex: number) =>
388473
setStateValues((prev) => {
389474
if (!prev.simulation) {
390475
throw new Error(
@@ -395,12 +480,52 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
395480
const totalFrames = prev.simulation.frames.length;
396481
const clampedIndex = Math.max(0, Math.min(frameIndex, totalFrames - 1));
397482

398-
return { ...prev, currentlyViewedFrame: clampedIndex };
483+
return {
484+
...prev,
485+
previousViewedFrameIndex: prev.currentViewedFrameIndex,
486+
currentViewedFrameIndex: clampedIndex,
487+
};
399488
}),
400489
};
401490

491+
// Compute the currently viewed frame state
492+
const currentViewedFrame =
493+
stateValues.currentViewedFrameIndex !== null
494+
? buildFrameState(
495+
stateValues.simulation,
496+
stateValues.currentViewedFrameIndex,
497+
)
498+
: null;
499+
500+
// Compute the frame diff (comparing current frame with previous frame)
501+
let currentViewedFrameDiff: SimulationFrameStateDiff | null = null;
502+
if (
503+
currentViewedFrame &&
504+
stateValues.currentViewedFrameIndex !== null &&
505+
stateValues.previousViewedFrameIndex !== null
506+
) {
507+
const previousFrame = buildFrameState(
508+
stateValues.simulation,
509+
stateValues.previousViewedFrameIndex,
510+
);
511+
if (previousFrame) {
512+
currentViewedFrameDiff = buildFrameStateDiff(
513+
currentViewedFrame,
514+
previousFrame,
515+
);
516+
}
517+
}
518+
402519
const contextValue: SimulationContextValue = {
403-
...stateValues,
520+
simulation: stateValues.simulation,
521+
state: stateValues.state,
522+
error: stateValues.error,
523+
errorItemId: stateValues.errorItemId,
524+
parameterValues: stateValues.parameterValues,
525+
initialMarking: stateValues.initialMarking,
526+
dt: stateValues.dt,
527+
currentViewedFrame,
528+
currentViewedFrameDiff,
404529
...actions,
405530
};
406531

libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export const SimulationControls: React.FC<SimulationControlsProps> = ({
7373
run,
7474
pause,
7575
dt,
76-
currentlyViewedFrame,
77-
setCurrentlyViewedFrame,
76+
currentViewedFrame,
77+
setCurrentViewedFrame,
7878
} = use(SimulationContext);
7979

8080
const { setBottomPanelOpen, setActiveBottomPanelTab } = use(EditorContext);
@@ -90,7 +90,8 @@ export const SimulationControls: React.FC<SimulationControlsProps> = ({
9090
const hasSimulation = simulation !== null;
9191
const isRunning = simulationState === "Running";
9292
const isComplete = simulationState === "Complete";
93-
const elapsedTime = simulation ? currentlyViewedFrame * simulation.dt : 0;
93+
const frameIndex = currentViewedFrame?.number ?? 0;
94+
const elapsedTime = currentViewedFrame?.time ?? 0;
9495

9596
const getPlayPauseTooltip = () => {
9697
if (isDisabled) {
@@ -178,7 +179,7 @@ export const SimulationControls: React.FC<SimulationControlsProps> = ({
178179
<div className={frameInfoStyle}>
179180
<div>Frame</div>
180181
<div>
181-
{currentlyViewedFrame + 1} / {totalFrames}
182+
{frameIndex + 1} / {totalFrames}
182183
</div>
183184
<div className={elapsedTimeStyle}>{elapsedTime.toFixed(3)}s</div>
184185
</div>
@@ -187,10 +188,10 @@ export const SimulationControls: React.FC<SimulationControlsProps> = ({
187188
type="range"
188189
min="0"
189190
max={Math.max(0, totalFrames - 1)}
190-
value={currentlyViewedFrame}
191+
value={frameIndex}
191192
disabled={isDisabled}
192193
onChange={(event) =>
193-
setCurrentlyViewedFrame(Number(event.target.value))
194+
setCurrentViewedFrame(Number(event.target.value))
194195
}
195196
className={sliderStyle}
196197
/>

libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,24 +254,21 @@ export const InitialStateEditor: React.FC<InitialStateEditorProps> = ({
254254
const internalResize = useResizable(250);
255255
const { height, isResizing, containerRef, startResize } = internalResize;
256256

257-
const {
258-
initialMarking,
259-
setInitialMarking,
260-
simulation,
261-
currentlyViewedFrame,
262-
} = use(SimulationContext);
257+
const { initialMarking, setInitialMarking, simulation, currentViewedFrame } =
258+
use(SimulationContext);
263259

264260
// Determine if we should show current simulation state or initial marking
265261
const hasSimulation = simulation !== null && simulation.frames.length > 0;
262+
const frameIndex = currentViewedFrame?.number ?? 0;
266263

267264
// Get current marking for this place - either from simulation frame or initial marking
268265
const getCurrentMarkingData = (): {
269266
values: Float64Array;
270267
count: number;
271268
} | null => {
272269
if (hasSimulation) {
273-
// Get from currently viewed frame
274-
const currentFrame = simulation.frames[currentlyViewedFrame];
270+
// Get from currently viewed frame (need raw frame for buffer access)
271+
const currentFrame = simulation.frames[frameIndex];
275272
if (!currentFrame) {
276273
return null;
277274
}

0 commit comments

Comments
 (0)