Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/@hashintel/petrinaut/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"d3-scale": "4.0.2",
"elkjs": "0.11.0",
"monaco-editor": "0.55.1",
"pyodide": "0.27.7",
"react-icons": "5.5.0",
"react-resizable-panels": "4.6.5",
"typescript": "5.9.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";

import { buildSimulation } from "./build-simulation";
import type { SimulationInput } from "./types";

// Mock the SymPy compilation to return simple passthrough functions.
// This lets us test buildSimulation's validation and buffer logic
// without requiring a real Pyodide instance.
vi.mock("./compile-via-sympy", () => ({
compileDifferentialEquationViaSymPy: vi.fn().mockResolvedValue(
// Returns a no-op dynamics function
() => [],
),
compileLambdaViaSymPy: vi.fn().mockResolvedValue(
// Returns a constant rate
() => 1.0,
),
compileTransitionKernelViaSymPy: vi.fn().mockResolvedValue(
// Returns empty kernel output
() => ({}),
),
}));

// Create a mock PyodideInterface
const mockPyodide = {} as Parameters<typeof buildSimulation>[1];

describe("buildSimulation", () => {
it("builds a simulation with a single place and initial tokens", () => {
it("builds a simulation with a single place and initial tokens", async () => {
const input: SimulationInput = {
sdcpn: {
types: [
Expand Down Expand Up @@ -56,7 +77,7 @@ describe("buildSimulation", () => {
maxTime: null,
};

const simulationInstance = buildSimulation(input);
const simulationInstance = await buildSimulation(input, mockPyodide);
const frame = simulationInstance.frames[0]!;

// Verify simulation instance properties
Expand Down Expand Up @@ -88,7 +109,7 @@ describe("buildSimulation", () => {
expect(simulationInstance.differentialEquationFns.has("p1")).toBe(true);
});

it("builds a simulation with multiple places, transitions, and proper buffer layout", () => {
it("builds a simulation with multiple places, transitions, and proper buffer layout", async () => {
const input: SimulationInput = {
sdcpn: {
types: [
Expand Down Expand Up @@ -204,7 +225,7 @@ describe("buildSimulation", () => {
maxTime: null,
};

const simulationInstance = buildSimulation(input);
const simulationInstance = await buildSimulation(input, mockPyodide);
const frame = simulationInstance.frames[0]!;

// Verify simulation instance properties
Expand Down Expand Up @@ -266,7 +287,7 @@ describe("buildSimulation", () => {
expect(typeof kernelFn).toBe("function");
});

it("throws error when initialMarking references non-existent place", () => {
it("throws error when initialMarking references non-existent place", async () => {
const input: SimulationInput = {
sdcpn: {
types: [
Expand Down Expand Up @@ -315,12 +336,12 @@ describe("buildSimulation", () => {
maxTime: null,
};

expect(() => buildSimulation(input)).toThrow(
await expect(buildSimulation(input, mockPyodide)).rejects.toThrow(
"Place with ID p_nonexistent in initialMarking does not exist in SDCPN",
);
});

it("throws error when token dimensions don't match place dimensions", () => {
it("throws error when token dimensions don't match place dimensions", async () => {
const input: SimulationInput = {
sdcpn: {
types: [
Expand Down Expand Up @@ -372,7 +393,7 @@ describe("buildSimulation", () => {
maxTime: null,
};

expect(() => buildSimulation(input)).toThrow(
await expect(buildSimulation(input, mockPyodide)).rejects.toThrow(
"Token dimension mismatch for place p1. Expected 4 values (2 dimensions Γ— 2 tokens), got 3",
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { PyodideInterface } from "pyodide";

import { SDCPNItemError } from "../../core/errors";
import {
deriveDefaultParameterValues,
mergeParameterValues,
} from "../../hooks/use-default-parameter-values";
import { compileUserCode } from "./compile-user-code";
import {
compileDifferentialEquationViaSymPy,
compileLambdaViaSymPy,
compileTransitionKernelViaSymPy,
} from "./compile-via-sympy";
import type {
DifferentialEquationFn,
LambdaFn,
ParameterValues,
SimulationFrame,
SimulationInput,
SimulationInstance,
Expand Down Expand Up @@ -49,12 +54,16 @@ function getPlaceDimensions(
* - All places and transitions initialized with proper state
*
* @param input - The simulation input configuration
* @param pyodide - Initialized Pyodide instance with SymPy
* @returns The initial simulation frame ready for execution
* @throws {Error} if place IDs in initialMarking don't match places in SDCPN
* @throws {Error} if token dimensions don't match place dimensions
* @throws {Error} if user code fails to compile
*/
export function buildSimulation(input: SimulationInput): SimulationInstance {
export async function buildSimulation(
input: SimulationInput,
pyodide: PyodideInterface,
): Promise<SimulationInstance> {
const {
sdcpn,
initialMarking,
Expand Down Expand Up @@ -100,7 +109,7 @@ export function buildSimulation(input: SimulationInput): SimulationInstance {
}
}

// Compile all differential equation functions
// Compile all differential equation functions via SymPy
const differentialEquationFns = new Map<string, DifferentialEquationFn>();
for (const place of sdcpn.places) {
// Skip places without dynamics enabled or without differential equation code
Expand All @@ -119,9 +128,11 @@ export function buildSimulation(input: SimulationInput): SimulationInstance {
const { code } = differentialEquation;

try {
const fn = compileUserCode<[Record<string, number>[], ParameterValues]>(
const fn = await compileDifferentialEquationViaSymPy(
code,
"Dynamics",
sdcpn,
place.colorId!,
pyodide,
);
differentialEquationFns.set(place.id, fn as DifferentialEquationFn);
} catch (error) {
Expand All @@ -134,13 +145,16 @@ export function buildSimulation(input: SimulationInput): SimulationInstance {
}
}

// Compile all lambda functions
// Compile all lambda functions via SymPy
const lambdaFns = new Map<string, LambdaFn>();
for (const transition of sdcpn.transitions) {
try {
const fn = compileUserCode<
[Record<string, Record<string, number>[]>, ParameterValues]
>(transition.lambdaCode, "Lambda");
const fn = await compileLambdaViaSymPy(
transition.lambdaCode,
sdcpn,
transition,
pyodide,
);
lambdaFns.set(transition.id, fn as LambdaFn);
} catch (error) {
throw new SDCPNItemError(
Expand Down Expand Up @@ -173,9 +187,12 @@ export function buildSimulation(input: SimulationInput): SimulationInstance {
}

try {
const fn = compileUserCode<
[Record<string, Record<string, number>[]>, ParameterValues]
>(transition.transitionKernelCode, "TransitionKernel");
const fn = await compileTransitionKernelViaSymPy(
transition.transitionKernelCode,
sdcpn,
transition,
pyodide,
);
transitionKernelFns.set(transition.id, fn as TransitionKernelFn);
} catch (error) {
throw new SDCPNItemError(
Expand Down
Loading
Loading