diff --git a/.changeset/funky-deserts-grin.md b/.changeset/funky-deserts-grin.md new file mode 100644 index 00000000000..ddc6ae5fcdb --- /dev/null +++ b/.changeset/funky-deserts-grin.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Add support for inhibitor arcs diff --git a/libs/@hashintel/petrinaut/src/clipboard/paste.test.ts b/libs/@hashintel/petrinaut/src/clipboard/paste.test.ts index 7a7e123c759..20e262985b4 100644 --- a/libs/@hashintel/petrinaut/src/clipboard/paste.test.ts +++ b/libs/@hashintel/petrinaut/src/clipboard/paste.test.ts @@ -420,7 +420,9 @@ describe("paste — arc remapping", () => { { id: "transition__old-t1", name: "T1", - inputArcs: [{ placeId: "place__old-p1", weight: 3 }], + inputArcs: [ + { placeId: "place__old-p1", weight: 3, type: "standard" }, + ], outputArcs: [{ placeId: "place__old-p1", weight: 1 }], lambdaType: "predicate", lambdaCode: "", @@ -449,7 +451,9 @@ describe("paste — arc remapping", () => { { id: "transition__old", name: "T1", - inputArcs: [{ placeId: "place__missing", weight: 1 }], + inputArcs: [ + { placeId: "place__missing", weight: 1, type: "standard" }, + ], outputArcs: [{ placeId: "place__missing", weight: 1 }], lambdaType: "predicate", lambdaCode: "", @@ -485,8 +489,8 @@ describe("paste — arc remapping", () => { id: "transition__t1", name: "T1", inputArcs: [ - { placeId: "place__included", weight: 1 }, - { placeId: "place__excluded", weight: 2 }, + { placeId: "place__included", weight: 1, type: "standard" }, + { placeId: "place__excluded", weight: 2, type: "standard" }, ], outputArcs: [{ placeId: "place__excluded", weight: 1 }], lambdaType: "predicate", diff --git a/libs/@hashintel/petrinaut/src/clipboard/serialize.test.ts b/libs/@hashintel/petrinaut/src/clipboard/serialize.test.ts index 8bdfff0a7fd..e757125c96d 100644 --- a/libs/@hashintel/petrinaut/src/clipboard/serialize.test.ts +++ b/libs/@hashintel/petrinaut/src/clipboard/serialize.test.ts @@ -46,7 +46,7 @@ const fullNet: SDCPN = { { id: "t1", name: "Transition1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 2 }], lambdaType: "predicate", lambdaCode: "return true;", @@ -466,7 +466,7 @@ describe("parseClipboardPayload", () => { { id: "t1", name: "T", - inputArcs: [{ placeId: "p1", weight: "heavy" }], + inputArcs: [{ placeId: "p1", weight: "heavy", type: "standard" }], outputArcs: [], lambdaType: "predicate", lambdaCode: "", diff --git a/libs/@hashintel/petrinaut/src/clipboard/types.ts b/libs/@hashintel/petrinaut/src/clipboard/types.ts index 20dcde00d8f..59667c2aa40 100644 --- a/libs/@hashintel/petrinaut/src/clipboard/types.ts +++ b/libs/@hashintel/petrinaut/src/clipboard/types.ts @@ -2,7 +2,13 @@ import { z } from "zod"; export const CLIPBOARD_FORMAT_VERSION = 1; -const arcSchema = z.object({ +const inputArcSchema = z.object({ + placeId: z.string(), + weight: z.number(), + type: z.enum(["standard", "inhibitor"]).optional().default("standard"), +}); + +const outputArcSchema = z.object({ placeId: z.string(), weight: z.number(), }); @@ -21,8 +27,8 @@ const placeSchema = z.object({ const transitionSchema = z.object({ id: z.string(), name: z.string(), - inputArcs: z.array(arcSchema), - outputArcs: z.array(arcSchema), + inputArcs: z.array(inputArcSchema), + outputArcs: z.array(outputArcSchema), lambdaType: z.enum(["predicate", "stochastic"]), lambdaCode: z.string(), transitionKernelCode: z.string(), diff --git a/libs/@hashintel/petrinaut/src/core/types/sdcpn.ts b/libs/@hashintel/petrinaut/src/core/types/sdcpn.ts index 383ecf94bd9..3ebc7e71589 100644 --- a/libs/@hashintel/petrinaut/src/core/types/sdcpn.ts +++ b/libs/@hashintel/petrinaut/src/core/types/sdcpn.ts @@ -1,10 +1,21 @@ export type ID = string; +export type InputArc = { + placeId: string; + weight: number; + type: "standard" | "inhibitor"; +}; + +export type OutputArc = { + placeId: string; + weight: number; +}; + export type Transition = { id: ID; name: string; - inputArcs: { placeId: string; weight: number }[]; - outputArcs: { placeId: string; weight: number }[]; + inputArcs: InputArc[]; + outputArcs: OutputArc[]; lambdaType: "predicate" | "stochastic"; lambdaCode: string; transitionKernelCode: string; diff --git a/libs/@hashintel/petrinaut/src/examples/broken-machines.ts b/libs/@hashintel/petrinaut/src/examples/broken-machines.ts index 76ec640af3c..59e55c731fd 100644 --- a/libs/@hashintel/petrinaut/src/examples/broken-machines.ts +++ b/libs/@hashintel/petrinaut/src/examples/broken-machines.ts @@ -105,6 +105,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__81e551b4-11dc-4781-9cd7-dd882fd7e947", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -132,6 +133,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__81e551b4-11dc-4781-9cd7-dd882fd7e947", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -159,10 +161,12 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__d662407f-c56d-4a96-bcbb-ead785a9c594", weight: 1, + type: "standard", }, { placeId: "place__2bdd959f-a5bc-404a-bd03-34fafcef66b8", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -185,6 +189,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__17c65d6e-0c3e-48e6-a677-2914e28131ac", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -208,6 +213,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__e5af0410-d80a-4c8b-b3bf-692918b98e6c", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -234,6 +240,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__4b72cf19-907b-4fc0-ac0a-555453e95d4b", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -257,10 +264,12 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { placeId: "place__eaca89b8-1db1-45fa-8c3a-6eb6f0419ffa", weight: 1, + type: "standard", }, { placeId: "place__9cb073fb-f1d7-4613-8b10-8d1b08796f24", weight: 1, + type: "standard", }, ], outputArcs: [ diff --git a/libs/@hashintel/petrinaut/src/examples/deployment-pipeline.ts b/libs/@hashintel/petrinaut/src/examples/deployment-pipeline.ts new file mode 100644 index 00000000000..8473657977d --- /dev/null +++ b/libs/@hashintel/petrinaut/src/examples/deployment-pipeline.ts @@ -0,0 +1,178 @@ +import { SNAP_GRID_SIZE } from "../constants/ui"; +import type { SDCPN } from "../core/types/sdcpn"; + +export const deploymentPipelineSDCPN: { + title: string; + petriNetDefinition: SDCPN; +} = { + title: "Deployment Pipeline", + petriNetDefinition: { + places: [ + { + id: "place__deployment-ready", + name: "DeploymentReady", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: -8 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "place__incident-being-investigated", + name: "IncidentBeingInvestigated", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: -8 * SNAP_GRID_SIZE, + y: 10 * SNAP_GRID_SIZE, + }, + { + id: "place__deployment-in-progress", + name: "DeploymentInProgress", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 25 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "place__completed-deployments", + name: "CompletedDeployments", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 55 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "place__resolved-incidents", + name: "ResolvedIncidents", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 25 * SNAP_GRID_SIZE, + y: 20 * SNAP_GRID_SIZE, + }, + ], + transitions: [ + { + id: "transition__create-deployment", + name: "Create Deployment", + inputArcs: [], + outputArcs: [{ placeId: "place__deployment-ready", weight: 1 }], + lambdaType: "stochastic", + lambdaCode: + "export default Lambda((tokens, parameters) => parameters.deployment_creation_rate)", + transitionKernelCode: + "export default TransitionKernel(() => {\n return {\n DeploymentReady: [{}],\n };\n});", + x: -30 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "transition__incident-raised", + name: "Incident Raised", + inputArcs: [], + outputArcs: [ + { placeId: "place__incident-being-investigated", weight: 1 }, + ], + lambdaType: "stochastic", + lambdaCode: + "export default Lambda((tokens, parameters) => parameters.incident_rate)", + transitionKernelCode: + "export default TransitionKernel(() => {\n return {\n IncidentBeingInvestigated: [{}],\n };\n});", + x: -30 * SNAP_GRID_SIZE, + y: 10 * SNAP_GRID_SIZE, + }, + { + id: "transition__start-deployment", + name: "Start Deployment", + inputArcs: [ + { + placeId: "place__deployment-ready", + weight: 1, + type: "standard", + }, + { + placeId: "place__incident-being-investigated", + weight: 1, + type: "inhibitor", + }, + { + placeId: "place__deployment-in-progress", + weight: 1, + type: "inhibitor", + }, + ], + outputArcs: [{ placeId: "place__deployment-in-progress", weight: 1 }], + lambdaType: "predicate", + lambdaCode: "export default Lambda(() => true)", + transitionKernelCode: + "export default TransitionKernel(() => {\n return {\n DeploymentInProgress: [{}],\n };\n});", + x: 8 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "transition__finish-deployment", + name: "Finish Deployment", + inputArcs: [ + { + placeId: "place__deployment-in-progress", + weight: 1, + type: "standard", + }, + ], + outputArcs: [{ placeId: "place__completed-deployments", weight: 1 }], + lambdaType: "predicate", + lambdaCode: "export default Lambda(() => true)", + transitionKernelCode: + "export default TransitionKernel(() => {\n return {\n CompletedDeployments: [{}],\n };\n});", + x: 40 * SNAP_GRID_SIZE, + y: -10 * SNAP_GRID_SIZE, + }, + { + id: "transition__close-incident", + name: "Close Incident", + inputArcs: [ + { + placeId: "place__incident-being-investigated", + weight: 1, + type: "standard", + }, + ], + outputArcs: [{ placeId: "place__resolved-incidents", weight: 1 }], + lambdaType: "stochastic", + lambdaCode: + "export default Lambda((tokens, parameters) => parameters.incident_resolution_rate)", + transitionKernelCode: + "export default TransitionKernel(() => {\n return {\n ResolvedIncidents: [{}],\n };\n});", + x: 8 * SNAP_GRID_SIZE, + y: 10 * SNAP_GRID_SIZE, + }, + ], + types: [], + differentialEquations: [], + parameters: [ + { + id: "param__deployment_creation_rate", + name: "Deployment Creation Rate", + variableName: "deployment_creation_rate", + type: "real", + defaultValue: "0.5", + }, + { + id: "param__incident_rate", + name: "Incident Rate", + variableName: "incident_rate", + type: "real", + defaultValue: "0.1", + }, + { + id: "param__incident_resolution_rate", + name: "Incident Resolution Rate", + variableName: "incident_resolution_rate", + type: "real", + defaultValue: "0.3", + }, + ], + }, +}; diff --git a/libs/@hashintel/petrinaut/src/examples/satellites-launcher.ts b/libs/@hashintel/petrinaut/src/examples/satellites-launcher.ts index ccbbf01bdc3..54c562c6bda 100644 --- a/libs/@hashintel/petrinaut/src/examples/satellites-launcher.ts +++ b/libs/@hashintel/petrinaut/src/examples/satellites-launcher.ts @@ -119,6 +119,7 @@ export const probabilisticSatellitesSDCPN: { { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", weight: 2, + type: "standard", }, ], outputArcs: [ @@ -172,6 +173,7 @@ export default TransitionKernel((tokens) => { { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", weight: 1, + type: "standard", }, ], outputArcs: [ diff --git a/libs/@hashintel/petrinaut/src/examples/satellites.ts b/libs/@hashintel/petrinaut/src/examples/satellites.ts index 92e9114ceb0..bee786edb94 100644 --- a/libs/@hashintel/petrinaut/src/examples/satellites.ts +++ b/libs/@hashintel/petrinaut/src/examples/satellites.ts @@ -116,6 +116,7 @@ export const satellitesSDCPN: { title: string; petriNetDefinition: SDCPN } = { { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", weight: 2, + type: "standard", }, ], outputArcs: [ @@ -169,6 +170,7 @@ export default TransitionKernel((tokens) => { { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", weight: 1, + type: "standard", }, ], outputArcs: [ diff --git a/libs/@hashintel/petrinaut/src/examples/sir-model.ts b/libs/@hashintel/petrinaut/src/examples/sir-model.ts index 240f7a1760a..12747433f47 100644 --- a/libs/@hashintel/petrinaut/src/examples/sir-model.ts +++ b/libs/@hashintel/petrinaut/src/examples/sir-model.ts @@ -41,10 +41,12 @@ export const sirModel: { title: string; petriNetDefinition: SDCPN } = { { placeId: "place__susceptible", weight: 1, + type: "standard", }, { placeId: "place__infected", weight: 1, + type: "standard", }, ], outputArcs: [ @@ -68,6 +70,7 @@ export const sirModel: { title: string; petriNetDefinition: SDCPN } = { { placeId: "place__infected", weight: 1, + type: "standard", }, ], outputArcs: [ diff --git a/libs/@hashintel/petrinaut/src/examples/supply-chain-stochastic.ts b/libs/@hashintel/petrinaut/src/examples/supply-chain-stochastic.ts index 3c25a8c8940..b78d1dc188f 100644 --- a/libs/@hashintel/petrinaut/src/examples/supply-chain-stochastic.ts +++ b/libs/@hashintel/petrinaut/src/examples/supply-chain-stochastic.ts @@ -77,8 +77,8 @@ export const supplyChainStochasticSDCPN: { id: "transition__0", name: "Deliver to Plant", inputArcs: [ - { placeId: "place__0", weight: 1 }, - { placeId: "place__1", weight: 1 }, + { placeId: "place__0", weight: 1, type: "standard" }, + { placeId: "place__1", weight: 1, type: "standard" }, ], outputArcs: [{ placeId: "place__2", weight: 1 }], lambdaType: "stochastic", @@ -90,7 +90,7 @@ export const supplyChainStochasticSDCPN: { { id: "transition__1", name: "Manufacture", - inputArcs: [{ placeId: "place__2", weight: 1 }], + inputArcs: [{ placeId: "place__2", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__3", weight: 1 }], lambdaType: "stochastic", lambdaCode: "export default Lambda(() => 1);", @@ -108,7 +108,7 @@ export default TransitionKernel(() => { { id: "transition__2", name: "Dispatch", - inputArcs: [{ placeId: "place__3", weight: 1 }], + inputArcs: [{ placeId: "place__3", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__5", weight: 1 }], lambdaType: "predicate", lambdaCode: `// Dispatch if product quality exceeds the quality threshold @@ -123,7 +123,7 @@ export default Lambda((tokens, parameters) => { { id: "transition__3", name: "Dispose", - inputArcs: [{ placeId: "place__3", weight: 1 }], + inputArcs: [{ placeId: "place__3", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__4", weight: 1 }], lambdaType: "predicate", lambdaCode: `// Dispose if product quality is below the quality threshold @@ -138,7 +138,7 @@ export default Lambda((tokens, parameters) => { { id: "transition__4", name: "Ship", - inputArcs: [{ placeId: "place__5", weight: 1 }], + inputArcs: [{ placeId: "place__5", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__6", weight: 1 }], lambdaType: "stochastic", lambdaCode: "export default Lambda(() => 1 / 3);", diff --git a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts b/libs/@hashintel/petrinaut/src/examples/supply-chain.ts index f756ec3dcdc..58c0e51fefa 100644 --- a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts +++ b/libs/@hashintel/petrinaut/src/examples/supply-chain.ts @@ -74,8 +74,8 @@ export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = { id: "transition__0", name: "Deliver to Plant", inputArcs: [ - { placeId: "place__0", weight: 1 }, - { placeId: "place__1", weight: 1 }, + { placeId: "place__0", weight: 1, type: "standard" }, + { placeId: "place__1", weight: 1, type: "standard" }, ], outputArcs: [{ placeId: "place__2", weight: 1 }], lambdaType: "predicate", @@ -87,7 +87,7 @@ export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = { { id: "transition__1", name: "Manufacture", - inputArcs: [{ placeId: "place__2", weight: 1 }], + inputArcs: [{ placeId: "place__2", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__3", weight: 1 }], lambdaType: "predicate", lambdaCode: "export default Lambda(() => true);", @@ -98,7 +98,7 @@ export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = { { id: "transition__2", name: "Quality Check", - inputArcs: [{ placeId: "place__3", weight: 1 }], + inputArcs: [{ placeId: "place__3", weight: 1, type: "standard" }], outputArcs: [ { placeId: "place__5", weight: 1 }, { placeId: "place__4", weight: 1 }, @@ -112,7 +112,7 @@ export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = { { id: "transition__3", name: "Ship", - inputArcs: [{ placeId: "place__5", weight: 1 }], + inputArcs: [{ placeId: "place__5", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place__6", weight: 1 }], lambdaType: "predicate", lambdaCode: "export default Lambda(() => true);", diff --git a/libs/@hashintel/petrinaut/src/file-format/types.ts b/libs/@hashintel/petrinaut/src/file-format/types.ts index a18229bd9f7..da98c2a893c 100644 --- a/libs/@hashintel/petrinaut/src/file-format/types.ts +++ b/libs/@hashintel/petrinaut/src/file-format/types.ts @@ -2,7 +2,13 @@ import { z } from "zod"; export const SDCPN_FILE_FORMAT_VERSION = 1; -const arcSchema = z.object({ +const inputArcSchema = z.object({ + placeId: z.string(), + weight: z.number(), + type: z.enum(["standard", "inhibitor"]).optional().default("standard"), +}); + +const outputArcSchema = z.object({ placeId: z.string(), weight: z.number(), }); @@ -21,8 +27,8 @@ const placeSchema = z.object({ const transitionSchema = z.object({ id: z.string(), name: z.string(), - inputArcs: z.array(arcSchema), - outputArcs: z.array(arcSchema), + inputArcs: z.array(inputArcSchema), + outputArcs: z.array(outputArcSchema), lambdaType: z.enum(["predicate", "stochastic"]), lambdaCode: z.string(), transitionKernelCode: z.string(), diff --git a/libs/@hashintel/petrinaut/src/lib/deep-equal.test.ts b/libs/@hashintel/petrinaut/src/lib/deep-equal.test.ts index 2947827008d..95a8b7cf427 100644 --- a/libs/@hashintel/petrinaut/src/lib/deep-equal.test.ts +++ b/libs/@hashintel/petrinaut/src/lib/deep-equal.test.ts @@ -27,7 +27,7 @@ const sampleNet: SDCPN = { { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p1", weight: 2 }], lambdaType: "predicate", lambdaCode: "return true;", diff --git a/libs/@hashintel/petrinaut/src/lsp/lib/checker.test.ts b/libs/@hashintel/petrinaut/src/lsp/lib/checker.test.ts index 4fdb53700e5..bfcf53261ee 100644 --- a/libs/@hashintel/petrinaut/src/lsp/lib/checker.test.ts +++ b/libs/@hashintel/petrinaut/src/lsp/lib/checker.test.ts @@ -39,7 +39,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "predicate", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 1 }], lambdaCode: `export default Lambda((input, parameters) => { return input.Source[0].value > 0; @@ -223,7 +223,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "predicate", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { const token = input.Source[0]; @@ -250,7 +250,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "predicate", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { const token = input.UndefinedPlace[0]; @@ -283,7 +283,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "predicate", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { return 42; @@ -312,7 +312,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "stochastic", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { return input.Source[0].rate; @@ -342,7 +342,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 1 }], transitionKernelCode: `export default TransitionKernel((input, parameters) => { return { Target: [input.Source[0]] }; @@ -370,7 +370,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 1 }], transitionKernelCode: `export default TransitionKernel((input, parameters) => { return { WrongPlace: [input.Source[0]] }; @@ -401,7 +401,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 2 }], // expects 2 tokens transitionKernelCode: `export default TransitionKernel((input, parameters) => { return { Target: [input.Source[0]] }; @@ -435,7 +435,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 1 }], transitionKernelCode: `export default TransitionKernel((input, parameters) => { const newX = input.Source[0].x * parameters.multiplier; @@ -466,8 +466,8 @@ describe("checkSDCPN", () => { { id: "t1", inputArcs: [ - { placeId: "place1", weight: 1 }, - { placeId: "place2", weight: 1 }, + { placeId: "place1", weight: 1, type: "standard" }, + { placeId: "place2", weight: 1, type: "standard" }, ], outputArcs: [{ placeId: "place3", weight: 1 }], transitionKernelCode: `export default TransitionKernel((input, parameters) => { @@ -505,7 +505,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [ { placeId: "place2", weight: 1 }, { placeId: "place3", weight: 1 }, @@ -540,7 +540,7 @@ describe("checkSDCPN", () => { transitions: [ { id: "t1", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "place2", weight: 1 }], transitionKernelCode: `export default TransitionKernel((input, parameters) => { // Accessing 'nonExistentProperty' should fail since color1 only has 'x' @@ -585,7 +585,7 @@ describe("checkSDCPN", () => { { id: "t1", lambdaType: "predicate", - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { return input.UndefinedPlace[0]; @@ -625,7 +625,7 @@ describe("checkSDCPN", () => { { id: "transitionA", lambdaType: "predicate", - inputArcs: [{ placeId: "placeIn", weight: 1 }], + inputArcs: [{ placeId: "placeIn", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { return input.PlaceIn[0].x > 0; @@ -638,7 +638,7 @@ describe("checkSDCPN", () => { { id: "transitionB", lambdaType: "predicate", - inputArcs: [{ placeId: "placeIn", weight: 1 }], + inputArcs: [{ placeId: "placeIn", weight: 1, type: "standard" }], outputArcs: [], lambdaCode: `export default Lambda((input, parameters) => { return input.PlaceIn[0].x < 100; @@ -673,7 +673,7 @@ describe("checkSDCPN", () => { { id: "transitionA", lambdaType: "predicate", - inputArcs: [{ placeId: "placeIn", weight: 1 }], + inputArcs: [{ placeId: "placeIn", weight: 1, type: "standard" }], outputArcs: [{ placeId: "placeOut", weight: 1 }], lambdaCode: `export default Lambda((input, parameters) => { return input.PlaceIn[0].x > 0; diff --git a/libs/@hashintel/petrinaut/src/lsp/lib/create-sdcpn-language-service.test.ts b/libs/@hashintel/petrinaut/src/lsp/lib/create-sdcpn-language-service.test.ts index 68d0a6e7ccf..f52457dc9a5 100644 --- a/libs/@hashintel/petrinaut/src/lsp/lib/create-sdcpn-language-service.test.ts +++ b/libs/@hashintel/petrinaut/src/lsp/lib/create-sdcpn-language-service.test.ts @@ -110,7 +110,9 @@ describe("SDCPNLanguageServer completions", () => { { id: "t1", lambdaType: "predicate" as const, - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [ + { placeId: "place1", weight: 1, type: "standard" as const }, + ], outputArcs: [{ placeId: "place2", weight: 1 }], lambdaCode: "", transitionKernelCode: "", @@ -290,7 +292,9 @@ describe("SDCPNLanguageServer completions", () => { { id: "t1", lambdaType: "predicate" as const, - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [ + { placeId: "place1", weight: 1, type: "standard" as const }, + ], outputArcs: [], lambdaCode: "", }, @@ -314,7 +318,9 @@ describe("SDCPNLanguageServer completions", () => { { id: "t1", lambdaType: "predicate" as const, - inputArcs: [{ placeId: "place1", weight: 1 }], + inputArcs: [ + { placeId: "place1", weight: 1, type: "standard" as const }, + ], outputArcs: [], lambdaCode: `export default Lambda((input) => {\n const t = input.Source[0];\n return t.`, }, diff --git a/libs/@hashintel/petrinaut/src/lsp/lib/generate-virtual-files.ts b/libs/@hashintel/petrinaut/src/lsp/lib/generate-virtual-files.ts index 3e2aa7006fa..a10e96e166b 100644 --- a/libs/@hashintel/petrinaut/src/lsp/lib/generate-virtual-files.ts +++ b/libs/@hashintel/petrinaut/src/lsp/lib/generate-virtual-files.ts @@ -120,6 +120,11 @@ export function generateVirtualFiles(sdcpn: SDCPN): Map { const inputTypeProperties: string[] = []; for (const arc of transition.inputArcs) { + // Inhibitor arcs never deliver tokens to the transition, so they should + // not contribute to the input type. + if (arc.type === "inhibitor") { + continue; + } const place = placeById.get(arc.placeId); if (!place?.colorId) { continue; diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/build-simulation.test.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/build-simulation.test.ts index b06c6fbb6e3..37d15adaed0 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/build-simulation.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/build-simulation.test.ts @@ -158,7 +158,7 @@ describe("buildSimulation", () => { { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "export default Lambda((tokens) => { return 1.0; });", @@ -170,7 +170,7 @@ describe("buildSimulation", () => { { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p2", weight: 1 }], + inputArcs: [{ placeId: "p2", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p3", weight: 1 }], lambdaType: "stochastic", lambdaCode: "export default Lambda((tokens) => { return 2.0; });", diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.test.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.test.ts index f82115f3983..19848eec179 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.test.ts @@ -22,7 +22,7 @@ describe("isTransitionStructurallyEnabled", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -56,7 +56,7 @@ describe("isTransitionStructurallyEnabled", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -90,7 +90,7 @@ describe("isTransitionStructurallyEnabled", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 3 }], // Requires 3 tokens + inputArcs: [{ placeId: "p1", weight: 3, type: "standard" }], // Requires 3 tokens outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -131,8 +131,8 @@ describe("isTransitionStructurallyEnabled", () => { id: "t1", name: "Transition 1", inputArcs: [ - { placeId: "p1", weight: 1 }, - { placeId: "p2", weight: 1 }, + { placeId: "p1", weight: 1, type: "standard" }, + { placeId: "p2", weight: 1, type: "standard" }, ], outputArcs: [], lambdaType: "stochastic", @@ -153,6 +153,232 @@ describe("isTransitionStructurallyEnabled", () => { expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(false); }); + it("returns true for inhibitor arc when place has fewer tokens than weight", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 1, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // 1 token < weight 2, so inhibitor condition is satisfied + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(true); + }); + + it("returns false for inhibitor arc when place has enough tokens", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 3, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // 3 tokens >= weight 2, so inhibitor condition is NOT satisfied + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(false); + }); + + it("returns false for inhibitor arc when place has exactly the weight in tokens", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 2, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // 2 tokens is NOT < weight 2, so inhibitor condition is NOT satisfied + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(false); + }); + + it("returns true for inhibitor arc when place is empty", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 0, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [{ placeId: "p1", weight: 1, type: "inhibitor" }], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // 0 tokens < weight 1, inhibitor condition satisfied + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(true); + }); + + it("checks mixed standard and inhibitor arcs together", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 2, + dimensions: 0, + }, + p2: { + offset: 0, + count: 0, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [ + { placeId: "p1", weight: 1, type: "standard" }, + { placeId: "p2", weight: 1, type: "inhibitor" }, + ], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // p1 has 2 >= 1 (standard satisfied), p2 has 0 < 1 (inhibitor satisfied) + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(true); + }); + + it("returns false when standard arc is satisfied but inhibitor arc is not", () => { + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 2, + dimensions: 0, + }, + p2: { + offset: 0, + count: 3, + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [ + { placeId: "p1", weight: 1, type: "standard" }, + { placeId: "p2", weight: 1, type: "inhibitor" }, + ], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // p1 has 2 >= 1 (standard satisfied), but p2 has 3 >= 1 (inhibitor NOT satisfied) + expect(isTransitionStructurallyEnabled(frame, "t1")).toBe(false); + }); + it("returns true for transitions with no input arcs", () => { const frame: SimulationFrame = { time: 0, @@ -203,7 +429,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -219,7 +445,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p2", weight: 1 }], + inputArcs: [{ placeId: "p2", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -262,7 +488,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -278,7 +504,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p2", weight: 1 }], + inputArcs: [{ placeId: "p2", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -333,7 +559,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -349,7 +575,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p1", weight: 2 }], + inputArcs: [{ placeId: "p1", weight: 2, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -365,7 +591,7 @@ describe("checkTransitionEnablement", () => { instance: { id: "t3", name: "Transition 3", - inputArcs: [{ placeId: "p1", weight: 5 }], + inputArcs: [{ placeId: "p1", weight: 5, type: "standard" }], outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.ts index a4c7dce0f7b..94ef6532ce8 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/check-transition-enablement.ts @@ -47,7 +47,9 @@ export const isTransitionStructurallyEnabled = ( ); } - return placeState.count >= arc.weight; + return arc.type === "inhibitor" + ? placeState.count < arc.weight + : placeState.count >= arc.weight; }); }; diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-next-frame.test.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-next-frame.test.ts index 3780808e6c5..6019ac0e8ff 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-next-frame.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-next-frame.test.ts @@ -44,7 +44,7 @@ describe("computeNextFrame", () => { { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p1", weight: 1 }], lambdaType: "stochastic", lambdaCode: diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.test.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.test.ts index 9ad5ac6b424..a6ed2af6b54 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.test.ts @@ -35,7 +35,7 @@ describe("computePossibleTransition", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 2 }], // Requires 2 tokens + inputArcs: [{ placeId: "p1", weight: 2, type: "standard" }], // Requires 2 tokens outputArcs: [], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -58,6 +58,181 @@ describe("computePossibleTransition", () => { expect(result).toBeNull(); }); + it("returns null when inhibitor arc condition is not met (place has enough tokens)", () => { + // GIVEN a frame where the inhibitor place has enough tokens to block the transition + const simulation: SimulationInstance = { + places: new Map(), + transitions: new Map(), + types: new Map(), + differentialEquationFns: new Map(), + lambdaFns: new Map([["t1", () => 1.0]]), + transitionKernelFns: new Map([["t1", () => ({})]]), + parameterValues: {}, + dt: 0.1, + maxTime: null, + rngState: 42, + frames: [], + currentFrameNumber: 0, + }; + + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 2, // 2 tokens present + dimensions: 0, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [{ placeId: "p1", weight: 2, type: "inhibitor" }], // Inhibitor: needs count < 2 + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "return 1.0;", + transitionKernelCode: "return {};", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 1.0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([]), + }; + + // WHEN computing possible transition + const result = computePossibleTransition(frame, simulation, "t1", 42); + + // THEN it should return null (inhibitor condition not met: 2 is not < 2) + expect(result).toBeNull(); + }); + + it("does not consume tokens from inhibitor arc when transition fires", () => { + // GIVEN a frame with a standard arc and an inhibitor arc, both conditions met + const simulation: SimulationInstance = { + places: new Map([ + [ + "p1", + { + id: "p1", + name: "Source", + colorId: "type1", + dynamicsEnabled: false, + differentialEquationId: null, + x: 0, + y: 0, + }, + ], + [ + "p2", + { + id: "p2", + name: "Guard", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 0, + y: 0, + }, + ], + [ + "p3", + { + id: "p3", + name: "Target", + colorId: "type1", + dynamicsEnabled: false, + differentialEquationId: null, + x: 0, + y: 0, + }, + ], + ]), + transitions: new Map(), + types: new Map([ + [ + "type1", + { + id: "type1", + name: "Type1", + iconSlug: "circle", + displayColor: "#FF0000", + elements: [{ elementId: "e1", name: "x", type: "real" }], + }, + ], + ]), + differentialEquationFns: new Map(), + lambdaFns: new Map([["t1", () => 10.0]]), + transitionKernelFns: new Map([["t1", () => ({ Target: [{ x: 5.0 }] })]]), + parameterValues: {}, + dt: 0.1, + maxTime: null, + rngState: 42, + frames: [], + currentFrameNumber: 0, + }; + + const frame: SimulationFrame = { + time: 0, + places: { + p1: { + offset: 0, + count: 1, + dimensions: 1, + }, + p2: { + offset: 1, + count: 0, // Empty — inhibitor condition satisfied (0 < 1) + dimensions: 0, + }, + p3: { + offset: 1, + count: 0, + dimensions: 1, + }, + }, + transitions: { + t1: { + instance: { + id: "t1", + name: "Transition 1", + inputArcs: [ + { placeId: "p1", weight: 1, type: "standard" }, + { placeId: "p2", weight: 1, type: "inhibitor" }, + ], + outputArcs: [{ placeId: "p3", weight: 1 }], + lambdaType: "stochastic", + lambdaCode: "return 10.0;", + transitionKernelCode: "return { Target: [{ x: 5.0 }] };", + x: 0, + y: 0, + }, + timeSinceLastFiringMs: 1.0, + firedInThisFrame: false, + firingCount: 0, + }, + }, + buffer: new Float64Array([3.0]), + }; + + // WHEN computing possible transition + const result = computePossibleTransition(frame, simulation, "t1", 42); + + // THEN it should fire + expect(result).not.toBeNull(); + // Standard arc's place (p1) should have tokens removed + expect(result!.remove).toHaveProperty("p1"); + // Inhibitor arc's place (p2) should NOT be in the remove map + expect(result!.remove).not.toHaveProperty("p2"); + // Output tokens should be added to p3 + expect(result!.add).toMatchObject({ p3: [[5.0]] }); + }); + it("returns token combinations when transition is enabled and fires", () => { // GIVEN a frame with sufficient tokens and favorable random conditions const simulation: SimulationInstance = { @@ -140,7 +315,7 @@ describe("computePossibleTransition", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], // Requires 1 token + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], // Requires 1 token outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.ts index 2a2a12840de..ce508803983 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/compute-possible-transition.ts @@ -40,12 +40,19 @@ export function computePossibleTransition( ); } - return { ...placeState, placeId: arc.placeId, weight: arc.weight }; + return { + ...placeState, + placeId: arc.placeId, + weight: arc.weight, + type: arc.type, + }; }); // Transition is enabled if all input places have more tokens than the arc weight. - const isTransitionEnabled = inputPlaces.every( - (inputPlace) => inputPlace.count >= inputPlace.weight, + const isTransitionEnabled = inputPlaces.every((inputPlace) => + inputPlace.type === "inhibitor" + ? inputPlace.count < inputPlace.weight + : inputPlace.count >= inputPlace.weight, ); // Return null if not enabled @@ -81,10 +88,10 @@ export function computePossibleTransition( // (just multiply by time since last transition) const inputPlacesWithAtLeastOneDimension = inputPlaces.filter( - (place) => place.dimensions > 0, + (place) => place.dimensions > 0 && place.type !== "inhibitor", ); const inputPlacesWithZeroDimensions = inputPlaces.filter( - (place) => place.dimensions === 0, + (place) => place.dimensions === 0 && place.type !== "inhibitor", ); // TODO: This should acumulate lambda over time, but for now we just consider that lambda is constant per combination. @@ -283,9 +290,10 @@ export function computePossibleTransition( // TODO: Need to provide better typing here, to not let TS infer to any[] // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment remove: Object.fromEntries([ - ...inputPlacesWithZeroDimensions.map((inputPlace) => { - return [inputPlace.placeId, inputPlace.weight]; - }), + ...inputPlacesWithZeroDimensions.map((inputPlace) => [ + inputPlace.placeId, + inputPlace.weight, + ]), ...tokenCombinationIndices.map((placeTokenIndices, placeIndex) => { const inputArc = inputPlacesWithAtLeastOneDimension[placeIndex]!; return [inputArc.placeId, new Set(placeTokenIndices)]; diff --git a/libs/@hashintel/petrinaut/src/simulation/simulator/execute-transitions.test.ts b/libs/@hashintel/petrinaut/src/simulation/simulator/execute-transitions.test.ts index 89a999e023e..34876fb24ac 100644 --- a/libs/@hashintel/petrinaut/src/simulation/simulator/execute-transitions.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/simulator/execute-transitions.test.ts @@ -34,7 +34,7 @@ describe("executeTransitions", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 1.0;", @@ -134,7 +134,7 @@ describe("executeTransitions", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", @@ -270,7 +270,7 @@ describe("executeTransitions", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", @@ -286,7 +286,7 @@ describe("executeTransitions", () => { instance: { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p3", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", @@ -406,7 +406,7 @@ describe("executeTransitions", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", @@ -518,7 +518,7 @@ describe("executeTransitions", () => { instance: { id: "t1", name: "Transition 1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 10.0;", @@ -534,7 +534,7 @@ describe("executeTransitions", () => { instance: { id: "t2", name: "Transition 2", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "stochastic", lambdaCode: "return 0.001;", diff --git a/libs/@hashintel/petrinaut/src/state/mutation-context.ts b/libs/@hashintel/petrinaut/src/state/mutation-context.ts index b0d981c2df3..6f2a6106baa 100644 --- a/libs/@hashintel/petrinaut/src/state/mutation-context.ts +++ b/libs/@hashintel/petrinaut/src/state/mutation-context.ts @@ -29,21 +29,26 @@ export type MutationHelperFunctions = { removeTransition: (transitionId: string) => void; addArc: ( transitionId: string, - arcType: "input" | "output", + arcDirection: "input" | "output", placeId: string, weight: number, ) => void; removeArc: ( transitionId: string, - arcType: "input" | "output", + arcDirection: "input" | "output", placeId: string, ) => void; updateArcWeight: ( transitionId: string, - arcType: "input" | "output", + arcDirection: "input" | "output", placeId: string, weight: number, ) => void; + updateArcType: ( + transitionId: string, + placeId: string, + type: "standard" | "inhibitor", + ) => void; addType: (type: Color) => void; updateType: (typeId: string, updateFn: (type: Color) => void) => void; removeType: (typeId: string) => void; @@ -85,6 +90,7 @@ const DEFAULT_CONTEXT_VALUE: MutationContextValue = { addArc: () => {}, removeArc: () => {}, updateArcWeight: () => {}, + updateArcType: () => {}, addType: () => {}, updateType: () => {}, removeType: () => {}, diff --git a/libs/@hashintel/petrinaut/src/state/mutation-provider.test.tsx b/libs/@hashintel/petrinaut/src/state/mutation-provider.test.tsx index e7429dbadcf..400f6a46e0d 100644 --- a/libs/@hashintel/petrinaut/src/state/mutation-provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/state/mutation-provider.test.tsx @@ -435,7 +435,7 @@ describe("MutationProvider", () => { { id: "t1", name: "T1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "predicate", lambdaCode: "", @@ -555,7 +555,7 @@ describe("MutationProvider", () => { { id: "t1", name: "T1", - inputArcs: [{ placeId: "p1", weight: 1 }], + inputArcs: [{ placeId: "p1", weight: 1, type: "standard" }], outputArcs: [{ placeId: "p2", weight: 1 }], lambdaType: "predicate", lambdaCode: "", diff --git a/libs/@hashintel/petrinaut/src/state/mutation-provider.tsx b/libs/@hashintel/petrinaut/src/state/mutation-provider.tsx index e5514bf3961..cedae7b9722 100644 --- a/libs/@hashintel/petrinaut/src/state/mutation-provider.tsx +++ b/libs/@hashintel/petrinaut/src/state/mutation-provider.tsx @@ -122,29 +122,34 @@ export const MutationProvider: React.FC = ({ } }); }, - addArc(transitionId, arcType, placeId, weight) { + addArc(transitionId, arcDirection, placeId, weight) { guardedMutate((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === transitionId) { - transition[arcType === "input" ? "inputArcs" : "outputArcs"].push({ - placeId, - weight, - }); + if (arcDirection === "input") { + transition["inputArcs"].push({ + type: "standard", + placeId, + weight, + }); + } else { + transition["outputArcs"].push({ placeId, weight }); + } break; } } }); }, - removeArc(transitionId, arcType, placeId) { + removeArc(transitionId, arcDirection, placeId) { guardedMutate((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === transitionId) { for (const [index, arc] of transition[ - arcType === "input" ? "inputArcs" : "outputArcs" + arcDirection === "input" ? "inputArcs" : "outputArcs" ].entries()) { if (arc.placeId === placeId) { transition[ - arcType === "input" ? "inputArcs" : "outputArcs" + arcDirection === "input" ? "inputArcs" : "outputArcs" ].splice(index, 1); break; } @@ -154,12 +159,12 @@ export const MutationProvider: React.FC = ({ } }); }, - updateArcWeight(transitionId, arcType, placeId, weight) { + updateArcWeight(transitionId, arcDirection, placeId, weight) { guardedMutate((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === transitionId) { for (const arc of transition[ - arcType === "input" ? "inputArcs" : "outputArcs" + arcDirection === "input" ? "inputArcs" : "outputArcs" ]) { if (arc.placeId === placeId) { arc.weight = weight; @@ -171,6 +176,21 @@ export const MutationProvider: React.FC = ({ } }); }, + updateArcType(transitionId, placeId, type) { + guardedMutate((sdcpn) => { + for (const transition of sdcpn.transitions) { + if (transition.id === transitionId) { + for (const arc of transition["inputArcs"]) { + if (arc.placeId === placeId) { + arc.type = type; + break; + } + } + break; + } + } + }); + }, addType(type) { guardedMutate((sdcpn) => { sdcpn.types.push(type); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx index 5aaf5a74bef..c4860a2d6d4 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx @@ -4,6 +4,7 @@ import { use, useRef, useState } from "react"; import { Box } from "../../components/box"; import { Stack } from "../../components/stack"; import { productionMachines } from "../../examples/broken-machines"; +import { deploymentPipelineSDCPN } from "../../examples/deployment-pipeline"; import { satellitesSDCPN } from "../../examples/satellites"; import { probabilisticSatellitesSDCPN } from "../../examples/satellites-launcher"; import { sirModel } from "../../examples/sir-model"; @@ -318,6 +319,14 @@ export const EditorView = ({ clearSelection(); }, }, + { + id: "load-example-deployment-pipeline", + label: "Deployment Pipeline", + onClick: () => { + createNewNet(deploymentPipelineSDCPN); + clearSelection(); + }, + }, ], }, ] diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx index 4e15f057c0f..f828f111889 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx @@ -6,6 +6,7 @@ import { TbTrash } from "react-icons/tb"; import { IconButton } from "../../../../../components/icon-button"; import { NumberInput } from "../../../../../components/number-input"; import { Section, SectionList } from "../../../../../components/section"; +import { Select } from "../../../../../components/select"; import type { SubView } from "../../../../../components/sub-view/types"; import { VerticalSubViewsContainer } from "../../../../../components/sub-view/vertical/vertical-sub-views-container"; import { UI_MESSAGES } from "../../../../../constants/ui-messages"; @@ -31,19 +32,25 @@ interface ArcPropertiesData { arcId: string; transitionId: string; placeId: string; - arcType: "input" | "output"; + arcDirection: "input" | "output"; sourceName: string; targetName: string; weight: number; + type: "standard" | "inhibitor"; updateArcWeight: ( transitionId: string, - arcType: "input" | "output", + arcDirection: "input" | "output", placeId: string, weight: number, ) => void; + updateArcType: ( + transitionId: string, + placeId: string, + type: "standard" | "inhibitor", + ) => void; removeArc: ( transitionId: string, - arcType: "input" | "output", + arcDirection: "input" | "output", placeId: string, ) => void; } @@ -64,11 +71,13 @@ const ArcMainContent: React.FC = () => { const { transitionId, placeId, - arcType, + arcDirection, sourceName, targetName, weight, + type, updateArcWeight, + updateArcType, } = useArcPropertiesContext(); const isReadOnly = useIsReadOnly(); @@ -80,6 +89,26 @@ const ArcMainContent: React.FC = () => {
{targetName}
+ {arcDirection === "input" && ( +
+