diff --git a/libs/@hashintel/petrinaut/docs/README.md b/libs/@hashintel/petrinaut/docs/README.md new file mode 100644 index 00000000000..a7295f4b46b --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/README.md @@ -0,0 +1,20 @@ +# Petrinaut User Guide + +Petrinaut is a visual editor for [Petri nets](https://en.wikipedia.org/wiki/Petri_net). + +It lets you build, configure, and simulate Petri nets. It has support for various extensions including typed tokens (colours), continuous dynamics, and stochastic transitions. + +## Live site + +Petrinaut is available at [demo.petrinaut.org](https://demo.petrinaut.org). + +Net data will be stored in local browser storage. You can also export and import nets as JSON files for transfer between devices and browsers. + +## Contents + +- [Drawing a Net](drawing-a-net.md) -- Add nodes (places and transitions), connect them with arcs, and navigate the editor. +- [Petri Net Extensions](petri-net-extensions.md) -- Add types, dynamics, transition kernels, firing rules, and inhibitor arcs, as well as parameters and state visualizers. +- [Useful Patterns](useful-patterns.md) -- Common modelling techniques, including duration and resource pools. +- [Simulation](simulation.md) -- Set initial state, run the simulation, use the timeline, and control playback. +- [Visual Settings](visual-settings.md) -- Configure the editor appearance and behavior. +- [Examples](examples.md) -- Walkthrough of the built-in example nets. diff --git a/libs/@hashintel/petrinaut/docs/drawing-a-net.md b/libs/@hashintel/petrinaut/docs/drawing-a-net.md new file mode 100644 index 00000000000..547e6377d7a --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/drawing-a-net.md @@ -0,0 +1,122 @@ +# Drawing a Net + +## Editor layout + +The editor is organized around a central canvas where you build your net: + +- **Canvas** (center) -- the main workspace where places and transitions are displayed and connected. +- **Left sidebar** -- lists of entities organized into tabs: Nodes, Types, Differential Equations, Parameters. +- **Properties panel** (right) -- opens when you select an entity, showing its configurable properties. +- **Bottom panel** -- tabs for Diagnostics (code errors), Simulation Settings, and Timeline (during simulation). +- **Bottom toolbar** -- editing mode buttons and simulation controls (+ show/hide toggle for bottom panel). + +full-editor + +## Adding places and transitions + +Use the bottom toolbar to add nodes: + +- **Add Place** (shortcut: **N**) -- click the canvas to drop a place, or click and drag the button onto the canvas. +- **Add Transition** (shortcut: **T**) -- click the canvas to drop a transition, or drag the button onto the canvas. + +New nodes are named automatically (Place1, Place2, Transition1, etc.). Rename them by selecting the node and editing the name in the properties panel. + +add-place-transition-toolbar + +## Connecting with arcs + +Drag from a node's handle to connect it: + +- **Place to Transition** creates an **input arc** (the transition consumes tokens from the place). +- **Transition to Place** creates an **output arc** (the transition produces tokens in the place). + +Petri nets are bipartite: you cannot connect a place to another place or a transition to another transition. New arcs default to weight 1. + +![drawing-arc](https://github.com/user-attachments/assets/ac688560-bba8-44fe-a6f8-c7ff320474a4) + +## Arc weight + +Select an arc to open its properties. Set the **weight** to control how many tokens are consumed (input) or produced (output) per firing. + +You can also edit an arc's weight via the properties panel for the transition it is connected to. + +See also: [arc weight for multi-token operations](useful-patterns.md#arc-weight-for-multi-token-operations). + +## Pan and Select modes + +The editor has two cursor modes, toggled from the bottom toolbar dropdown: + +| Mode | Shortcut | Behavior | +| ---------- | -------- | ------------------------------------------------------ | +| **Pan** | H | Click and drag to pan the canvas. This is the default. | +| **Select** | V | Click and drag to draw a selection box around nodes. | + +With a selection, you can: + +- **Move** -- drag selected nodes to reposition them. +- **Delete** -- press **Backspace** or **Delete**. +- **Copy** -- **Cmd+C** (Mac) / **Ctrl+C** (Windows/Linux). +- **Paste** -- **Cmd+V** / **Ctrl+V**. + +Whether a node must be fully inside or only partially inside the selection box is configurable in [visual settings](visual-settings.md). + +selection + +## Left sidebar + +The left sidebar has four tabs for creating and managing entities: + +| Tab | Contents | +| -------------------------- | --------------------------------------------------------------------- | +| **Nodes** | All places and transitions. Click to select and open properties. | +| **Types** | Token types (colours). Click **+** to create a new type. | +| **Differential Equations** | ODE definitions for continuous dynamics. Click **+** to create. | +| **Parameters** | Global parameters available in all user code. Click **+** to create. | + +Toggle the sidebar with the button in the top-left corner. + +## Search + +Press **Cmd+F** / **Ctrl+F** to open a search bar. Type to filter entities by name. Press **Escape** to close. + +## Undo / Redo + +Use the **Cmd+Z** / **Ctrl+Z** shortcut to undo the last action. Use the **Cmd+Shift+Z** / **Ctrl+Shift+Z** shortcut to redo the last action. + +The recent history is displayed in the top-right corner. Click on a history entry to go back to that state. + +## Keyboard shortcuts + +| Shortcut | Action | +| -------------------- | --------------------------------------- | +| N | Add Place mode | +| T | Add Transition mode | +| H | Pan mode | +| V | Select mode | +| Escape | Clear selection, return to cursor mode | +| Cmd+A | Select all places and transitions | +| Cmd+C | Copy selection | +| Cmd+V | Paste | +| Cmd+Z | Undo | +| Cmd+Shift+Z | Redo | +| Cmd+F | Search | +| Delete / Backspace | Delete selection | + +On Windows/Linux, use Ctrl instead of Cmd. + +## Snap to grid + +When enabled, node positions snap to a grid when placing or dragging. Toggle this in [visual settings](visual-settings.md). + +## Import and export + +From the hamburger menu (top-left): + +- **Export as JSON** -- saves the full net definition including positions and visual styling. +- **Export as JSON without visual info** -- strips node positions and type display colours. Useful for sharing the logical structure only. +- **Export as TikZ** -- generates a `.tex` file with a structural diagram. This is a simplified view: no colours, inhibitor arcs, dynamics, or token types are encoded. Intended for papers and presentations. +- **Import from JSON** -- loads a net from a `.json` file. If node positions are missing, an automatic layout is applied. + +## Auto-layout + +From the hamburger menu, select **Layout** to apply an automatic graph layout (ELK) that rearranges all nodes. Useful after importing a net without positions or when a net has become cluttered. This will not always be an improvement! diff --git a/libs/@hashintel/petrinaut/docs/examples.md b/libs/@hashintel/petrinaut/docs/examples.md new file mode 100644 index 00000000000..e70d1fcc745 --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/examples.md @@ -0,0 +1,118 @@ +# Examples + +Petrinaut includes several built-in example nets accessible from the **hamburger menu** (top-left) under **Load example**. They are listed below from simplest to most complex. + +## SIR Epidemic Model + +The classic Susceptible-Infected-Recovered compartmental model from epidemiology, implemented as a stochastic Petri net. + +**Demonstrates:** + +- **Stochastic firing rates** controlled by [global parameters](petri-net-extensions.md#global-parameters) (`infection_rate`, `recovery_rate`). +- **Arc weight > 1** -- the Infection transition consumes 1 Susceptible + 1 Infected and produces 2 Infected tokens, modelling the S+I -> 2I mass-action dynamics. +- Simple parameter-driven lambdas: `parameters.infection_rate` and `parameters.recovery_rate`. + +**Suggested initial state:** set Susceptible to **100** tokens, Infected to **1**, Recovered to **0**. All places are untyped, so you just set token counts. Press Play and watch the epidemic curve in the timeline. + +**Key concepts:** [stochastic firing](petri-net-extensions.md#stochastic-rate), [parameters](petri-net-extensions.md#global-parameters), [arc weight](useful-patterns.md#arc-weight-for-multi-token-operations). + +SIR + +## Supply Chain (Stochastic) + +A manufacturing pipeline from raw material suppliers through manufacturing, quality assurance, and shipping to a hospital. Products have a random `quality` attribute that determines whether they pass QA. + +**Demonstrates:** + +- **Typed place** -- QAQueue has a "Product" type with a `quality` dimension. +- **`Distribution.Uniform`** in a transition kernel to sample random quality at manufacturing time. +- **Competing predicate transitions** -- "Dispatch" fires when `quality >= threshold`, "Dispose" fires when `quality < threshold`, routing tokens to different output places. +- **Parameter-driven guard** -- the `quality_threshold` parameter controls the pass/fail boundary. + +**Suggested initial state:** set PlantASupply and PlantBSupply to **10** tokens each. Everything else starts empty. The stochastic "Deliver to Plant" transition will begin consuming from both suppliers once the simulation starts. + +**Key concepts:** [types](petri-net-extensions.md#typed-vs-untyped-places), [distributions](petri-net-extensions.md#distributions), [competing transitions](useful-patterns.md#competing-transitions--routing). + +probabilitic-supply-chain + +## Deployment Pipeline + +A software deployment process with incident handling. Deployments are created at a stochastic rate and proceed through a pipeline, but are blocked by incidents. + +**Demonstrates:** + +- **Inhibitor arcs** -- "Start Deployment" has inhibitor arcs from "IncidentBeingInvestigated" and "DeploymentInProgress", preventing new deployments while an incident is open or another deployment is running. +- **Source transitions** -- "Create Deployment" and "Incident Raised" have no input arcs, modelling Poisson arrivals at configurable rates. +- **Stochastic rates from parameters** -- `deployment_creation_rate`, `incident_rate`, `incident_resolution_rate`. + +**Suggested initial state:** no initial tokens needed -- all places can start empty. The source transitions "Create Deployment" and "Incident Raised" generate tokens at their stochastic rates. Just press Play. + +**Key concepts:** [inhibitor arcs](petri-net-extensions.md#inhibitor-arcs), [source transitions](useful-patterns.md#source-transitions-exogenous-arrivals), [mutual exclusion](useful-patterns.md#mutual-exclusion-with-inhibitor-arcs). + +deployment-pipeline + +## Production Machines + +A manufacturing system where machines produce goods, accumulate damage, break down, and are repaired by travelling technicians. + +**Demonstrates:** + +- **Multiple typed places** with three different types: Machine (`machine_damage_ratio`), MachineProducingProduct (`machine_damage_ratio`, `transformation_progress`), and Technician (`distance_to_site`). +- **Differential equations** on three places: production progress advancing, damage being repaired, and technicians travelling to the repair site. +- **Predicate guards** based on continuous state -- production completes when `transformation_progress >= 1`, repair finishes when `machine_damage_ratio <= 0`, technician arrives when `distance_to_site <= 0`. +- **Competing outcomes** -- "Production Success" (predicate) vs "Machine Fail" (stochastic with rate `machine_damage_ratio ** 100`, increasing sharply with accumulated damage). + +**Suggested initial state:** + +| Place | Tokens | Values | +| ----------------- | ------ | ------------------------------ | +| RawMaterial | 100 | (untyped) | +| AvailableMachines | 3 | `machine_damage_ratio: 0` each | + +All other places start empty. "Start Production" will immediately consume a raw material and an available machine to begin. + +**Key concepts:** [dynamics](petri-net-extensions.md#differential-equations-dynamics), [resource pools](useful-patterns.md#resource-pools), predicate vs stochastic on competing transitions. + +production-machines + +## Satellites in Orbit + +An orbital mechanics simulation with satellites orbiting Earth, subject to collision and crash events. + +**Demonstrates:** + +- **Continuous dynamics** -- gravitational ODE computes acceleration, updating satellite position (`x`, `y`) and motion (`direction`, `velocity`) each step. +- **Custom place visualization** -- an SVG visualizer renders Earth, satellite positions, and velocity vectors in the properties panel. +- **Predicate transitions based on geometry** -- "Collision" checks distance between two satellites, "Crash" checks distance from Earth's surface. +- **Arc weight 2** on the "Collision" transition -- requires two satellites from the same place to evaluate pairwise proximity. +- **Parameters** for physical constants: `earth_radius`, `satellite_radius`, `gravitational_constant`, `crash_threshold`. + +**Suggested initial state:** add 3--5 satellite tokens to the Space place. Position them in a rough orbit around the origin (Earth is at 0,0). For example: + +| x | y | direction | velocity | +| --- | ---- | --------- | -------- | +| 80 | 0 | 1.57 | 70 | +| 0 | 100 | 3.14 | 55 | +| -60 | -60 | 0.78 | 80 | + +The velocity needed for a roughly circular orbit at radius `r` is approximately `sqrt(gravitational_constant / r)`. With the default `gravitational_constant` of 400000, that's about 71 at radius 80. Select the Space place and open the visualizer preview to watch the orbits. + +**Key concepts:** [dynamics](petri-net-extensions.md#differential-equations-dynamics), [visualizers](petri-net-extensions.md#visualizer), [arc weight](useful-patterns.md#arc-weight-for-multi-token-operations). + +satellites + +## Probabilistic Satellites Launcher + +Extends the Satellites example with ongoing satellite launches at a stochastic rate. + +**Demonstrates:** + +- **Source transition** with stochastic rate -- "LaunchSatellite" has no inputs and fires at a constant rate, injecting new satellites into orbit. +- **`Distribution.Uniform` and `Distribution.Gaussian`** in the launch kernel for randomized initial conditions. +- **`Distribution.map()`** for coordinate conversion -- a uniform angle is sampled once, then `.map()` derives both `x` (cosine) and `y` (sine) from the same underlying sample for coherent polar-to-cartesian conversion. + +**Suggested initial state:** no initial tokens needed -- start with all places empty. The "LaunchSatellite" source transition fires at a rate of 1 per second, creating satellites with randomized orbital positions and velocities. Just press Play and watch the Space visualizer fill up. + +**Key concepts:** [source transitions](useful-patterns.md#source-transitions-exogenous-arrivals), [distributions and `.map()`](petri-net-extensions.md#distributions). + +probabilistic-satellites diff --git a/libs/@hashintel/petrinaut/docs/petri-net-extensions.md b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md new file mode 100644 index 00000000000..2c5c3591ec6 --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md @@ -0,0 +1,181 @@ +# Petri Net Extensions + +Petrinaut extends basic Petri nets with typed tokens, continuous dynamics, stochastic firing, and more. This page covers each extension. + +## Typed vs untyped places + +By default, places hold **untyped tokens** -- they only track a count. Tokens are indistinguishable from each other. This is sufficient for simple flow models. + +To give tokens structure, assign a **type** to a place. Each token then carries named dimensions (e.g. `x`, `y`, `velocity`), enabling dynamics, visualization, and data-dependent transition logic. + +**To create a type:** + +1. Open the **Token Types** tab in the left sidebar. +2. Click **+** to add a new type. +3. Give it a **name** and **display colour**. + +token-type + +**To assign a type to a place:** select the place, then choose the type from the **Accepted token type** dropdown in the properties panel. + +Once a place has a type, its tokens are accessible in code as structured objects. For example, a type with dimensions `x` and `y` means each token is `{ x: number, y: number }`. + +## Global parameters + +Parameters are named values available in all user-authored code: dynamics, firing rate, kernels, and visualizers. They are accessed via the `parameters` argument. + +**To create a parameter:** + +1. Open the **Parameters** tab in the left sidebar. +2. Click **+** to add a new parameter. +3. Set a **name** (display label), **variable name** (used in code), and **default value** (can be overridden in the simulation settings). + +parameters-SIR + +Override parameter values before running a simulation in the **Simulation Settings** panel (see [Simulation](simulation.md#simulation-settings)). This lets you experiment with different values without editing code. + +**Example:** the [SIR Epidemic Model](examples.md#sir-epidemic-model) defines `infection_rate` and `recovery_rate` as parameters, used in its transition lambdas. + +## Differential equations (dynamics) + +Differential equations define how token data evolves continuously over time. They are integrated at each simulation step using the Euler method. + +**Setup:** + +1. Create a differential equation in the **Differential Equations** tab (left sidebar). +2. Give it a name and associate it with a **type** (the equation applies to tokens of that type). +3. Select a place, enable **Dynamics**, and choose an equation that matches the type assigned to the place. + +**Function signature:** + +```ts +export default Dynamics((tokens, parameters) => { + return tokens.map(({ x, y }) => { + return { x: /* dx/dt */, y: /* dy/dt */ }; + }); +}); +``` + +The function receives the current token values and global parameters. It must return an array of derivative objects -- one per token, with the same dimension names. + +diff-equations + +**Example:** in [Satellites in Orbit](examples.md#satellites-in-orbit), the orbital dynamics equation computes gravitational acceleration to update satellite position and velocity each step. + +## Visualizer + +A visualizer renders a custom view of a place's tokens during simulation. It is a React component that returns JSX (SVG is recommended). + +**To enable:** select a place, then toggle **Visualizer** in its properties. A code editor opens. + +```tsx +export default Visualization(({ tokens, parameters }) => { + return + {tokens.map(({ x, y }, i) => ( + + ))} + +}); +``` + +The component receives `tokens` (array of token objects) and `parameters` (global parameter values). It renders in the properties panel. During simulation, it updates live as token state changes. + +visauliser-preview + +Use the menu in the code editor header to **Load default template** for a starting point. + +You can also toggle between the code, a preview, and both at once. + +**Example:** the [Satellites in Orbit](examples.md#satellites-in-orbit) example includes a visualizer that renders Earth and orbiting satellites with velocity vectors. + +## Transition kernel + +The transition kernel defines how input tokens are transformed into output tokens when a transition fires. + +```ts +export default TransitionKernel((tokensByPlace, parameters) => { + return { + OutputPlace: [{ x: tokensByPlace.InputPlace[0].x + 1 }], + }; +}); +``` + +`tokensByPlace` is keyed by **place name**. Each value is an array of token objects from that input place. The return value is keyed by **output place name**, each containing an array of token objects to produce. + +Use the menu in the code editor header to **Load default template** for a starting point. + +### Distributions + +Kernel output values can be numbers or `Distribution` objects for stochastic output: + +- `Distribution.Gaussian(mean, standardDeviation)` +- `Distribution.Uniform(min, max)` +- `Distribution.Lognormal(mu, sigma)` + +Use `.map(fn)` to transform a sampled value: + +```ts +const angle = Distribution.Uniform(0, 2 * Math.PI); +return { + Space: [{ + x: angle.map(a => Math.cos(a) * 80), + y: angle.map(a => Math.sin(a) * 80), + }], +}; +``` + +The underlying random sample is drawn once and shared across chained `.map()` calls, so `x` and `y` above are derived from the same angle. + +### Empty kernels + +For transitions where all output places are **untyped**, the kernel code can be left empty. The engine produces the correct number of black tokens automatically. + +## Firing rate / predicate + +Each transition has a **firing rate** that controls when it fires, once structurally enabled (sufficient tokens in input places). Choose between two modes in the transition properties: + +### Predicate + +The function returns a **boolean**. The transition fires immediately when it returns `true`. + +```ts +export default Lambda((tokensByPlace, parameters) => { + return tokensByPlace.MyPlace[0].progress >= 1.0; +}); +``` + +Use predicates for deterministic guards based on token state. + +### Stochastic rate + +The function returns a **number** representing the average firing rate per second: + +- `0` -- disabled (will not fire). +- Any positive number -- average rate (e.g. `2.0` means roughly twice per second). +- `Infinity` -- fires immediately when enabled. + +```ts +export default Lambda((tokensByPlace, parameters) => { + return parameters.rate; +}); +``` + +## Inhibitor arcs + +An inhibitor arc is a special input arc that **prevents** a transition from firing when the source place has tokens equal to or greater than the arc weight -- the opposite of a normal arc. + +**To set:** select an input arc (place to transition) and switch its **Type** to **Inhibitor** in the properties panel. Only input arcs can be inhibitor. + +**Semantics:** the transition is enabled (on this arc) when the source place has **fewer tokens than the arc weight**. With the default weight of 1, this means the place must be empty. + +Inhibitor arcs **do not consume tokens** when the transition fires. + +inhibitor-arc-deployment + +**Example:** in [Deployment Pipeline](examples.md#deployment-pipeline), inhibitor arcs from "IncidentBeingInvestigated" and "DeploymentInProgress" block new deployments while an incident is open or a deployment is already running. + +## Diagnostics + +The **Diagnostics** tab in the bottom panel shows TypeScript errors in your code (dynamics, firing rate, kernels, visualizers), grouped by entity. Click a diagnostic to select the relevant entity and see the error in context. + +Diagnostics must be resolved before running a simulation -- pressing Play with unresolved errors opens the Diagnostics tab instead of starting the simulation. diff --git a/libs/@hashintel/petrinaut/docs/simulation.md b/libs/@hashintel/petrinaut/docs/simulation.md new file mode 100644 index 00000000000..69c5cee1286 --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/simulation.md @@ -0,0 +1,133 @@ +# Simulation + +## Initial state + +Before running a simulation, set the **initial marking** -- the starting tokens in each place. + +Select a place and open the **State** sub-view in its properties: + +- **Untyped places** -- set a token count (integer). +- **Typed places** -- define individual tokens with values for each dimension in a spreadsheet editor. Add a row to create a new token. + +initial-states + +If no initial marking is set, a place starts empty (zero tokens). + +## Simulation settings + +Open the **Simulation Settings** tab in the bottom panel to configure: + +### Parameters + +Override [global parameter](petri-net-extensions.md#global-parameters) values for this run. Each parameter shows its name, variable name, and a value input (pre-filled with the default). Changes here do not modify the parameter definition -- they only apply to the simulation. + +Parameter values are locked while a simulation is running. Reset the simulation to change them. + +### Time step (dt) + +The time step in seconds per frame. Controls the resolution of ODE integration and how frequently transitions are evaluated. + +- **Smaller dt** -- finer approximation, but slower computation. +- **Larger dt** -- faster, but less accurate for continuous dynamics. + +Default: `0.01` seconds. + +### ODE solver + +The numerical method for integrating differential equations. Currently only **Euler** is available. + +## Running a simulation + +Press **Play** in the bottom toolbar. The simulation: + +1. Initializes with a random seed, the current dt, and parameter values. +2. Computes frames in a background Web Worker. +3. Streams frames to the UI for playback. + +If there are unresolved [diagnostics](petri-net-extensions.md#diagnostics) (code errors), pressing Play opens the Diagnostics tab instead of starting the simulation. Fix all errors first. + +simulation-settings + +## How a frame is computed + +Each simulation step proceeds in two phases: + +1. **Continuous dynamics** -- for every place with dynamics enabled, the differential equation is integrated one step (Euler method, step size = dt). This updates all token dimension values. + +2. **Discrete transitions** -- transitions are evaluated in definition order (deterministic, not random). For each transition: + - Checks structural enablement (enough tokens in input places, inhibitor conditions met). + - Evaluates the lambda (predicate or stochastic rate). + - If the transition fires, removes input tokens **immediately** (subsequent transitions see the updated state). + + All produced output tokens are added at the end of the step. + +Simulation time advances by `dt` each frame. + +## Deadlock + +If no transition fires in a step **and** no transition is structurally enabled (regardless of lambda values), the simulation reports **deadlock** and stops (a "Simulation Complete" message is shown). + +This only stops computation: the simulation will continue to playback computed frames until no more are available. + +If transitions are structurally enabled but their lambdas prevent firing, the simulation continues stepping. + +## Playback controls + +The bottom toolbar provides playback controls: + +| Control | Description | +| ---------------- | ----------------------------------- | +| **Play** | Start or resume playback. | +| **Pause** | Pause at the current frame. | +| **Stop / Reset** | Stop playback and reset to frame 0. | + +The frame counter shows the current frame number, total frames, and elapsed simulation time. + +simulation-toolbar + +### Speed + +Choose a playback speed multiplier via playback settings: **1x**, **2x**, **5x**, **10x**, **30x**, **60x**, **120x**, or **Max** (as fast as possible). + +### Play mode + +Controls how computation and playback interact: + +| Mode | Behavior | +| ---------------------------- | ----------------------------------------------------------- | +| **Play computed steps only** | Replay already-computed frames without further computation. | +| **Play + compute buffer** | Compute only a small buffer ahead of the playhead. | +| **Play + compute max** | Compute frames as fast as possible while playing. | + +### Stopping condition + +- **Run indefinitely** -- simulation continues until manually paused or deadlock. +- **End at fixed time** -- simulation stops after a set number of seconds (simulation time). + +Stopping conditions are **locked after the simulation starts**. Reset the simulation to change them. + +## Timeline + +The **Timeline** tab appears in the bottom panel during and after simulation. It shows token counts per place over time as a chart. + +timeline + +- **Chart type** -- toggle between **Run** (line chart) and **Stacked** (area chart) using the control in the tab header. +- **Scrub** -- click or drag on the chart to jump to any frame. A playhead indicator shows the current position. +- **Legend** -- click place names to show/hide individual traces. Hover to dim other traces. Y axis is automatically scaled to the maximum value. + +## Viewing state during simulation + +Select a place during simulation to see its current token values in the properties panel. For typed places, individual token dimension values are displayed. + +If the place has a [visualizer](petri-net-extensions.md#visualizer) defined, it renders live in the properties panel, updating as the simulation progresses. + +![visualiser](https://github.com/user-attachments/assets/9324bb5b-4912-499e-8a5d-f2bc6a7754c2) + +## Locked editing + +The editor is **read-only** during simulation and after a simulation completes. You cannot add, remove, or modify nodes, arcs, types, or code while a simulation exists. + +Press **Stop / Reset** to return to editing mode. + +At the last frame of a completed simulation, Play is disabled -- reset to replay from the beginning. diff --git a/libs/@hashintel/petrinaut/docs/useful-patterns.md b/libs/@hashintel/petrinaut/docs/useful-patterns.md new file mode 100644 index 00000000000..a21a8d8713c --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/useful-patterns.md @@ -0,0 +1,140 @@ +# Useful Patterns + +Useful modelling techniques for Petri nets in Petrinaut. + +## Modelling duration (exponential) + +For processes with **exponentially distributed** duration, set the transition's stochastic firing rate to `1 / mean_duration`. The exponential distribution is built into the stochastic firing mechanism -- no extra setup needed. + +```ts +export default Lambda((tokensByPlace, parameters) => { + return 1 / parameters.mean_repair_time; +}); +``` + +This is the simplest way to model duration and works well for many processes (service times, failure intervals, etc.). + +## Modelling duration (non-exponential) + +For other distributions (e.g. log-normal, deterministic), place dynamics and durations sampled in the preceding transition kernel can be used. The general approach: + +1. **Add a time dimension** to the token type (e.g. `remaining_time`). +2. **Sample the duration** in a transition kernel using a `Distribution`: + +```ts +export default TransitionKernel((tokensByPlace, parameters) => { + return { + InProgress: [{ + remaining_time: Distribution.Lognormal(2.0, 0.5), + // ... other dimensions + }], + }; +}); +``` + +1. **Count down** with a differential equation: + +```ts +export default Dynamics((tokens, parameters) => { + return tokens.map(() => ({ remaining_time: -1 })); +}); +``` + +1. **Guard the completion transition** with a predicate: + +```ts +export default Lambda((tokensByPlace, parameters) => { + return tokensByPlace.InProgress[0].remaining_time <= 0; +}); +``` + +**Alternative approach:** use two dimensions -- a fixed `sampled_duration` that doesn't change and a `counter` that increments via dynamics. Guard on `counter >= sampled_duration`. This preserves the original sampled value for inspection. + +## Resource pools + +Use a place as a **pool** of tokens representing limited resources (machines, workers, servers). Transitions consume from the pool when starting work and return tokens when done. + +**Structure:** + +```text +(Available) ---> [StartWork] ---> (InUse) ---> [FinishWork] ---> [Available) +``` + +The number of initial tokens in "Available" determines the resource capacity. If no tokens are available, "StartWork" cannot fire -- work is naturally queued. + +**Example:** the [Production Machines](examples.md#production-machines) example models machines cycling between available, producing, broken, and being repaired states. + +## Mutual exclusion with inhibitor arcs + +Use an [inhibitor arc](petri-net-extensions.md#inhibitor-arcs) from a "busy" or "blocked" place to prevent a transition from firing while a condition holds. + +**Structure:** + +```text +(Busy) ---o [StartNew] (inhibitor arc, weight 1) +``` + +"StartNew" can only fire when "Busy" has zero tokens. Once something enters the busy state, no new work can start until the token is removed. + +**Example:** the [Deployment Pipeline](examples.md#deployment-pipeline) uses inhibitor arcs to block new deployments while an incident is being investigated or another deployment is already in progress. + +## Source transitions (exogenous arrivals) + +A transition with **no input arcs** is always structurally enabled. Set a stochastic rate to model arrivals following a Poisson process. + +```ts +export default Lambda((tokensByPlace, parameters) => { + return parameters.arrival_rate; +}); +``` + +Use the transition kernel to define the properties of newly created tokens (if the output place is typed). + +**Examples:** + +- [Deployment Pipeline](examples.md#deployment-pipeline) -- "Create Deployment" and "Incident Raised" generate events at configurable rates. +- [Probabilistic Satellites Launcher](examples.md#probabilistic-satellites-launcher) -- "LaunchSatellite" creates satellites with randomized initial positions and velocities using `Distribution.Uniform` and `Distribution.Gaussian`. + +## Sink transitions (removal / absorption) + +A transition with **no output arcs** consumes tokens without producing any. Useful for modelling: + +- **Expiry** -- tokens that age out or are consumed. +- **Departure** -- entities leaving the system. +- **Disposal** -- rejected or failed items. + +No special configuration needed -- just create a transition with input arcs only. + +## Competing transitions / routing + +Multiple transitions consuming from the **same place** with **complementary predicates** can model routing or branching decisions. + +**Structure:** + +```text + /--> [Pass] ---> (Dispatched) +(QAQueue) --< + \--> [Fail] ---> (Disposed) +``` + +```ts +// Pass transition +export default Lambda((tokensByPlace, parameters) => { + return tokensByPlace.QAQueue[0].quality >= parameters.quality_threshold; +}); + +// Fail transition +export default Lambda((tokensByPlace, parameters) => { + return tokensByPlace.QAQueue[0].quality < parameters.quality_threshold; +}); +``` + +**Example:** the [Supply Chain (Stochastic)](examples.md#supply-chain-stochastic) example routes products to dispatch or disposal based on a quality threshold. + +## Arc weight for multi-token operations + +An input arc with **weight > 1** requires multiple tokens from the same place for the transition to be enabled. This is useful for interactions between entities. + +**Example:** the [Satellites in Orbit](examples.md#satellites-in-orbit) example has a "Collision" transition with input weight 2 from the "Space" place -- it requires two satellites to be present and checks their distance in the lambda to detect collisions. + +The transition kernel receives the consumed tokens and can compute outputs based on all of them. diff --git a/libs/@hashintel/petrinaut/docs/visual-settings.md b/libs/@hashintel/petrinaut/docs/visual-settings.md new file mode 100644 index 00000000000..38f7875d07f --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/visual-settings.md @@ -0,0 +1,51 @@ +# Visual Settings + +Access the settings dialog via the **gear icon** in the viewport controls (bottom-right corner of the canvas). + + + +## Available settings + +### Animations + +Toggle panel transition and UI interaction animations. Disable for a snappier feel or if animations cause performance issues. + +### Keep panels mounted + +When enabled, hidden panels remain loaded in the background. Switching between panels is faster, but uses more memory. When disabled, panels are unmounted when hidden and re-created when opened. + +### Minimap + +Show or hide the **overview minimap** in the top-right corner of the canvas. The minimap provides a zoomed-out view of the entire net for orientation in large models. + +### Snap to grid + +When enabled, node positions snap to a grid when placing new nodes or dragging existing ones. Helps keep nets tidy and aligned. + +### Compact nodes + +Switch between two node rendering styles: + +- **Compact** (enabled) -- smaller card-style nodes. +- **Classic** (disabled) -- larger nodes with more detail. + +### Partial selection + +Controls selection box behavior in [Select mode](drawing-a-net.md#pan-and-select-modes): + +- **Enabled** -- nodes that are only partially inside the selection box are selected. +- **Disabled** -- nodes must be fully enclosed to be selected. + +### Entities tree view (experimental) + +Replaces the tabbed left sidebar with a unified **tree view** showing all entities (nodes, types, equations, parameters) in a single hierarchy. + +### Arcs rendering + +Choose how arcs are drawn between nodes: + +| Style | Description | +| ------------------- | -------------------------------------------------------------| +| **Square** | Right-angle paths (smoothstep routing). | +| **Bezier** | Smooth curved paths. | +| **Adaptive Bezier** | Curved paths that adjust based on node positions. (Default) | diff --git a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts b/libs/@hashintel/petrinaut/src/examples/supply-chain.ts deleted file mode 100644 index 58c0e51fefa..00000000000 --- a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { SNAP_GRID_SIZE } from "../constants/ui"; -import type { SDCPN } from "../core/types/sdcpn"; - -export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = { - title: "Drug Production", - petriNetDefinition: { - places: [ - { - id: "place__0", - name: "PlantASupply", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: SNAP_GRID_SIZE, - y: 8 * SNAP_GRID_SIZE, - }, - { - id: "place__1", - name: "PlantBSupply", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: SNAP_GRID_SIZE, - y: 40 * SNAP_GRID_SIZE, - }, - { - id: "place__2", - name: "ManufacturingPlant", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 20 * SNAP_GRID_SIZE, - y: 20 * SNAP_GRID_SIZE, - }, - { - id: "place__3", - name: "QAQueue", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 47 * SNAP_GRID_SIZE, - y: 23 * SNAP_GRID_SIZE, - }, - { - id: "place__4", - name: "Disposal", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 73 * SNAP_GRID_SIZE, - y: 40 * SNAP_GRID_SIZE, - }, - { - id: "place__5", - name: "Dispatch", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 67 * SNAP_GRID_SIZE, - y: 13 * SNAP_GRID_SIZE, - }, - { - id: "place__6", - name: "Hospital", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 87 * SNAP_GRID_SIZE, - y: 25 * SNAP_GRID_SIZE, - }, - ], - transitions: [ - { - id: "transition__0", - name: "Deliver to Plant", - inputArcs: [ - { placeId: "place__0", weight: 1, type: "standard" }, - { placeId: "place__1", weight: 1, type: "standard" }, - ], - outputArcs: [{ placeId: "place__2", weight: 1 }], - lambdaType: "predicate", - lambdaCode: "export default Lambda(() => true);", - transitionKernelCode: "", - x: 7 * SNAP_GRID_SIZE, - y: 27 * SNAP_GRID_SIZE, - }, - { - id: "transition__1", - name: "Manufacture", - inputArcs: [{ placeId: "place__2", weight: 1, type: "standard" }], - outputArcs: [{ placeId: "place__3", weight: 1 }], - lambdaType: "predicate", - lambdaCode: "export default Lambda(() => true);", - transitionKernelCode: "", - x: 33 * SNAP_GRID_SIZE, - y: 23 * SNAP_GRID_SIZE, - }, - { - id: "transition__2", - name: "Quality Check", - inputArcs: [{ placeId: "place__3", weight: 1, type: "standard" }], - outputArcs: [ - { placeId: "place__5", weight: 1 }, - { placeId: "place__4", weight: 1 }, - ], - lambdaType: "predicate", - lambdaCode: "export default Lambda(() => true);", - transitionKernelCode: "", - x: 58 * SNAP_GRID_SIZE, - y: 27 * SNAP_GRID_SIZE, - }, - { - id: "transition__3", - name: "Ship", - inputArcs: [{ placeId: "place__5", weight: 1, type: "standard" }], - outputArcs: [{ placeId: "place__6", weight: 1 }], - lambdaType: "predicate", - lambdaCode: "export default Lambda(() => true);", - transitionKernelCode: "", - x: 77 * SNAP_GRID_SIZE, - y: 19 * SNAP_GRID_SIZE, - }, - ], - types: [], - differentialEquations: [], - parameters: [], - }, -}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx index c4860a2d6d4..541f6045436 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx @@ -8,7 +8,6 @@ import { deploymentPipelineSDCPN } from "../../examples/deployment-pipeline"; import { satellitesSDCPN } from "../../examples/satellites"; import { probabilisticSatellitesSDCPN } from "../../examples/satellites-launcher"; import { sirModel } from "../../examples/sir-model"; -import { supplyChainSDCPN } from "../../examples/supply-chain"; import { supplyChainStochasticSDCPN } from "../../examples/supply-chain-stochastic"; import { exportSDCPN } from "../../file-format/export-sdcpn"; import { importSDCPN } from "../../file-format/import-sdcpn"; @@ -271,14 +270,6 @@ export const EditorView = ({ id: "load-example", label: "Load example", submenu: [ - { - id: "load-example-supply-chain", - label: "Supply Chain", - onClick: () => { - createNewNet(supplyChainSDCPN); - clearSelection(); - }, - }, { id: "load-example-supply-chain-stochastic", label: "Probabilistic Supply Chain", @@ -331,6 +322,17 @@ export const EditorView = ({ }, ] : []), + { + id: "docs", + label: "Docs", + onClick: () => { + window.open( + "https://github.com/hashintel/hash/tree/main/libs/%40hashintel/petrinaut/docs", + "_blank", + "noopener,noreferrer", + ); + }, + }, ]; const portalContainerRef = useRef(null);