diff --git a/.agents/skills/publishing-packages/SKILL.md b/.agents/skills/publishing-packages/SKILL.md new file mode 100644 index 00000000000..e672af841a6 --- /dev/null +++ b/.agents/skills/publishing-packages/SKILL.md @@ -0,0 +1,33 @@ +--- +name: publishing-packages +description: Conventions for publishing npm packages from this repository, including when a PR needs a changeset and how to write one. Use when adding or editing a changeset, versioning or releasing a package, or deciding whether a change to a publishable library needs a changelog entry. +license: AGPL-3.0 +metadata: + triggers: + type: domain + enforcement: suggest + priority: high + keywords: + - changeset + - changelog + - npm publish + - release + - version bump + intent-patterns: + - "\\b(add|write|create|edit|split)\\b.*?\\bchangesets?\\b" + - "\\b(publish|release|version)\\b.*?\\bpackages?\\b" +--- + +# Publishing Packages + +npm-publishable packages in this repository are versioned and released with [changesets](https://github.com/changesets/changesets). The changeset configuration, including which packages are ignored, lives in [`.changeset/config.json`](../../../.changeset/config.json). + +## Changesets + +A PR that changes an npm-publishable package adds a changeset file under `.changeset/`. The changeset text becomes the package's published changelog entry, so write it for the package's consumers: + +- Describe the change as a consumer of the package experiences it: behaviour changes, bug fixes, and interface changes. Leave out implementation details that do not affect consumers, and do not mention this repository or its internal systems. +- Every sentence must apply to each package the changeset lists. When a PR changes several packages in different ways, write a separate changeset for each package. +- Use at most one paragraph per changeset. + +A package's own `AGENTS.md` can carry further conventions, such as the bump level it accepts; check it before writing the changeset. diff --git a/.changeset/sdcpn-description-metadata-core.md b/.changeset/sdcpn-description-metadata-core.md new file mode 100644 index 00000000000..3ef0557778e --- /dev/null +++ b/.changeset/sdcpn-description-metadata-core.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut-core": patch +--- + +SDCPN elements carry an optional `description` (places, transitions, types, subnets, component instances, and the net root) and an optional `metadata` record of JSON values (transitions, subnets, component instances, and the net root). `metadata` is host-defined and opaque to the library. Both fields validate against the entity schemas, survive file import/export, and are preserved when an `SDCPNInput` is normalized. Files written without the fields still validate. diff --git a/.changeset/sdcpn-description-metadata-ui.md b/.changeset/sdcpn-description-metadata-ui.md new file mode 100644 index 00000000000..023c2ef016e --- /dev/null +++ b/.changeset/sdcpn-description-metadata-ui.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +The place, transition, component-instance, and type properties panels display and allow editing of the element's optional `description`. diff --git a/.claude/skills/publishing-packages b/.claude/skills/publishing-packages new file mode 120000 index 00000000000..0fcb353b5a9 --- /dev/null +++ b/.claude/skills/publishing-packages @@ -0,0 +1 @@ +../../.agents/skills/publishing-packages \ No newline at end of file diff --git a/.claude/skills/skill-rules.json b/.claude/skills/skill-rules.json index 7b5114a9578..82497a363c2 100644 --- a/.claude/skills/skill-rules.json +++ b/.claude/skills/skill-rules.json @@ -214,6 +214,33 @@ "blockMessage": "Skill is required to proceed", "skipConditions": {} }, + "publishing-packages": { + "type": "domain", + "enforcement": "suggest", + "priority": "high", + "description": "Conventions for publishing npm packages from this repository, including when a PR needs a changeset and how to write one. Use when adding or editing a changeset, versioning or releasing a package, or deciding whether a change to a publishable library needs a changelog entry.", + "promptTriggers": { + "keywords": [ + "changeset", + "changelog", + "npm publish", + "release", + "version bump" + ], + "intentPatterns": [ + "\\b(add|write|create|edit|split)\\b.*?\\bchangesets?\\b", + "\\b(publish|release|version)\\b.*?\\bpackages?\\b" + ] + }, + "fileTriggers": { + "include": [], + "exclude": [], + "content": [], + "create-only": false + }, + "blockMessage": "Skill is required to proceed", + "skipConditions": {} + }, "rust-coding-style": { "type": "domain", "enforcement": "suggest", diff --git a/apps/petrinaut-website/scripts/generate-example-artifacts.ts b/apps/petrinaut-website/scripts/generate-example-artifacts.ts index 7343f526b60..d0674b8ced6 100644 --- a/apps/petrinaut-website/scripts/generate-example-artifacts.ts +++ b/apps/petrinaut-website/scripts/generate-example-artifacts.ts @@ -18,7 +18,6 @@ import { exampleCatalog, type ExampleCatalogEntry, } from "../src/examples/catalog-metadata.ts"; -import { normalizeExampleDefinition } from "../src/examples/normalize-example.ts"; const scriptDirectory = dirname(fileURLToPath(import.meta.url)); const examplesDirectory = resolve(scriptDirectory, "../src/examples"); @@ -33,8 +32,8 @@ const readModelFile = async (slug: string): Promise => { if (!parsed.ok) { throw new Error(`${slug}: ${parsed.error}`); } - const { title: _title, ...rawDefinition } = parsed.sdcpn; - return normalizeExampleDefinition(slug, rawDefinition); + const { title: _title, ...definition } = parsed.sdcpn; + return definition; }; const loadDefinition = (entry: ExampleCatalogEntry): Promise => { diff --git a/apps/petrinaut-website/src/examples/catalog.ts b/apps/petrinaut-website/src/examples/catalog.ts index 0c1ad3706d9..e6964e06da4 100644 --- a/apps/petrinaut-website/src/examples/catalog.ts +++ b/apps/petrinaut-website/src/examples/catalog.ts @@ -16,7 +16,6 @@ import { type ExampleSlug, type ModelFileExampleSlug, } from "./catalog-metadata"; -import { normalizeExampleDefinition } from "./normalize-example"; export { exampleCatalog, @@ -88,8 +87,8 @@ const loadModelFile = async (slug: ModelFileExampleSlug): Promise => { throw new Error(parsed.error); } - const { title: _title, ...rawDefinition } = parsed.sdcpn; - return normalizeExampleDefinition(slug, rawDefinition); + const { title: _title, ...definition } = parsed.sdcpn; + return definition; }; const loadDefinition = async (entry: ExampleCatalogEntry): Promise => { diff --git a/apps/petrinaut-website/src/examples/models/gases-1-pn-consumption-trigger.json b/apps/petrinaut-website/src/examples/models/gases-1-pn-consumption-trigger.json index fd8d6e3fadd..938d4918e66 100644 --- a/apps/petrinaut-website/src/examples/models/gases-1-pn-consumption-trigger.json +++ b/apps/petrinaut-website/src/examples/models/gases-1-pn-consumption-trigger.json @@ -3,6 +3,7 @@ { "id": "place__idle_tankers", "name": "IdleTankers", + "description": "Tankers parked at the depot and free to be dispatched. Dispatch takes one; unloading returns it.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -13,6 +14,7 @@ { "id": "place__loads_delivered", "name": "LoadsDelivered", + "description": "Cumulative count of tanker deliveries unloaded at a customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -23,6 +25,7 @@ { "id": "place__s1_order_placed", "name": "SteadyNitrogenOrderPlaced", + "description": "An open order from the steady nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -33,6 +36,7 @@ { "id": "place__s1_order_permits", "name": "SteadyNitrogenOrderPermits", + "description": "The single permit that lets the steady nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -43,6 +47,7 @@ { "id": "place__s1_on_route", "name": "SteadyNitrogenOnRoute", + "description": "A dispatched tanker travelling to the steady nitrogen customer with a 12-unit load.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -53,6 +58,7 @@ { "id": "place__s1_vented", "name": "SteadyNitrogenVented", + "description": "Cumulative units of product the steady nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__s1_line_running", "name": "SteadyNitrogenLineRunning", + "description": "Marker token present while the production line of the steady nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__s1_line_stopped", "name": "SteadyNitrogenLineStopped", + "description": "Marker token present while the line of the steady nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__s1_stockouts", "name": "SteadyNitrogenStockouts", + "description": "Cumulative count of times the tank of the steady nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__s1_consumed", "name": "SteadyNitrogenConsumed", + "description": "Cumulative units drawn by the process of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__s1_evaporated", "name": "SteadyNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__s1_contents", "name": "SteadyNitrogenContents", + "description": "Units of liquid in the tank of the steady nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__s1_ullage", "name": "SteadyNitrogenUllage", + "description": "Empty space in the tank of the steady nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -133,6 +146,7 @@ { "id": "place__s1_units_drawn_since_order", "name": "SteadyNitrogenUnitsDrawnSinceOrder", + "description": "Counter of units the steady nitrogen customer has drawn since the last order was raised. Reaching 8 raises the next order, so ordering follows consumption rather than tank level.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -145,6 +159,7 @@ { "id": "transition__s1_draw", "name": "Draw a unit (SteadyNitrogen)", + "description": "The process of the steady nitrogen customer draws one unit from the tank into Consumed, freeing one unit of ullage and advancing the units-drawn counter. Enabled only while draw_enabled is set, so the shut-plant scenario can switch consumption off.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -175,6 +190,7 @@ { "id": "transition__s1_boil_off", "name": "Boil off a unit (SteadyNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the steady nitrogen customer into Evaporated, freeing one unit of ullage. Runs whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -201,6 +217,7 @@ { "id": "transition__s1_raise_order", "name": "Raise an order, 8 units drawn (SteadyNitrogen)", + "description": "Places an order for the steady nitrogen customer once 8 units have been drawn since the last order, consuming the counter and the single order permit.", "inputArcs": [ { "placeId": "place__s1_units_drawn_since_order", @@ -228,6 +245,7 @@ { "id": "transition__s1_dispatch", "name": "Dispatch a tanker (SteadyNitrogen)", + "description": "Pairs the open order of the steady nitrogen customer with an idle tanker and sends it on route.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -255,6 +273,7 @@ { "id": "transition__s1_arrive", "name": "Unload the tanker (SteadyNitrogen)", + "description": "The tanker arrives at the steady nitrogen customer and discharges 12 units into the tank, which must have 12 units of ullage to take them. Returns the order permit and the tanker to the depot and counts a delivery.", "inputArcs": [ { "placeId": "place__s1_on_route", @@ -294,6 +313,7 @@ { "id": "transition__s1_vent", "name": "Vent through the relief valve (SteadyNitrogen)", + "description": "With no ullage left in the tank of the steady nitrogen customer, boil-off gas has nowhere to go: one unit is vented and one unit of ullage is left free.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -325,6 +345,7 @@ { "id": "transition__s1_stop_line", "name": "Stop the line (SteadyNitrogen)", + "description": "When the tank of the steady nitrogen customer is empty (inhibitor arc on Contents) the running line stops and a stockout is counted.", "inputArcs": [ { "placeId": "place__s1_line_running", @@ -356,6 +377,7 @@ { "id": "transition__s1_resume_line", "name": "Resume the line (SteadyNitrogen)", + "description": "The stopped line of the steady nitrogen customer restarts as soon as the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s1_line_stopped", @@ -482,5 +504,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Gases 1 — plain net, one customer (consumption trigger)" + "title": "Gases 1 — plain net, one customer (consumption trigger)", + "description": "The one-customer plain net with a different ordering policy: an order is raised after 8 units have been drawn rather than when the tank level drops below a trigger. Boil-off drains the tank without advancing the counter, so a shut plant can run dry before an order is ever placed." } diff --git a/apps/petrinaut-website/src/examples/models/gases-1-pn.json b/apps/petrinaut-website/src/examples/models/gases-1-pn.json index d4b42b96572..24b6a80ed4b 100644 --- a/apps/petrinaut-website/src/examples/models/gases-1-pn.json +++ b/apps/petrinaut-website/src/examples/models/gases-1-pn.json @@ -3,6 +3,7 @@ { "id": "place__idle_tankers", "name": "IdleTankers", + "description": "Tankers parked at the depot and free to be dispatched. Dispatch takes one; unloading returns it.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -13,6 +14,7 @@ { "id": "place__loads_delivered", "name": "LoadsDelivered", + "description": "Cumulative count of tanker deliveries unloaded at a customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -23,6 +25,7 @@ { "id": "place__s1_order_placed", "name": "SteadyNitrogenOrderPlaced", + "description": "An open order from the steady nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -33,6 +36,7 @@ { "id": "place__s1_order_permits", "name": "SteadyNitrogenOrderPermits", + "description": "The single permit that lets the steady nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -43,6 +47,7 @@ { "id": "place__s1_on_route", "name": "SteadyNitrogenOnRoute", + "description": "A dispatched tanker travelling to the steady nitrogen customer with a 12-unit load.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -53,6 +58,7 @@ { "id": "place__s1_vented", "name": "SteadyNitrogenVented", + "description": "Cumulative units of product the steady nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__s1_line_running", "name": "SteadyNitrogenLineRunning", + "description": "Marker token present while the production line of the steady nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__s1_line_stopped", "name": "SteadyNitrogenLineStopped", + "description": "Marker token present while the line of the steady nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__s1_stockouts", "name": "SteadyNitrogenStockouts", + "description": "Cumulative count of times the tank of the steady nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__s1_consumed", "name": "SteadyNitrogenConsumed", + "description": "Cumulative units drawn by the process of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__s1_evaporated", "name": "SteadyNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__s1_contents", "name": "SteadyNitrogenContents", + "description": "Units of liquid in the tank of the steady nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__s1_ullage", "name": "SteadyNitrogenUllage", + "description": "Empty space in the tank of the steady nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -135,6 +148,7 @@ { "id": "transition__s1_draw", "name": "Draw a unit (SteadyNitrogen)", + "description": "The process of the steady nitrogen customer draws one unit from the tank into Consumed, freeing one unit of ullage. Enabled only while draw_enabled is set, so the shut-plant scenario can switch consumption off.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -161,6 +175,7 @@ { "id": "transition__s1_boil_off", "name": "Boil off a unit (SteadyNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the steady nitrogen customer into Evaporated, freeing one unit of ullage. Runs whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -187,6 +202,7 @@ { "id": "transition__s1_raise_order", "name": "Raise an order, level below trigger (SteadyNitrogen)", + "description": "Places an order for the steady nitrogen customer when the inhibitor arc sees fewer than 16 units in the tank, taking the single order permit so only one order is open at a time.", "inputArcs": [ { "placeId": "place__s1_order_permits", @@ -214,6 +230,7 @@ { "id": "transition__s1_dispatch", "name": "Dispatch a tanker (SteadyNitrogen)", + "description": "Pairs the open order of the steady nitrogen customer with an idle tanker and sends it on route.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -241,6 +258,7 @@ { "id": "transition__s1_arrive", "name": "Unload the tanker (SteadyNitrogen)", + "description": "The tanker arrives at the steady nitrogen customer and discharges 12 units into the tank, which must have 12 units of ullage to take them. Returns the order permit and the tanker to the depot and counts a delivery.", "inputArcs": [ { "placeId": "place__s1_on_route", @@ -280,6 +298,7 @@ { "id": "transition__s1_vent", "name": "Vent through the relief valve (SteadyNitrogen)", + "description": "With no ullage left in the tank of the steady nitrogen customer, boil-off gas has nowhere to go: one unit is vented and one unit of ullage is left free.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -311,6 +330,7 @@ { "id": "transition__s1_stop_line", "name": "Stop the line (SteadyNitrogen)", + "description": "When the tank of the steady nitrogen customer is empty (inhibitor arc on Contents) the running line stops and a stockout is counted.", "inputArcs": [ { "placeId": "place__s1_line_running", @@ -342,6 +362,7 @@ { "id": "transition__s1_resume_line", "name": "Resume the line (SteadyNitrogen)", + "description": "The stopped line of the steady nitrogen customer restarts as soon as the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s1_line_stopped", @@ -468,5 +489,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Gases 1 — plain net, one customer" + "title": "Gases 1 — plain net, one customer", + "description": "Plain Petri net of one industrial-gas customer: a cryogenic tank drained one unit at a time by process draw and boil-off, refilled by 12-unit tanker deliveries ordered when the level falls below 16 units. Empty stops the customer's line and full vents product, so the ordering policy has to hold the level inside a band." } diff --git a/apps/petrinaut-website/src/examples/models/gases-2-spn.json b/apps/petrinaut-website/src/examples/models/gases-2-spn.json index 31eb77a1c1a..b5ab5430ce0 100644 --- a/apps/petrinaut-website/src/examples/models/gases-2-spn.json +++ b/apps/petrinaut-website/src/examples/models/gases-2-spn.json @@ -3,6 +3,7 @@ { "id": "place__idle_tankers", "name": "IdleTankers", + "description": "Tankers parked at the depot and free to be dispatched. Dispatch takes one; unloading returns it.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -13,6 +14,7 @@ { "id": "place__loads_delivered", "name": "LoadsDelivered", + "description": "Cumulative count of tanker deliveries unloaded at a customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -23,6 +25,7 @@ { "id": "place__returning", "name": "Returning", + "description": "Tankers on the empty run back to the depot after unloading; the return trip takes return_time hours on average.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -33,6 +36,7 @@ { "id": "place__s1_order_placed", "name": "SteadyNitrogenOrderPlaced", + "description": "An open order from the steady nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -43,6 +47,7 @@ { "id": "place__s1_order_permits", "name": "SteadyNitrogenOrderPermits", + "description": "The single permit that lets the steady nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -53,6 +58,7 @@ { "id": "place__s1_on_route", "name": "SteadyNitrogenOnRoute", + "description": "A dispatched tanker travelling to the steady nitrogen customer with a 12-unit load.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__s1_vented", "name": "SteadyNitrogenVented", + "description": "Cumulative units of product the steady nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__s1_line_running", "name": "SteadyNitrogenLineRunning", + "description": "Marker token present while the production line of the steady nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__s1_line_stopped", "name": "SteadyNitrogenLineStopped", + "description": "Marker token present while the line of the steady nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__s1_stockouts", "name": "SteadyNitrogenStockouts", + "description": "Cumulative count of times the tank of the steady nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__s1_consumed", "name": "SteadyNitrogenConsumed", + "description": "Cumulative units drawn by the process of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__s1_evaporated", "name": "SteadyNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__s1_contents", "name": "SteadyNitrogenContents", + "description": "Units of liquid in the tank of the steady nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -133,6 +146,7 @@ { "id": "place__s1_ullage", "name": "SteadyNitrogenUllage", + "description": "Empty space in the tank of the steady nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -143,6 +157,7 @@ { "id": "place__s2_order_placed", "name": "SlowNitrogenOrderPlaced", + "description": "An open order from the slow-drawing nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -153,6 +168,7 @@ { "id": "place__s2_order_permits", "name": "SlowNitrogenOrderPermits", + "description": "The single permit that lets the slow-drawing nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -163,6 +179,7 @@ { "id": "place__s2_on_route", "name": "SlowNitrogenOnRoute", + "description": "A dispatched tanker travelling to the slow-drawing nitrogen customer with a 12-unit load.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -173,6 +190,7 @@ { "id": "place__s2_vented", "name": "SlowNitrogenVented", + "description": "Cumulative units of product the slow-drawing nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -183,6 +201,7 @@ { "id": "place__s2_line_running", "name": "SlowNitrogenLineRunning", + "description": "Marker token present while the production line of the slow-drawing nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -193,6 +212,7 @@ { "id": "place__s2_line_stopped", "name": "SlowNitrogenLineStopped", + "description": "Marker token present while the line of the slow-drawing nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -203,6 +223,7 @@ { "id": "place__s2_stockouts", "name": "SlowNitrogenStockouts", + "description": "Cumulative count of times the tank of the slow-drawing nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -213,6 +234,7 @@ { "id": "place__s2_consumed", "name": "SlowNitrogenConsumed", + "description": "Cumulative units drawn by the process of the slow-drawing nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -223,6 +245,7 @@ { "id": "place__s2_evaporated", "name": "SlowNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the slow-drawing nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -233,6 +256,7 @@ { "id": "place__s2_contents", "name": "SlowNitrogenContents", + "description": "Units of liquid in the tank of the slow-drawing nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -243,6 +267,7 @@ { "id": "place__s2_ullage", "name": "SlowNitrogenUllage", + "description": "Empty space in the tank of the slow-drawing nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -255,6 +280,7 @@ { "id": "transition__s1_draw", "name": "Draw a unit (SteadyNitrogen)", + "description": "The process of the steady nitrogen customer draws one unit from the tank at rate draw_1 while draw_enabled is set (a 1e-9 floor keeps the rate positive), moving it to Consumed and freeing one unit of ullage.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -281,6 +307,7 @@ { "id": "transition__s1_boil_off", "name": "Boil off a unit (SteadyNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the steady nitrogen customer at boiloff_rate into Evaporated, freeing one unit of ullage, whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -307,6 +334,7 @@ { "id": "transition__s1_raise_order", "name": "Raise an order, level below trigger (SteadyNitrogen)", + "description": "Once the tank of the steady nitrogen customer holds fewer than 16 units (inhibitor arc on Contents), an order is placed at review_rate, taking the single order permit.", "inputArcs": [ { "placeId": "place__s1_order_permits", @@ -334,6 +362,7 @@ { "id": "transition__s1_dispatch", "name": "Dispatch a tanker (SteadyNitrogen)", + "description": "Pairs the open order of the steady nitrogen customer with an idle tanker and loads it at loading_rate.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -361,6 +390,7 @@ { "id": "transition__s1_arrive", "name": "Unload the tanker (SteadyNitrogen)", + "description": "The journey to the steady nitrogen customer takes 6 hours on average, scaled by route_scale. On arrival 12 units go into the tank (which needs 12 units of ullage), the order permit is returned, a delivery is counted, and the tanker heads for Returning.", "inputArcs": [ { "placeId": "place__s1_on_route", @@ -400,6 +430,7 @@ { "id": "transition__s1_vent", "name": "Vent through the relief valve (SteadyNitrogen)", + "description": "With no ullage left in the tank of the steady nitrogen customer, boil-off gas vents one unit at instant_rate, leaving one unit of ullage free.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -431,6 +462,7 @@ { "id": "transition__s1_stop_line", "name": "Stop the line (SteadyNitrogen)", + "description": "When the tank of the steady nitrogen customer is empty (inhibitor arc on Contents) the running line stops at instant_rate and a stockout is counted.", "inputArcs": [ { "placeId": "place__s1_line_running", @@ -462,6 +494,7 @@ { "id": "transition__s1_resume_line", "name": "Resume the line (SteadyNitrogen)", + "description": "The stopped line of the steady nitrogen customer restarts at instant_rate once the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s1_line_stopped", @@ -489,6 +522,7 @@ { "id": "transition__s2_draw", "name": "Draw a unit (SlowNitrogen)", + "description": "The process of the slow-drawing nitrogen customer draws one unit from the tank at rate draw_2 while draw_enabled is set (a 1e-9 floor keeps the rate positive), moving it to Consumed and freeing one unit of ullage.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -515,6 +549,7 @@ { "id": "transition__s2_boil_off", "name": "Boil off a unit (SlowNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the slow-drawing nitrogen customer at boiloff_rate into Evaporated, freeing one unit of ullage, whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -541,6 +576,7 @@ { "id": "transition__s2_raise_order", "name": "Raise an order, level below trigger (SlowNitrogen)", + "description": "Once the tank of the slow-drawing nitrogen customer holds fewer than 6 units (inhibitor arc on Contents), an order is placed at review_rate, taking the single order permit.", "inputArcs": [ { "placeId": "place__s2_order_permits", @@ -568,6 +604,7 @@ { "id": "transition__s2_dispatch", "name": "Dispatch a tanker (SlowNitrogen)", + "description": "Pairs the open order of the slow-drawing nitrogen customer with an idle tanker and loads it at loading_rate.", "inputArcs": [ { "placeId": "place__s2_order_placed", @@ -595,6 +632,7 @@ { "id": "transition__s2_arrive", "name": "Unload the tanker (SlowNitrogen)", + "description": "The journey to the slow-drawing nitrogen customer takes 9 hours on average, scaled by route_scale. On arrival 12 units go into the tank (which needs 12 units of ullage), the order permit is returned, a delivery is counted, and the tanker heads for Returning.", "inputArcs": [ { "placeId": "place__s2_on_route", @@ -634,6 +672,7 @@ { "id": "transition__s2_vent", "name": "Vent through the relief valve (SlowNitrogen)", + "description": "With no ullage left in the tank of the slow-drawing nitrogen customer, boil-off gas vents one unit at instant_rate, leaving one unit of ullage free.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -665,6 +704,7 @@ { "id": "transition__s2_stop_line", "name": "Stop the line (SlowNitrogen)", + "description": "When the tank of the slow-drawing nitrogen customer is empty (inhibitor arc on Contents) the running line stops at instant_rate and a stockout is counted.", "inputArcs": [ { "placeId": "place__s2_line_running", @@ -696,6 +736,7 @@ { "id": "transition__s2_resume_line", "name": "Resume the line (SlowNitrogen)", + "description": "The stopped line of the slow-drawing nitrogen customer restarts at instant_rate once the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s2_line_stopped", @@ -723,6 +764,7 @@ { "id": "transition__return_to_depot", "name": "Return a tanker to the depot", + "description": "The empty tanker reaches the depot at rate 1/return_time and is idle again.", "inputArcs": [ { "placeId": "place__returning", @@ -974,5 +1016,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Gases 2 — stochastic net, two customers on one tanker" + "title": "Gases 2 — stochastic net, two customers on one tanker", + "description": "Stochastic net with two nitrogen customers, one drawing steadily and one slowly, sharing a single tanker. Draw, boil-off, order review, loading, the journey, and the return trip are all exponential rates, so delivery delay has a tail and the two customers contend for the same trailer." } diff --git a/apps/petrinaut-website/src/examples/models/gases-3-cpn.json b/apps/petrinaut-website/src/examples/models/gases-3-cpn.json index 6c0226d031e..2f230237f8c 100644 --- a/apps/petrinaut-website/src/examples/models/gases-3-cpn.json +++ b/apps/petrinaut-website/src/examples/models/gases-3-cpn.json @@ -3,6 +3,7 @@ { "id": "place__idle_tankers", "name": "IdleTankers", + "description": "Tankers parked at the depot and free to be dispatched, each tagged with the product it carries.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -13,6 +14,7 @@ { "id": "place__loads_delivered", "name": "LoadsDelivered", + "description": "Cumulative count of tanker deliveries unloaded at a customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -23,6 +25,7 @@ { "id": "place__returning", "name": "Returning", + "description": "Tankers on the empty run back to the depot after unloading, keeping their product tag; the return trip takes return_time hours on average.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -33,6 +36,7 @@ { "id": "place__s1_order_placed", "name": "SteadyNitrogenOrderPlaced", + "description": "An open order from the steady nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -43,6 +47,7 @@ { "id": "place__s1_order_permits", "name": "SteadyNitrogenOrderPermits", + "description": "The single permit that lets the steady nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -53,6 +58,7 @@ { "id": "place__s1_on_route", "name": "SteadyNitrogenOnRoute", + "description": "A dispatched tanker travelling to the steady nitrogen customer with a 12-unit load.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__s1_vented", "name": "SteadyNitrogenVented", + "description": "Cumulative units of product the steady nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__s1_line_running", "name": "SteadyNitrogenLineRunning", + "description": "Marker token present while the production line of the steady nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__s1_line_stopped", "name": "SteadyNitrogenLineStopped", + "description": "Marker token present while the line of the steady nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__s1_stockouts", "name": "SteadyNitrogenStockouts", + "description": "Cumulative count of times the tank of the steady nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__s1_consumed", "name": "SteadyNitrogenConsumed", + "description": "Cumulative units drawn by the process of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__s1_evaporated", "name": "SteadyNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the steady nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__s1_contents", "name": "SteadyNitrogenContents", + "description": "Units of liquid in the tank of the steady nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -133,6 +146,7 @@ { "id": "place__s1_ullage", "name": "SteadyNitrogenUllage", + "description": "Empty space in the tank of the steady nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -143,6 +157,7 @@ { "id": "place__s2_order_placed", "name": "SlowNitrogenOrderPlaced", + "description": "An open order from the slow-drawing nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -153,6 +168,7 @@ { "id": "place__s2_order_permits", "name": "SlowNitrogenOrderPermits", + "description": "The single permit that lets the slow-drawing nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -163,6 +179,7 @@ { "id": "place__s2_on_route", "name": "SlowNitrogenOnRoute", + "description": "A dispatched tanker travelling to the slow-drawing nitrogen customer with a 12-unit load.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -173,6 +190,7 @@ { "id": "place__s2_vented", "name": "SlowNitrogenVented", + "description": "Cumulative units of product the slow-drawing nitrogen customer lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -183,6 +201,7 @@ { "id": "place__s2_line_running", "name": "SlowNitrogenLineRunning", + "description": "Marker token present while the production line of the slow-drawing nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -193,6 +212,7 @@ { "id": "place__s2_line_stopped", "name": "SlowNitrogenLineStopped", + "description": "Marker token present while the line of the slow-drawing nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -203,6 +223,7 @@ { "id": "place__s2_stockouts", "name": "SlowNitrogenStockouts", + "description": "Cumulative count of times the tank of the slow-drawing nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -213,6 +234,7 @@ { "id": "place__s2_consumed", "name": "SlowNitrogenConsumed", + "description": "Cumulative units drawn by the process of the slow-drawing nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -223,6 +245,7 @@ { "id": "place__s2_evaporated", "name": "SlowNitrogenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the slow-drawing nitrogen customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -233,6 +256,7 @@ { "id": "place__s2_contents", "name": "SlowNitrogenContents", + "description": "Units of liquid in the tank of the slow-drawing nitrogen customer. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -243,6 +267,7 @@ { "id": "place__s2_ullage", "name": "SlowNitrogenUllage", + "description": "Empty space in the tank of the slow-drawing nitrogen customer, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -253,6 +278,7 @@ { "id": "place__s3_order_placed", "name": "CriticalOxygenOrderPlaced", + "description": "An open order from the oxygen customer, a metals plant for which supply is critical, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -263,6 +289,7 @@ { "id": "place__s3_order_permits", "name": "CriticalOxygenOrderPermits", + "description": "The single permit that lets the oxygen customer, a metals plant for which supply is critical have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -273,6 +300,7 @@ { "id": "place__s3_on_route", "name": "CriticalOxygenOnRoute", + "description": "A dispatched tanker travelling to the oxygen customer, a metals plant for which supply is critical with a 12-unit load.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -283,6 +311,7 @@ { "id": "place__s3_vented", "name": "CriticalOxygenVented", + "description": "Cumulative units of product the oxygen customer, a metals plant for which supply is critical lost through the relief valve because the tank was full.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -293,6 +322,7 @@ { "id": "place__s3_line_running", "name": "CriticalOxygenLineRunning", + "description": "Marker token present while the production line of the oxygen customer, a metals plant for which supply is critical is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -303,6 +333,7 @@ { "id": "place__s3_line_stopped", "name": "CriticalOxygenLineStopped", + "description": "Marker token present while the line of the oxygen customer, a metals plant for which supply is critical is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -313,6 +344,7 @@ { "id": "place__s3_stockouts", "name": "CriticalOxygenStockouts", + "description": "Cumulative count of times the tank of the oxygen customer, a metals plant for which supply is critical ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -323,6 +355,7 @@ { "id": "place__s3_consumed", "name": "CriticalOxygenConsumed", + "description": "Cumulative units drawn by the process of the oxygen customer, a metals plant for which supply is critical.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -333,6 +366,7 @@ { "id": "place__s3_evaporated", "name": "CriticalOxygenEvaporated", + "description": "Cumulative units lost to boil-off from the cryogenic tank of the oxygen customer, a metals plant for which supply is critical.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -343,6 +377,7 @@ { "id": "place__s3_contents", "name": "CriticalOxygenContents", + "description": "Units of liquid in the tank of the oxygen customer, a metals plant for which supply is critical. Draw and boil-off take one at a time and a delivery adds 12; Contents plus Ullage is the tank's capacity.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -353,6 +388,7 @@ { "id": "place__s3_ullage", "name": "CriticalOxygenUllage", + "description": "Empty space in the tank of the oxygen customer, a metals plant for which supply is critical, in units. Every unit drawn, boiled off, or vented frees one; a delivery needs 12 free units to unload.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -365,6 +401,7 @@ { "id": "transition__s1_draw", "name": "Draw a unit (SteadyNitrogen)", + "description": "The process of the steady nitrogen customer draws one unit from the tank at rate draw_1, moving it to Consumed and freeing one unit of ullage.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -391,6 +428,7 @@ { "id": "transition__s1_boil_off", "name": "Boil off a unit (SteadyNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the steady nitrogen customer at boiloff_rate into Evaporated, freeing one unit of ullage, whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -417,6 +455,7 @@ { "id": "transition__s1_raise_order", "name": "Raise an order, level below trigger (SteadyNitrogen)", + "description": "Once the tank of the steady nitrogen customer holds fewer than 16 units (inhibitor arc on Contents), an order is placed at review_rate, taking the single order permit.", "inputArcs": [ { "placeId": "place__s1_order_permits", @@ -444,6 +483,7 @@ { "id": "transition__s1_dispatch", "name": "Dispatch a tanker (SteadyNitrogen)", + "description": "Loads an idle tanker for the open order of the steady nitrogen customer at loading_rate, but only a tanker carrying nitrogen: a mismatched tanker's rate is 1e-9. The tanker's product travels with it onto the route.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -471,6 +511,7 @@ { "id": "transition__s1_arrive", "name": "Unload the tanker (SteadyNitrogen)", + "description": "The journey to the steady nitrogen customer takes 6 hours on average, scaled by route_scale. On arrival 12 units go into the tank (which needs 12 units of ullage), the order permit is returned, a delivery is counted, and the tanker heads for Returning.", "inputArcs": [ { "placeId": "place__s1_on_route", @@ -510,6 +551,7 @@ { "id": "transition__s1_vent", "name": "Vent through the relief valve (SteadyNitrogen)", + "description": "With no ullage left in the tank of the steady nitrogen customer, boil-off gas vents one unit at instant_rate, leaving one unit of ullage free.", "inputArcs": [ { "placeId": "place__s1_contents", @@ -541,6 +583,7 @@ { "id": "transition__s1_stop_line", "name": "Stop the line (SteadyNitrogen)", + "description": "When the tank of the steady nitrogen customer is empty (inhibitor arc on Contents) the running line stops at instant_rate and a stockout is counted.", "inputArcs": [ { "placeId": "place__s1_line_running", @@ -572,6 +615,7 @@ { "id": "transition__s1_resume_line", "name": "Resume the line (SteadyNitrogen)", + "description": "The stopped line of the steady nitrogen customer restarts at instant_rate once the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s1_line_stopped", @@ -599,6 +643,7 @@ { "id": "transition__s2_draw", "name": "Draw a unit (SlowNitrogen)", + "description": "The process of the slow-drawing nitrogen customer draws one unit from the tank at rate draw_2, moving it to Consumed and freeing one unit of ullage.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -625,6 +670,7 @@ { "id": "transition__s2_boil_off", "name": "Boil off a unit (SlowNitrogen)", + "description": "Heat leak evaporates one unit from the tank of the slow-drawing nitrogen customer at boiloff_rate into Evaporated, freeing one unit of ullage, whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -651,6 +697,7 @@ { "id": "transition__s2_raise_order", "name": "Raise an order, level below trigger (SlowNitrogen)", + "description": "Once the tank of the slow-drawing nitrogen customer holds fewer than 6 units (inhibitor arc on Contents), an order is placed at review_rate, taking the single order permit.", "inputArcs": [ { "placeId": "place__s2_order_permits", @@ -678,6 +725,7 @@ { "id": "transition__s2_dispatch", "name": "Dispatch a tanker (SlowNitrogen)", + "description": "Loads an idle tanker for the open order of the slow-drawing nitrogen customer at loading_rate, but only a tanker carrying nitrogen: a mismatched tanker's rate is 1e-9. The tanker's product travels with it onto the route.", "inputArcs": [ { "placeId": "place__s2_order_placed", @@ -705,6 +753,7 @@ { "id": "transition__s2_arrive", "name": "Unload the tanker (SlowNitrogen)", + "description": "The journey to the slow-drawing nitrogen customer takes 9 hours on average, scaled by route_scale. On arrival 12 units go into the tank (which needs 12 units of ullage), the order permit is returned, a delivery is counted, and the tanker heads for Returning.", "inputArcs": [ { "placeId": "place__s2_on_route", @@ -744,6 +793,7 @@ { "id": "transition__s2_vent", "name": "Vent through the relief valve (SlowNitrogen)", + "description": "With no ullage left in the tank of the slow-drawing nitrogen customer, boil-off gas vents one unit at instant_rate, leaving one unit of ullage free.", "inputArcs": [ { "placeId": "place__s2_contents", @@ -775,6 +825,7 @@ { "id": "transition__s2_stop_line", "name": "Stop the line (SlowNitrogen)", + "description": "When the tank of the slow-drawing nitrogen customer is empty (inhibitor arc on Contents) the running line stops at instant_rate and a stockout is counted.", "inputArcs": [ { "placeId": "place__s2_line_running", @@ -806,6 +857,7 @@ { "id": "transition__s2_resume_line", "name": "Resume the line (SlowNitrogen)", + "description": "The stopped line of the slow-drawing nitrogen customer restarts at instant_rate once the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s2_line_stopped", @@ -833,6 +885,7 @@ { "id": "transition__s3_draw", "name": "Draw a unit (CriticalOxygen)", + "description": "The process of the oxygen customer, a metals plant for which supply is critical draws one unit from the tank at rate draw_3, moving it to Consumed and freeing one unit of ullage.", "inputArcs": [ { "placeId": "place__s3_contents", @@ -859,6 +912,7 @@ { "id": "transition__s3_boil_off", "name": "Boil off a unit (CriticalOxygen)", + "description": "Heat leak evaporates one unit from the tank of the oxygen customer, a metals plant for which supply is critical at boiloff_rate into Evaporated, freeing one unit of ullage, whether or not the customer is drawing.", "inputArcs": [ { "placeId": "place__s3_contents", @@ -885,6 +939,7 @@ { "id": "transition__s3_raise_order", "name": "Raise an order, level below trigger (CriticalOxygen)", + "description": "Once the tank of the oxygen customer, a metals plant for which supply is critical holds fewer than 20 units (inhibitor arc on Contents), an order is placed at review_rate, taking the single order permit.", "inputArcs": [ { "placeId": "place__s3_order_permits", @@ -912,6 +967,7 @@ { "id": "transition__s3_dispatch", "name": "Dispatch a tanker (CriticalOxygen)", + "description": "Loads an idle tanker for the open order of the oxygen customer, a metals plant for which supply is critical at loading_rate, but only a tanker carrying oxygen: a mismatched tanker's rate is 1e-9. The tanker's product travels with it onto the route.", "inputArcs": [ { "placeId": "place__s3_order_placed", @@ -939,6 +995,7 @@ { "id": "transition__s3_arrive", "name": "Unload the tanker (CriticalOxygen)", + "description": "The journey to the oxygen customer, a metals plant for which supply is critical takes 12 hours on average, scaled by route_scale. On arrival 12 units go into the tank (which needs 12 units of ullage), the order permit is returned, a delivery is counted, and the tanker heads for Returning.", "inputArcs": [ { "placeId": "place__s3_on_route", @@ -978,6 +1035,7 @@ { "id": "transition__s3_vent", "name": "Vent through the relief valve (CriticalOxygen)", + "description": "With no ullage left in the tank of the oxygen customer, a metals plant for which supply is critical, boil-off gas vents one unit at instant_rate, leaving one unit of ullage free.", "inputArcs": [ { "placeId": "place__s3_contents", @@ -1009,6 +1067,7 @@ { "id": "transition__s3_stop_line", "name": "Stop the line (CriticalOxygen)", + "description": "When the tank of the oxygen customer, a metals plant for which supply is critical is empty (inhibitor arc on Contents) the running line stops at instant_rate and a stockout is counted.", "inputArcs": [ { "placeId": "place__s3_line_running", @@ -1040,6 +1099,7 @@ { "id": "transition__s3_resume_line", "name": "Resume the line (CriticalOxygen)", + "description": "The stopped line of the oxygen customer, a metals plant for which supply is critical restarts at instant_rate once the read arc sees product back in the tank.", "inputArcs": [ { "placeId": "place__s3_line_stopped", @@ -1067,6 +1127,7 @@ { "id": "transition__return_to_depot", "name": "Return a tanker to the depot", + "description": "The empty tanker reaches the depot at rate 1/return_time and is idle again, still carrying its product tag.", "inputArcs": [ { "placeId": "place__returning", @@ -1091,6 +1152,7 @@ { "id": "type__tanker", "name": "Tanker", + "description": "A road tanker, tagged with the product it carries (nitrogen or oxygen); only a tanker of the matching product can serve an order.", "iconSlug": "circle", "displayColor": "#0ea5e9", "elements": [ @@ -1296,5 +1358,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Gases 3 \u2014 coloured net, three customers and a mixed fleet" + "title": "Gases 3 — coloured net, three customers and a mixed fleet", + "description": "Coloured net with three customers, two on nitrogen and one on oxygen, served by a mixed fleet. Each tanker carries its product as a token attribute and a dispatch only takes a tanker whose product matches the order, so the fleet's composition, not just its size, decides who waits." } diff --git a/apps/petrinaut-website/src/examples/models/gases-4-dcpn.json b/apps/petrinaut-website/src/examples/models/gases-4-dcpn.json index cf0b2b1947f..ce46d796856 100644 --- a/apps/petrinaut-website/src/examples/models/gases-4-dcpn.json +++ b/apps/petrinaut-website/src/examples/models/gases-4-dcpn.json @@ -3,6 +3,7 @@ { "id": "place__idle_tankers", "name": "IdleTankers", + "description": "Tankers parked at the depot and free to be dispatched, each carrying its product, payload, and whether it is a spot hire.", "colorId": "type__tanker", "dynamicsEnabled": false, "differentialEquationId": null, @@ -13,6 +14,7 @@ { "id": "place__loads_delivered", "name": "LoadsDelivered", + "description": "Cumulative count of tanker deliveries unloaded at a customer.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -23,6 +25,7 @@ { "id": "place__returning", "name": "Returning", + "description": "Tankers on the empty run back to the depot; the returning dynamics count their journey clock down to zero.", "colorId": "type__tanker", "dynamicsEnabled": true, "differentialEquationId": "de__returning", @@ -33,6 +36,7 @@ { "id": "place__open_orders", "name": "OpenOrders", + "description": "Count of orders currently open across all customers. Hiring a tanker needs at least three; releasing a hired one needs none.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -43,6 +47,7 @@ { "id": "place__hires", "name": "Hires", + "description": "Cumulative count of spot-hired tankers.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -53,6 +58,7 @@ { "id": "place__plant", "name": "Plant", + "description": "Single token for the air separation plant that fills the tankers: up is 1 while producing and 0 during an outage. Dispatch reads it to pick the normal or re-sourced route.", "colorId": "type__plant", "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__outages", "name": "Outages", + "description": "Cumulative count of plant trips.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__s1_order_placed", "name": "SteadyNitrogenOrderPlaced", + "description": "An open order from the steady nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__s1_order_permits", "name": "SteadyNitrogenOrderPermits", + "description": "The single permit that lets the steady nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__s1_on_route", "name": "SteadyNitrogenOnRoute", + "description": "A tanker on its way to the steady nitrogen customer. Its journey clock counts down under the on-route dynamics and the delivery unloads when it reaches zero.", "colorId": "type__tanker", "dynamicsEnabled": true, "differentialEquationId": "de__on_route", @@ -103,6 +113,7 @@ { "id": "place__s1_vented", "name": "SteadyNitrogenVented", + "description": "Cumulative count of relief-valve openings at the tank of the steady nitrogen customer, each losing vent_loss units.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__s1_line_running", "name": "SteadyNitrogenLineRunning", + "description": "Marker token present while the production line of the steady nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__s1_line_stopped", "name": "SteadyNitrogenLineStopped", + "description": "Marker token present while the line of the steady nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -133,6 +146,7 @@ { "id": "place__s1_stockouts", "name": "SteadyNitrogenStockouts", + "description": "Cumulative count of times the tank of the steady nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -143,6 +157,7 @@ { "id": "place__s1_tank", "name": "SteadyNitrogenTank", + "description": "The cryogenic tank of the steady nitrogen customer as a single token. Its level and pressure evolve continuously under the Tank dynamics; the ordering, venting, and line transitions read them and fire on threshold crossings (trigger_1 for ordering).", "colorId": "type__tank", "dynamicsEnabled": true, "differentialEquationId": "de__tank", @@ -153,6 +168,7 @@ { "id": "place__s2_order_placed", "name": "SlowNitrogenOrderPlaced", + "description": "An open order from the slow-drawing nitrogen customer, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -163,6 +179,7 @@ { "id": "place__s2_order_permits", "name": "SlowNitrogenOrderPermits", + "description": "The single permit that lets the slow-drawing nitrogen customer have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -173,6 +190,7 @@ { "id": "place__s2_on_route", "name": "SlowNitrogenOnRoute", + "description": "A tanker on its way to the slow-drawing nitrogen customer. Its journey clock counts down under the on-route dynamics and the delivery unloads when it reaches zero.", "colorId": "type__tanker", "dynamicsEnabled": true, "differentialEquationId": "de__on_route", @@ -183,6 +201,7 @@ { "id": "place__s2_vented", "name": "SlowNitrogenVented", + "description": "Cumulative count of relief-valve openings at the tank of the slow-drawing nitrogen customer, each losing vent_loss units.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -193,6 +212,7 @@ { "id": "place__s2_line_running", "name": "SlowNitrogenLineRunning", + "description": "Marker token present while the production line of the slow-drawing nitrogen customer is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -203,6 +223,7 @@ { "id": "place__s2_line_stopped", "name": "SlowNitrogenLineStopped", + "description": "Marker token present while the line of the slow-drawing nitrogen customer is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -213,6 +234,7 @@ { "id": "place__s2_stockouts", "name": "SlowNitrogenStockouts", + "description": "Cumulative count of times the tank of the slow-drawing nitrogen customer ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -223,6 +245,7 @@ { "id": "place__s2_tank", "name": "SlowNitrogenTank", + "description": "The cryogenic tank of the slow-drawing nitrogen customer as a single token. Its level and pressure evolve continuously under the Tank dynamics; the ordering, venting, and line transitions read them and fire on threshold crossings (trigger_2 for ordering).", "colorId": "type__tank", "dynamicsEnabled": true, "differentialEquationId": "de__tank", @@ -233,6 +256,7 @@ { "id": "place__s3_order_placed", "name": "CriticalOxygenOrderPlaced", + "description": "An open order from the oxygen customer, a metals plant for which supply is critical, waiting for a tanker. At most one is open at a time because raising an order also takes the order permit.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -243,6 +267,7 @@ { "id": "place__s3_order_permits", "name": "CriticalOxygenOrderPermits", + "description": "The single permit that lets the oxygen customer, a metals plant for which supply is critical have one order open at a time. Raising an order takes it; unloading the delivery hands it back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -253,6 +278,7 @@ { "id": "place__s3_on_route", "name": "CriticalOxygenOnRoute", + "description": "A tanker on its way to the oxygen customer, a metals plant for which supply is critical. Its journey clock counts down under the on-route dynamics and the delivery unloads when it reaches zero.", "colorId": "type__tanker", "dynamicsEnabled": true, "differentialEquationId": "de__on_route", @@ -263,6 +289,7 @@ { "id": "place__s3_vented", "name": "CriticalOxygenVented", + "description": "Cumulative count of relief-valve openings at the tank of the oxygen customer, a metals plant for which supply is critical, each losing vent_loss units.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -273,6 +300,7 @@ { "id": "place__s3_line_running", "name": "CriticalOxygenLineRunning", + "description": "Marker token present while the production line of the oxygen customer, a metals plant for which supply is critical is running on gas from the tank.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -283,6 +311,7 @@ { "id": "place__s3_line_stopped", "name": "CriticalOxygenLineStopped", + "description": "Marker token present while the line of the oxygen customer, a metals plant for which supply is critical is stopped because the tank is empty.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -293,6 +322,7 @@ { "id": "place__s3_stockouts", "name": "CriticalOxygenStockouts", + "description": "Cumulative count of times the tank of the oxygen customer, a metals plant for which supply is critical ran dry and stopped the line.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -303,6 +333,7 @@ { "id": "place__s3_tank", "name": "CriticalOxygenTank", + "description": "The cryogenic tank of the oxygen customer, a metals plant for which supply is critical as a single token. Its level and pressure evolve continuously under the Tank dynamics; the ordering, venting, and line transitions read them and fire on threshold crossings (trigger_3 for ordering).", "colorId": "type__tank", "dynamicsEnabled": true, "differentialEquationId": "de__tank", @@ -315,6 +346,7 @@ { "id": "transition__s1_raise_order", "name": "Raise an order (SteadyNitrogen)", + "description": "Fires the moment the tank level of the steady nitrogen customer drops below trigger_1, taking the order permit and adding to OpenOrders. The tank token is read and put back unchanged.", "inputArcs": [ { "placeId": "place__s1_tank", @@ -350,6 +382,7 @@ { "id": "transition__s1_dispatch", "name": "Dispatch a tanker (SteadyNitrogen)", + "description": "With the plant up, an idle tanker carrying nitrogen loads for the steady nitrogen customer at loading_rate (a mismatched tanker's rate is 1e-9) and sets off with a lognormal journey around 6 hours times route_scale, spread route_spread. Closes one open order.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -387,6 +420,7 @@ { "id": "transition__s1_dispatch_resourced", "name": "Dispatch a re-sourced tanker (SteadyNitrogen)", + "description": "The same dispatch for the steady nitrogen customer while the plant is down: product comes from a farther source, so the journey mean is multiplied by outage_route_penalty.", "inputArcs": [ { "placeId": "place__s1_order_placed", @@ -424,6 +458,7 @@ { "id": "transition__s1_arrive", "name": "Unload the tanker (SteadyNitrogen)", + "description": "When the journey clock reaches zero the tanker discharges as much of its payload as the tank of the steady nitrogen customer has room for, hands back the order permit, counts a delivery, and starts the return run.", "inputArcs": [ { "placeId": "place__s1_on_route", @@ -463,6 +498,7 @@ { "id": "transition__s1_vent", "name": "Vent through the relief valve (SteadyNitrogen)", + "description": "Pressure in the tank of the steady nitrogen customer reaching relief_setpoint lifts the valve: vent_loss units of product are lost and pressure drops to the setpoint minus relief_reseat, so the valve cycles while the tank stays nearly full.", "inputArcs": [ { "placeId": "place__s1_tank", @@ -489,6 +525,7 @@ { "id": "transition__s1_stop_line", "name": "Stop the line (SteadyNitrogen)", + "description": "The line of the steady nitrogen customer stops the instant the tank level reaches zero and a stockout is counted.", "inputArcs": [ { "placeId": "place__s1_tank", @@ -524,6 +561,7 @@ { "id": "transition__s1_resume_line", "name": "Resume the line (SteadyNitrogen)", + "description": "The line of the steady nitrogen customer restarts as soon as the tank level is above zero again.", "inputArcs": [ { "placeId": "place__s1_tank", @@ -555,6 +593,7 @@ { "id": "transition__s2_raise_order", "name": "Raise an order (SlowNitrogen)", + "description": "Fires the moment the tank level of the slow-drawing nitrogen customer drops below trigger_2, taking the order permit and adding to OpenOrders. The tank token is read and put back unchanged.", "inputArcs": [ { "placeId": "place__s2_tank", @@ -590,6 +629,7 @@ { "id": "transition__s2_dispatch", "name": "Dispatch a tanker (SlowNitrogen)", + "description": "With the plant up, an idle tanker carrying nitrogen loads for the slow-drawing nitrogen customer at loading_rate (a mismatched tanker's rate is 1e-9) and sets off with a lognormal journey around 9 hours times route_scale, spread route_spread. Closes one open order.", "inputArcs": [ { "placeId": "place__s2_order_placed", @@ -627,6 +667,7 @@ { "id": "transition__s2_dispatch_resourced", "name": "Dispatch a re-sourced tanker (SlowNitrogen)", + "description": "The same dispatch for the slow-drawing nitrogen customer while the plant is down: product comes from a farther source, so the journey mean is multiplied by outage_route_penalty.", "inputArcs": [ { "placeId": "place__s2_order_placed", @@ -664,6 +705,7 @@ { "id": "transition__s2_arrive", "name": "Unload the tanker (SlowNitrogen)", + "description": "When the journey clock reaches zero the tanker discharges as much of its payload as the tank of the slow-drawing nitrogen customer has room for, hands back the order permit, counts a delivery, and starts the return run.", "inputArcs": [ { "placeId": "place__s2_on_route", @@ -703,6 +745,7 @@ { "id": "transition__s2_vent", "name": "Vent through the relief valve (SlowNitrogen)", + "description": "Pressure in the tank of the slow-drawing nitrogen customer reaching relief_setpoint lifts the valve: vent_loss units of product are lost and pressure drops to the setpoint minus relief_reseat, so the valve cycles while the tank stays nearly full.", "inputArcs": [ { "placeId": "place__s2_tank", @@ -729,6 +772,7 @@ { "id": "transition__s2_stop_line", "name": "Stop the line (SlowNitrogen)", + "description": "The line of the slow-drawing nitrogen customer stops the instant the tank level reaches zero and a stockout is counted.", "inputArcs": [ { "placeId": "place__s2_tank", @@ -764,6 +808,7 @@ { "id": "transition__s2_resume_line", "name": "Resume the line (SlowNitrogen)", + "description": "The line of the slow-drawing nitrogen customer restarts as soon as the tank level is above zero again.", "inputArcs": [ { "placeId": "place__s2_tank", @@ -795,6 +840,7 @@ { "id": "transition__s3_raise_order", "name": "Raise an order (CriticalOxygen)", + "description": "Fires the moment the tank level of the oxygen customer, a metals plant for which supply is critical drops below trigger_3, taking the order permit and adding to OpenOrders. The tank token is read and put back unchanged.", "inputArcs": [ { "placeId": "place__s3_tank", @@ -830,6 +876,7 @@ { "id": "transition__s3_dispatch", "name": "Dispatch a tanker (CriticalOxygen)", + "description": "With the plant up, an idle tanker carrying oxygen loads for the oxygen customer, a metals plant for which supply is critical at loading_rate (a mismatched tanker's rate is 1e-9) and sets off with a lognormal journey around 12 hours times route_scale, spread route_spread. Closes one open order.", "inputArcs": [ { "placeId": "place__s3_order_placed", @@ -867,6 +914,7 @@ { "id": "transition__s3_dispatch_resourced", "name": "Dispatch a re-sourced tanker (CriticalOxygen)", + "description": "The same dispatch for the oxygen customer, a metals plant for which supply is critical while the plant is down: product comes from a farther source, so the journey mean is multiplied by outage_route_penalty.", "inputArcs": [ { "placeId": "place__s3_order_placed", @@ -904,6 +952,7 @@ { "id": "transition__s3_arrive", "name": "Unload the tanker (CriticalOxygen)", + "description": "When the journey clock reaches zero the tanker discharges as much of its payload as the tank of the oxygen customer, a metals plant for which supply is critical has room for, hands back the order permit, counts a delivery, and starts the return run.", "inputArcs": [ { "placeId": "place__s3_on_route", @@ -943,6 +992,7 @@ { "id": "transition__s3_vent", "name": "Vent through the relief valve (CriticalOxygen)", + "description": "Pressure in the tank of the oxygen customer, a metals plant for which supply is critical reaching relief_setpoint lifts the valve: vent_loss units of product are lost and pressure drops to the setpoint minus relief_reseat, so the valve cycles while the tank stays nearly full.", "inputArcs": [ { "placeId": "place__s3_tank", @@ -969,6 +1019,7 @@ { "id": "transition__s3_stop_line", "name": "Stop the line (CriticalOxygen)", + "description": "The line of the oxygen customer, a metals plant for which supply is critical stops the instant the tank level reaches zero and a stockout is counted.", "inputArcs": [ { "placeId": "place__s3_tank", @@ -1004,6 +1055,7 @@ { "id": "transition__s3_resume_line", "name": "Resume the line (CriticalOxygen)", + "description": "The line of the oxygen customer, a metals plant for which supply is critical restarts as soon as the tank level is above zero again.", "inputArcs": [ { "placeId": "place__s3_tank", @@ -1035,6 +1087,7 @@ { "id": "transition__return_to_depot", "name": "Return a tanker to the depot", + "description": "The return clock reaches zero and the tanker is idle at the depot again, keeping its product, payload, and hire status.", "inputArcs": [ { "placeId": "place__returning", @@ -1057,6 +1110,7 @@ { "id": "transition__hire_tanker", "name": "Hire a tanker", + "description": "With three or more orders open and hire_enabled set, a spot tanker is hired at hire_rate: it carries hired_payload of nitrogen (oxygen if hire_oxygen is set) and is flagged as hired.", "inputArcs": [ { "placeId": "place__open_orders", @@ -1083,6 +1137,7 @@ { "id": "transition__release_tanker", "name": "Release a hired tanker", + "description": "An idle hired tanker leaves the fleet as soon as no orders are open.", "inputArcs": [ { "placeId": "place__idle_tankers", @@ -1105,6 +1160,7 @@ { "id": "transition__plant_trips", "name": "Trip the air separation plant", + "description": "The plant trips at rate 1/uptime_hours while up, counting an outage.", "inputArcs": [ { "placeId": "place__plant", @@ -1131,6 +1187,7 @@ { "id": "transition__plant_recovers", "name": "Restart the air separation plant", + "description": "A tripped plant is back up at rate 1/repair_hours.", "inputArcs": [ { "placeId": "place__plant", @@ -1155,6 +1212,7 @@ { "id": "type__tanker", "name": "Tanker", + "description": "A road tanker: its journey clock (remaining), the product it carries, its payload in units, and whether it is a spot hire.", "iconSlug": "circle", "displayColor": "#0ea5e9", "elements": [ @@ -1183,6 +1241,7 @@ { "id": "type__tank", "name": "Tank", + "description": "A customer's cryogenic tank: liquid level, gas pressure, capacity, the customer's draw rate, cumulative units drawn, boiled off, and spilled, its criticality, and the product it holds.", "iconSlug": "circle", "displayColor": "#f97316", "elements": [ @@ -1236,6 +1295,7 @@ { "id": "type__plant", "name": "Plant", + "description": "The air separation plant supplying the depot: up is 1 when producing and 0 during an outage.", "iconSlug": "circle", "displayColor": "#ef4444", "elements": [ @@ -1615,5 +1675,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Gases 4 \u2014 dynamic coloured net" + "title": "Gases 4 — dynamic coloured net", + "description": "Dynamic coloured net: each customer's tank is one token whose level and pressure evolve continuously, with orders, venting, and line stops fired by threshold crossings rather than by counting units. Journeys are lognormal countdowns, spare tankers can be hired when orders pile up, and an air separation plant that trips forces deliveries onto longer re-sourced routes." } diff --git a/apps/petrinaut-website/src/examples/models/semiconductor-fab-drift.json b/apps/petrinaut-website/src/examples/models/semiconductor-fab-drift.json index a6ced807e89..d8a63a269e0 100644 --- a/apps/petrinaut-website/src/examples/models/semiconductor-fab-drift.json +++ b/apps/petrinaut-website/src/examples/models/semiconductor-fab-drift.json @@ -3,6 +3,7 @@ { "id": "place__fab_entrance", "name": "FabEntrance", + "description": "Lots created by demand but not yet released into the fab; they wait here for a WIP permit.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__aging", @@ -13,6 +14,7 @@ { "id": "place__wip_queue", "name": "WIPQueue", + "description": "Released lots waiting for a chamber. The urgency dynamics raise priority as the due date approaches, and wait_time drives the periodic priority updates.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__aging", @@ -23,6 +25,7 @@ { "id": "place__batch_queue", "name": "BatchQueue", + "description": "Lots at a furnace layer waiting to be loaded into a batch. batch_wait accumulates so a partial batch can start on timeout.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__batch_wait", @@ -33,6 +36,7 @@ { "id": "place__in_process", "name": "InProcess", + "description": "Lots inside a chamber. process_remaining counts down to zero, at which point the chamber holding the lot releases it.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__processing", @@ -43,6 +47,7 @@ { "id": "place__post_process", "name": "PostProcess", + "description": "Lots that have just finished a step, one layer further on, before routing back to the queue or, at layer 28, to final test.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__aging", @@ -53,6 +58,7 @@ { "id": "place__in_inspection", "name": "InInspection", + "description": "Lots on an inspection chamber. The inspection countdown runs like a process step but adds no defects.", "colorId": "type__lot", "dynamicsEnabled": true, "differentialEquationId": "de__processing", @@ -63,6 +69,7 @@ { "id": "place__finished", "name": "Finished", + "description": "Lots that completed all 28 layers with a defect count under scrap_threshold.", "colorId": "type__lot", "dynamicsEnabled": false, "differentialEquationId": null, @@ -73,6 +80,7 @@ { "id": "place__scrapped", "name": "Scrapped", + "description": "Lots lost, either failing final test with too many defects or destroyed when their chamber broke down mid-process.", "colorId": "type__lot", "dynamicsEnabled": false, "differentialEquationId": null, @@ -83,6 +91,7 @@ { "id": "place__wip_permits", "name": "WIPPermits", + "description": "Release permits capping work in progress at wip_cap lots. A release takes one; a finished or scrapped lot returns one.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -93,6 +102,7 @@ { "id": "place__chambers_available", "name": "ChambersAvailable", + "description": "Idle chambers ready to accept a lot, or furnaces still filling a batch. Each carries its condition, particle count, drift, and product qualification.", "colorId": "type__chamber", "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__chambers_processing", "name": "ChambersProcessing", + "description": "Chambers running a lot or batch. The wear dynamics raise condition and particle count while they run, and the diffusion step injects process noise.", "colorId": "type__chamber", "dynamicsEnabled": true, "differentialEquationId": "de__chamber_processing", @@ -113,6 +124,7 @@ { "id": "place__chambers_in_maintenance", "name": "ChambersInMaintenance", + "description": "Chambers with a crew member assigned, counting maintenance_remaining down to recalibration.", "colorId": "type__chamber", "dynamicsEnabled": true, "differentialEquationId": "de__chamber_maintenance", @@ -123,6 +135,7 @@ { "id": "place__chambers_broken", "name": "ChambersBroken", + "description": "Chambers that need maintenance or failed while no crew member was free; they wait here until one is released.", "colorId": "type__chamber", "dynamicsEnabled": false, "differentialEquationId": null, @@ -133,6 +146,7 @@ { "id": "place__maintenance_crew", "name": "MaintenanceCrew", + "description": "Technicians free to service or repair a chamber. Each maintenance holds one until it completes.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -143,6 +157,7 @@ { "id": "place__maintenance_events", "name": "MaintenanceEvents", + "description": "Cumulative count of maintenance visits, whether preventive or forced by a chamber's condition at the end of a step.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -153,6 +168,7 @@ { "id": "place__breakdown_events", "name": "BreakdownEvents", + "description": "Cumulative count of chamber breakdowns.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -163,6 +179,7 @@ { "id": "place__lots_released", "name": "LotsReleased", + "description": "Cumulative count of lots released into the fab.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -173,6 +190,7 @@ { "id": "place__lots_completed", "name": "LotsCompleted", + "description": "Cumulative count of lots that passed final test.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -183,6 +201,7 @@ { "id": "place__calibrations", "name": "Calibrations", + "description": "Cumulative count of completed maintenances, each of which resets condition and re-centres process drift.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -195,6 +214,7 @@ { "id": "transition__demand_logic", "name": "Demand arrives (logic)", + "description": "Logic lots arrive at half of demand_rate with a 200 hour due date and enter FabEntrance at layer 0.", "inputArcs": [], "outputArcs": [ { @@ -211,6 +231,7 @@ { "id": "transition__demand_memory", "name": "Demand arrives (memory)", + "description": "Memory lots arrive at 35% of demand_rate with a 180 hour due date.", "inputArcs": [], "outputArcs": [ { @@ -227,6 +248,7 @@ { "id": "transition__demand_analog", "name": "Demand arrives (analog)", + "description": "Analog lots arrive at 15% of demand_rate with a 250 hour due date.", "inputArcs": [], "outputArcs": [ { @@ -243,6 +265,7 @@ { "id": "transition__release_lot", "name": "Release lot into fab", + "description": "Moves a waiting lot into the WIP queue once a WIP permit is free and counts a release.", "inputArcs": [ { "placeId": "place__fab_entrance", @@ -274,6 +297,7 @@ { "id": "transition__priority_update", "name": "Update lot priority toward deadline", + "description": "After priority_update_interval hours of waiting, recomputes a queued lot's priority as target_cycle_time over the time left to its due date (10 once overdue, never below 1) and resets its wait clock.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -296,6 +320,7 @@ { "id": "transition__extend_deadline", "name": "Renegotiate deadline (lot past due)", + "description": "A lot more than deadline_grace_period hours past its due date gets a new due date one target_cycle_time ahead and its priority reset to 1.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -318,6 +343,7 @@ { "id": "transition__dispatch_litho", "name": "Dispatch lot to litho chamber", + "description": "Sends a queued lot at a litho layer (0, 4, 9, 12, 16, 20, 24) to an idle litho chamber qualified for its product type, drawing a lognormal process time around litho_time scaled by product type.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -349,6 +375,7 @@ { "id": "transition__dispatch_etch", "name": "Dispatch lot to etch chamber", + "description": "Sends a queued lot at an etch layer to an idle etch chamber qualified for its product type, drawing a lognormal process time around etch_time scaled by product type.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -380,6 +407,7 @@ { "id": "transition__enter_batch_queue", "name": "Lot enters furnace batch queue", + "description": "A queued lot at a furnace layer (2, 7, 14, 19, 22, 26) moves to the batch queue instead of being dispatched on its own.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -402,6 +430,7 @@ { "id": "transition__load_furnace", "name": "Load lot into furnace batch", + "description": "Loads a batch-queued lot into a qualified furnace chamber with room under batch_size. The chamber stays available while its batch fills; the lot's process time is lognormal around furnace_time.", "inputArcs": [ { "placeId": "place__batch_queue", @@ -433,6 +462,7 @@ { "id": "transition__start_furnace_full", "name": "Start furnace (batch full)", + "description": "A furnace whose batch_count has reached batch_size starts processing.", "inputArcs": [ { "placeId": "place__chambers_available", @@ -455,6 +485,7 @@ { "id": "transition__start_furnace_timeout", "name": "Start furnace (batch timeout)", + "description": "A furnace holding a partial batch starts anyway once one of its loaded lots has waited batch_timeout hours.", "inputArcs": [ { "placeId": "place__chambers_available", @@ -482,6 +513,7 @@ { "id": "transition__dispatch_inspect", "name": "Dispatch lot to inspection", + "description": "Sends a queued lot at an inspection layer (3, 6, 11, 18, 23) to an idle inspection chamber with a lognormal time around inspect_time.", "inputArcs": [ { "placeId": "place__wip_queue", @@ -513,6 +545,7 @@ { "id": "transition__process_complete_ok", "name": "Processing complete, chamber ok", + "description": "When a lot's countdown ends in its chamber, the lot advances one layer with defects added at base_defect_rate scaled up by the chamber's condition, particle count, and drift, and inherits the chamber's drift as process bias. The chamber returns to available with lots_processed incremented. Fires only while the chamber's condition is under maintenance_threshold.", "inputArcs": [ { "placeId": "place__in_process", @@ -544,6 +577,7 @@ { "id": "transition__furnace_batch_continues", "name": "Furnace lot completes (batch continues)", + "description": "A finished lot leaves a furnace that still holds other lots of its batch; the chamber keeps processing with batch_count reduced by one.", "inputArcs": [ { "placeId": "place__in_process", @@ -575,6 +609,7 @@ { "id": "transition__process_complete_maintenance", "name": "Processing complete, chamber needs maintenance", + "description": "As for a healthy chamber, except that condition has reached maintenance_threshold: the lot advances and the chamber goes to ChambersBroken to wait for crew, counting a maintenance event.", "inputArcs": [ { "placeId": "place__in_process", @@ -610,6 +645,7 @@ { "id": "transition__route_to_queue", "name": "Route lot back to WIP queue", + "description": "A lot below layer 28 returns to the WIP queue for its next step.", "inputArcs": [ { "placeId": "place__post_process", @@ -632,6 +668,7 @@ { "id": "transition__lot_passes", "name": "Lot passes final test", + "description": "A lot at layer 28 with defect_count under scrap_threshold is finished; its WIP permit is released and a completion counted.", "inputArcs": [ { "placeId": "place__post_process", @@ -662,6 +699,7 @@ { "id": "transition__lot_fails", "name": "Lot fails final test", + "description": "A lot at layer 28 with defect_count at or above scrap_threshold is scrapped and its WIP permit released.", "inputArcs": [ { "placeId": "place__post_process", @@ -688,6 +726,7 @@ { "id": "transition__inspection_complete", "name": "Inspection complete", + "description": "The inspection countdown ends: the lot advances a layer back to the WIP queue and the chamber returns to available.", "inputArcs": [ { "placeId": "place__in_inspection", @@ -719,6 +758,7 @@ { "id": "transition__start_maintenance", "name": "Start preventive maintenance", + "description": "An idle chamber whose condition has reached maintenance_threshold takes a crew member and begins a lognormal maintenance_duration.", "inputArcs": [ { "placeId": "place__chambers_available", @@ -750,6 +790,7 @@ { "id": "transition__maintenance_complete", "name": "Maintenance complete (drift recalibrated)", + "description": "Maintenance ends: condition resets to 0, particles to 0.05, and process_drift is re-centred within calibration_residual. The crew member is freed and a calibration counted.", "inputArcs": [ { "placeId": "place__chambers_in_maintenance", @@ -780,6 +821,7 @@ { "id": "transition__breakdown", "name": "Chamber breakdown", + "description": "A litho, etch, or inspection chamber fails mid-run at breakdown_base_rate grown exponentially with condition. The lot inside is scrapped and its WIP permit released; a free crew member starts a lognormal breakdown_repair_time repair at once.", "inputArcs": [ { "placeId": "place__chambers_processing", @@ -824,6 +866,7 @@ { "id": "transition__breakdown_no_crew", "name": "Chamber breakdown (no crew available)", + "description": "The same failure hazard when no crew member is free: the lot is scrapped and the chamber waits in ChambersBroken.", "inputArcs": [ { "placeId": "place__chambers_processing", @@ -868,6 +911,7 @@ { "id": "transition__crew_reaches_broken", "name": "Crew reaches broken chamber", + "description": "A freed crew member takes on a broken chamber and starts its repair, keeping any repair time already assigned.", "inputArcs": [ { "placeId": "place__chambers_broken", @@ -895,6 +939,7 @@ { "id": "transition__drift_diffusion", "name": "Process noise injection (diffusion step)", + "description": "Every diffusion_step hours a processing chamber's particle count and process drift take a Gaussian step scaled by their volatilities, the stochastic half of the drift that the wear dynamics revert.", "inputArcs": [ { "placeId": "place__chambers_processing", @@ -919,6 +964,7 @@ { "id": "type__lot", "name": "Lot", + "description": "A wafer lot: product type (0 logic, 1 memory, 2 analog), current layer of 28, priority, age, accumulated defects, remaining process time, wait and batch clocks, due date, process bias, and the chamber holding it (tool_id times 100 plus chamber index, or -1).", "iconSlug": "circle", "displayColor": "#8b5cf6", "elements": [ @@ -982,6 +1028,7 @@ { "id": "type__chamber", "name": "Chamber", + "description": "A process chamber within a tool: machine group (0 litho, 1 etch, 2 furnace, 3 inspection), tool and chamber index, product qualification, and its state: condition, particle count, hours since maintenance, maintenance countdown, diffusion clock, lots processed, process drift, and furnace batch count.", "iconSlug": "circle", "displayColor": "#06b6d4", "elements": [ @@ -1541,5 +1588,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Semiconductor fab \u2014 process drift & multi-chamber tools (v3)" + "title": "Semiconductor fab — process drift & multi-chamber tools (v3)", + "description": "A wafer fab as a dynamic coloured net: lots of three product types cycle through 28 layers across litho, etch, furnace, and inspection chambers grouped into multi-chamber tools. Chambers degrade, gather particles, and drift continuously; the defects a step adds scale with that state, and maintenance recalibrates a chamber at the cost of taking it and a crew member out of service." } diff --git a/apps/petrinaut-website/src/examples/models/truck-fleet-predictive-maintenance.json b/apps/petrinaut-website/src/examples/models/truck-fleet-predictive-maintenance.json index f34bdebd96d..00cecb15179 100644 --- a/apps/petrinaut-website/src/examples/models/truck-fleet-predictive-maintenance.json +++ b/apps/petrinaut-website/src/examples/models/truck-fleet-predictive-maintenance.json @@ -3,6 +3,7 @@ { "id": "place__load_board", "name": "LoadBoard", + "description": "Loads on offer, one token per load with its route class, distance, due time, and revenue. A load ages on the board and goes to another haulier after board_patience hours.", "colorId": "type__load", "dynamicsEnabled": true, "differentialEquationId": "de__waiting_load", @@ -13,6 +14,7 @@ { "id": "place__available", "name": "Available", + "description": "Trucks parked at the depot and ready to dispatch, carrying their accumulated brake, engine, and tyre wear.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -23,6 +25,7 @@ { "id": "place__drivers", "name": "Drivers", + "description": "Pool of free drivers. A dispatch takes one; the truck's return or tow hands one back.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -33,6 +36,7 @@ { "id": "place__on_route", "name": "OnRoute", + "description": "Loaded trucks driving to the customer. The loaded dynamics count km_remaining down and add wear and fuel burn scaled by speed, road severity, and route class.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__driving", @@ -43,6 +47,7 @@ { "id": "place__stranded", "name": "Stranded", + "description": "Trucks that failed at the roadside, waiting for a recovery unit.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -53,6 +58,7 @@ { "id": "place__recovery", "name": "RecoveryUnits", + "description": "Recovery vehicles free to attend a stranded truck.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -63,6 +69,7 @@ { "id": "place__under_recovery", "name": "UnderRecovery", + "description": "Stranded trucks with a recovery unit on scene, being towed back over tow_time hours on average.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -73,6 +80,7 @@ { "id": "place__returning", "name": "Returning", + "description": "Empty trucks driving back to the depot after a delivery, wearing at 70% of the loaded rate.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__returning", @@ -83,6 +91,7 @@ { "id": "place__depot_queue", "name": "DepotQueue", + "description": "Trucks that have just arrived at the depot, about to be parked, sent for service, or rested depending on wear and hours driven.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -93,6 +102,7 @@ { "id": "place__bays", "name": "Bays", + "description": "Free workshop bays. A service or repair holds one until it finishes.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -103,6 +113,7 @@ { "id": "place__technicians", "name": "Technicians", + "description": "Free workshop technicians. Each truck in a bay ties up one.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -113,6 +124,7 @@ { "id": "place__spares", "name": "Spares", + "description": "Spare parts on the shelf. Each service or repair consumes one and orders a replacement; with none in stock the truck waits for parts.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -123,6 +135,7 @@ { "id": "place__in_bay", "name": "InBay", + "description": "Trucks under planned service or breakdown repair, counting service_remaining down.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__in_bay", @@ -133,6 +146,7 @@ { "id": "place__needs_repair", "name": "NeedsRepair", + "description": "Towed-in trucks waiting for a bay, a technician, and a spare so their repair can start.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -143,6 +157,7 @@ { "id": "place__awaiting_parts", "name": "AwaitingParts", + "description": "Trucks holding a bay and technician but waiting for a spare part to arrive.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__stopped", @@ -153,6 +168,7 @@ { "id": "place__parts_on_order", "name": "PartsOnOrder", + "description": "Replacement parts on order, each arriving after parts_lead_time hours on average.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -163,6 +179,7 @@ { "id": "place__delivered", "name": "DeliveredLoads", + "description": "Loads delivered on or before their due time.", "colorId": "type__load", "dynamicsEnabled": false, "differentialEquationId": null, @@ -173,6 +190,7 @@ { "id": "place__late_loads", "name": "LateLoads", + "description": "Loads delivered after their due time.", "colorId": "type__load", "dynamicsEnabled": false, "differentialEquationId": null, @@ -183,6 +201,7 @@ { "id": "place__dropped_loads", "name": "DroppedLoads", + "description": "Loads lost: left on the board past board_patience or abandoned when the truck carrying them failed.", "colorId": "type__load", "dynamicsEnabled": false, "differentialEquationId": null, @@ -193,6 +212,7 @@ { "id": "place__roadside_events", "name": "RoadsideEvents", + "description": "Cumulative count of roadside failures.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -203,6 +223,7 @@ { "id": "place__services_done", "name": "ServicesDone", + "description": "Cumulative count of planned services completed.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -213,6 +234,7 @@ { "id": "place__repairs_done", "name": "RepairsDone", + "description": "Cumulative count of breakdown repairs completed.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -223,6 +245,7 @@ { "id": "place__deferred", "name": "DeferredServices", + "description": "Cumulative count of times a truck due for service was sent back out because no bay was free.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -233,6 +256,7 @@ { "id": "place__conditions", "name": "Conditions", + "description": "Single token holding the regional weather: the mean road severity and speed factor a dispatched truck picks up. The weather dynamics pull them back toward their base values between resamples.", "colorId": "type__conditions", "dynamicsEnabled": true, "differentialEquationId": "de__conditions", @@ -243,6 +267,7 @@ { "id": "place__rest", "name": "Rest", + "description": "Trucks parked while their driver takes mandatory rest, counting rest_remaining down.", "colorId": "type__truck", "dynamicsEnabled": true, "differentialEquationId": "de__resting", @@ -253,6 +278,7 @@ { "id": "place__rest_events", "name": "RestEvents", + "description": "Cumulative count of mandatory rest stops.", "colorId": null, "dynamicsEnabled": false, "differentialEquationId": null, @@ -265,6 +291,7 @@ { "id": "transition__load_motorway", "name": "A motorway load is offered", + "description": "A 420 km motorway load appears on the board at motorway_rate, due after 420 km at average_speed times due_allowance and paying revenue_per_km.", "inputArcs": [], "outputArcs": [ { @@ -281,6 +308,7 @@ { "id": "transition__load_urban", "name": "A urban load is offered", + "description": "A 180 km urban load appears on the board at urban_rate, with due time and revenue derived the same way.", "inputArcs": [], "outputArcs": [ { @@ -297,6 +325,7 @@ { "id": "transition__load_mountain", "name": "A mountain load is offered", + "description": "A 260 km mountain load appears on the board at mountain_rate. Mountain routes wear brakes and tyres hardest.", "inputArcs": [], "outputArcs": [ { @@ -313,6 +342,7 @@ { "id": "transition__dispatch", "name": "Dispatch a truck", + "description": "Pairs a load with an available truck and driver when the truck's worst wear is under the limit for the route (severe_route_wear_limit for mountain loads, wear_limit otherwise) and the trip fits within 1.5 times max_driving_hours. The truck takes the current road severity and speed factor from Conditions.", "inputArcs": [ { "placeId": "place__load_board", @@ -354,6 +384,7 @@ { "id": "transition__deliver_on_time", "name": "Load delivered on time", + "description": "The truck reaches the customer (km_remaining zero) at or before the load's due time. A delivery is counted and the truck starts back with the return distance set.", "inputArcs": [ { "placeId": "place__on_route", @@ -380,6 +411,7 @@ { "id": "transition__deliver_late", "name": "Load delivered late", + "description": "The truck reaches the customer after the load's due time. The load counts as late and the truck starts back.", "inputArcs": [ { "placeId": "place__on_route", @@ -406,6 +438,7 @@ { "id": "transition__breakdown", "name": "Truck fails at the roadside", + "description": "Failure hazard of failure_rate, multiplied by route severity (1 motorway, 1.8 urban, 3.2 mountain) and by the worst of the brake, engine, and tyre wear hazards. The truck is stranded and its load dropped.", "inputArcs": [ { "placeId": "place__on_route", @@ -436,6 +469,7 @@ { "id": "transition__recover", "name": "Recovery unit reaches the truck", + "description": "A free recovery unit reaches a stranded truck after recovery_response hours on average.", "inputArcs": [ { "placeId": "place__stranded", @@ -463,6 +497,7 @@ { "id": "transition__tow_home", "name": "Truck towed back to the depot", + "description": "The tow takes tow_time hours on average. The truck joins NeedsRepair and the recovery unit and driver are freed.", "inputArcs": [ { "placeId": "place__under_recovery", @@ -493,6 +528,7 @@ { "id": "transition__start_repair", "name": "Repair after a breakdown starts", + "description": "A towed truck enters a bay once a bay, a technician, and a spare are all free, with a lognormal repair_time, and a replacement part is ordered.", "inputArcs": [ { "placeId": "place__needs_repair", @@ -534,6 +570,7 @@ { "id": "transition__repair_waits_for_parts", "name": "Repair waits for a part", + "description": "A towed truck takes a bay and technician but no spare is in stock, so it waits for parts, flagged as an unplanned repair.", "inputArcs": [ { "placeId": "place__needs_repair", @@ -571,6 +608,7 @@ { "id": "transition__arrive_depot", "name": "Truck arrives back at the depot", + "description": "The empty return leg ends (km_remaining zero): the truck joins the depot queue and its driver is freed.", "inputArcs": [ { "placeId": "place__returning", @@ -597,6 +635,7 @@ { "id": "transition__park", "name": "Truck parks up, no service due", + "description": "A queued truck whose worst wear is under service_wear_limit and whose driver is under max_driving_hours goes straight back to Available.", "inputArcs": [ { "placeId": "place__depot_queue", @@ -619,6 +658,7 @@ { "id": "transition__park_workshop_full", "name": "Service deferred, workshop full", + "description": "A queued truck due for service finds no free bay, so it goes back to Available and a deferral is counted.", "inputArcs": [ { "placeId": "place__depot_queue", @@ -650,6 +690,7 @@ { "id": "transition__into_bay", "name": "Truck goes into a bay", + "description": "A queued truck at or above service_wear_limit takes a bay, a technician, and a spare for a lognormal service_time, and a replacement part is ordered.", "inputArcs": [ { "placeId": "place__depot_queue", @@ -691,6 +732,7 @@ { "id": "transition__wait_for_parts", "name": "Truck waits for a part", + "description": "A queued truck due for service takes a bay and technician but the shelf is empty, so it waits for a part.", "inputArcs": [ { "placeId": "place__depot_queue", @@ -728,6 +770,7 @@ { "id": "transition__parts_arrive", "name": "Ordered part arrives", + "description": "An ordered part reaches the shelf after parts_lead_time hours on average.", "inputArcs": [ { "placeId": "place__parts_on_order", @@ -750,6 +793,7 @@ { "id": "transition__fit_part", "name": "Part fitted", + "description": "A waiting truck receives a spare and its service, or its repair if the stop was unplanned, begins with the matching lognormal duration. A replacement part is ordered.", "inputArcs": [ { "placeId": "place__awaiting_parts", @@ -781,6 +825,7 @@ { "id": "transition__service_complete", "name": "Planned service finished", + "description": "The service countdown ends: brake, engine, and tyre wear reset to zero and the truck, bay, and technician are released.", "inputArcs": [ { "placeId": "place__in_bay", @@ -815,6 +860,7 @@ { "id": "transition__repair_complete", "name": "Breakdown repair finished", + "description": "The repair countdown ends: wear is halved rather than reset, and the truck, bay, and technician are released.", "inputArcs": [ { "placeId": "place__in_bay", @@ -849,6 +895,7 @@ { "id": "transition__load_expires", "name": "Load goes to another haulier", + "description": "A load older than board_patience hours leaves the board as dropped.", "inputArcs": [ { "placeId": "place__load_board", @@ -871,6 +918,7 @@ { "id": "transition__conditions_on_route", "name": "Road noise injection (OnRoute)", + "description": "Every conditions_step hours a loaded truck's road severity and speed factor take a Gaussian step scaled by their volatilities, on top of the mean reversion in the dynamics.", "inputArcs": [ { "placeId": "place__on_route", @@ -893,6 +941,7 @@ { "id": "transition__conditions_returning", "name": "Road noise injection (Returning)", + "description": "The same periodic Gaussian step for trucks on the return leg.", "inputArcs": [ { "placeId": "place__returning", @@ -915,6 +964,7 @@ { "id": "transition__env_shift", "name": "Weather noise injection", + "description": "Every env_step hours the regional severity and speed means take a Gaussian step of env_volatility, so every truck shares a slowly wandering climate.", "inputArcs": [ { "placeId": "place__conditions", @@ -937,6 +987,7 @@ { "id": "transition__mandatory_rest", "name": "Mandatory rest", + "description": "A queued truck whose driver has reached max_driving_hours goes to Rest instead of back into service, and a rest stop is counted.", "inputArcs": [ { "placeId": "place__depot_queue", @@ -963,6 +1014,7 @@ { "id": "transition__mandatory_rest_available", "name": "Mandatory rest (parked truck)", + "description": "The same rule applied to a truck already parked as available.", "inputArcs": [ { "placeId": "place__available", @@ -989,6 +1041,7 @@ { "id": "transition__rest_complete", "name": "Rest complete", + "description": "rest_remaining reaches zero and the truck is available again.", "inputArcs": [ { "placeId": "place__rest", @@ -1013,6 +1066,7 @@ { "id": "type__truck", "name": "Truck", + "description": "A truck and its driver: brake, engine, and tyre wear (0 new, 1 fully worn), remaining and total route distance, service countdown, the current load's class, due time, and revenue, age, loads done, an unplanned-repair flag, the road severity and speed factor it is driving under, clocks for conditions resampling and rest, fuel burned, hours driven, and fuel rate.", "iconSlug": "circle", "displayColor": "#3b82f6", "elements": [ @@ -1116,6 +1170,7 @@ { "id": "type__load", "name": "Load", + "description": "A load on offer: route class (0 motorway, 1 urban, 2 mountain), distance, due time, revenue, and how long it has sat on the board.", "iconSlug": "circle", "displayColor": "#f97316", "elements": [ @@ -1149,6 +1204,7 @@ { "id": "type__conditions", "name": "Conditions", + "description": "Regional weather: the mean road severity and speed factor newly dispatched trucks inherit, plus the clock to the next resample.", "iconSlug": "circle", "displayColor": "#10b981", "elements": [ @@ -1543,7 +1599,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1592,7 +1648,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1641,7 +1697,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1696,7 +1752,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1745,7 +1801,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1800,7 +1856,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1861,7 +1917,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1922,7 +1978,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -1989,7 +2045,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: parameters.base_severity_mean,\n speed_factor: parameters.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: parameters.base_severity_mean, speed_mean: parameters.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -2068,7 +2124,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } }, { @@ -2135,7 +2191,7 @@ }, "initialState": { "type": "code", - "content": "const newTruck = {\n brake_wear: 0, engine_wear: 0, tyre_wear: 0, km_remaining: 0, route_distance: 0, service_remaining: 0, route_class: 0, load_due: 0, load_revenue: 0, age: 0, loads_done: 0, unplanned: 0, road_severity: 1.0, speed_factor: 1.0, conditions_clock: 0, fuel_burned: 0, hours_driven: 0, rest_remaining: 0, fuel_rate: 0\n };\n const fleet = [];\n for (let index = 0; index < scenario.trucks; index += 1) {\n fleet.push({ ...newTruck,\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n });\n }\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" + "content": "const fleet = range(scenario.trucks).map((index) => ({\n brake_wear: (index / scenario.trucks) * 0.1,\n engine_wear: (index / scenario.trucks) * 0.08,\n tyre_wear: (index / scenario.trucks) * 0.12,\n km_remaining: 0,\n route_distance: 0,\n service_remaining: 0,\n route_class: 0,\n load_due: 0,\n load_revenue: 0,\n age: 0,\n loads_done: 0,\n unplanned: 0,\n road_severity: scenario.base_severity_mean,\n speed_factor: scenario.base_speed_mean,\n conditions_clock: 0,\n fuel_burned: 0,\n hours_driven: 0,\n rest_remaining: 0,\n fuel_rate: 0,\n }));\n return {\n Available: fleet,\n LoadBoard: [],\n Drivers: scenario.drivers,\n Bays: scenario.bays,\n Technicians: scenario.technicians,\n Spares: scenario.spares,\n RecoveryUnits: scenario.recovery_units,\n Conditions: [{ severity_mean: scenario.base_severity_mean, speed_mean: scenario.base_speed_mean, clock: 0 }],\n Rest: [],\n RestEvents: 0,\n };" } } ], @@ -2261,5 +2317,6 @@ "meta": { "generator": "Petrinaut" }, - "title": "Truck fleet with condition-based maintenance (v4)" + "title": "Truck fleet with condition-based maintenance (v4)", + "description": "A haulage fleet under condition-based maintenance. Loads of three route classes are offered, dispatched to trucks whose wear is under the limit, and driven under drifting regional weather that sets wear and fuel burn. Worn trucks are serviced when a bay, a technician, and a spare are free; the rest run on and risk a roadside failure whose hazard grows with wear, followed by recovery, tow, and repair." } diff --git a/apps/petrinaut-website/src/examples/normalize-example.test.ts b/apps/petrinaut-website/src/examples/normalize-example.test.ts deleted file mode 100644 index 592bc4c2751..00000000000 --- a/apps/petrinaut-website/src/examples/normalize-example.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { parseSDCPNFile } from "@hashintel/petrinaut-core"; - -import { normalizeExampleDefinition } from "./normalize-example"; - -import type { SDCPN } from "@hashintel/petrinaut-core"; - -const loadModel = async (slug: string): Promise => { - const module = (await import(`./models/${slug}.json`)) as { - default: unknown; - }; - const parsed = parseSDCPNFile(module.default); - if (!parsed.ok) { - throw new Error(parsed.error); - } - const { title: _title, ...definition } = parsed.sdcpn; - return definition; -}; - -describe("normalizeExampleDefinition", () => { - it("rewrites the truck-fleet scenario initial states", async () => { - const slug = "truck-fleet-predictive-maintenance"; - const definition = await loadModel(slug); - const normalized = normalizeExampleDefinition(slug, definition); - - const codeScenarios = (normalized.scenarios ?? []).filter( - (scenario) => scenario.initialState.type === "code", - ); - expect(codeScenarios.length).toBeGreaterThan(0); - - // The content sniffing must actually have fired for the real model: the - // imperative loop is gone and the range() rewrite is in its place. If the - // source model changes shape, this fails here instead of as an opaque - // scenario-compilation error in the artifact-generation script. - for (const scenario of codeScenarios) { - const content = - scenario.initialState.type === "code" - ? scenario.initialState.content - : ""; - expect(content).not.toContain("for (let index = 0;"); - expect(content).toContain("range(scenario.trucks)"); - expect(content).toMatch(/(scenario|parameters)\.base_severity_mean/); - } - }); - - it("initialises every attribute of the types it rewrites", async () => { - // The rewrite hardcodes the attributes of `Truck` and `Conditions`. A field - // added to either type would otherwise fall back to the type default and - // simulate silently wrong, because the sniff still fires and the generator - // stays green. - const slug = "truck-fleet-predictive-maintenance"; - const definition = await loadModel(slug); - const normalized = normalizeExampleDefinition(slug, definition); - - const rewritten = (normalized.scenarios ?? []) - .map((scenario) => - scenario.initialState.type === "code" - ? scenario.initialState.content - : "", - ) - .join("\n"); - - for (const typeName of ["Truck", "Conditions"]) { - const colourType = definition.types.find( - (candidate) => candidate.name === typeName, - ); - expect(colourType, `${typeName} is missing from the model`).toBeDefined(); - for (const element of colourType?.elements ?? []) { - // Anchored on a word boundary so `clock:` is not satisfied by - // `conditions_clock:`. - expect( - rewritten, - `${typeName}.${element.name} is not initialised by the rewrite`, - ).toMatch(new RegExp(`\\b${element.name}:`, "u")); - } - } - }); - - it("passes other examples through unchanged", async () => { - const slug = "gases-1-pn"; - const definition = await loadModel(slug); - expect(normalizeExampleDefinition(slug, definition)).toBe(definition); - }); -}); diff --git a/apps/petrinaut-website/src/examples/normalize-example.ts b/apps/petrinaut-website/src/examples/normalize-example.ts deleted file mode 100644 index ee0d375bdfb..00000000000 --- a/apps/petrinaut-website/src/examples/normalize-example.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { SDCPN } from "@hashintel/petrinaut-core"; - -const TRUCK_FLEET_SLUG = "truck-fleet-predictive-maintenance"; - -const buildTruckFleetInitialState = ( - meanSource: "parameters" | "scenario", -): string => `const fleet = range(scenario.trucks).map((index) => ({ - brake_wear: (index / scenario.trucks) * 0.1, - engine_wear: (index / scenario.trucks) * 0.08, - tyre_wear: (index / scenario.trucks) * 0.12, - km_remaining: 0, - route_distance: 0, - service_remaining: 0, - route_class: 0, - load_due: 0, - load_revenue: 0, - age: 0, - loads_done: 0, - unplanned: 0, - road_severity: ${meanSource}.base_severity_mean, - speed_factor: ${meanSource}.base_speed_mean, - conditions_clock: 0, - fuel_burned: 0, - hours_driven: 0, - rest_remaining: 0, - fuel_rate: 0, - })); - return { - Available: fleet, - LoadBoard: [], - Drivers: scenario.drivers, - Bays: scenario.bays, - Technicians: scenario.technicians, - Spares: scenario.spares, - RecoveryUnits: scenario.recovery_units, - Conditions: [{ severity_mean: ${meanSource}.base_severity_mean, speed_mean: ${meanSource}.base_speed_mean, clock: 0 }], - Rest: [], - RestEvents: 0, - };`; - -/** - * Applies the small, reviewed compatibility transformations needed by the - * published examples. Source files stay byte-for-byte identical to the files - * supplied for FE-1500; both the canonical route and generated embed artifacts - * consume this normalized definition. - */ -export const normalizeExampleDefinition = ( - slug: string, - definition: SDCPN, -): SDCPN => { - if (slug !== TRUCK_FLEET_SLUG) { - return definition; - } - - return { - ...definition, - scenarios: definition.scenarios?.map((scenario) => { - if (scenario.initialState.type !== "code") { - return scenario; - } - - const content = scenario.initialState.content; - if ( - !content.includes("for (let index = 0;") || - !content.includes("...newTruck") - ) { - return scenario; - } - - const meanSource = content.includes("scenario.base_severity_mean") - ? "scenario" - : "parameters"; - - return { - ...scenario, - initialState: { - type: "code", - content: buildTruckFleetInitialState(meanSource), - }, - }; - }), - }; -}; diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/local-storage-demo-app.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/local-storage-demo-app.tsx index 9b40e97ea1c..fb5380700a8 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/local-storage-demo-app.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/local-storage-demo-app.tsx @@ -3,7 +3,7 @@ * @role Editable demo shell: nets in local storage, one live document handle */ -import { produce } from "immer"; +import { castDraft, produce } from "immer"; import { useEffect, useMemo, useState } from "react"; import { BRUNCH_PRINCIPAL_HEADER } from "@hashintel/brunch-agent-transport-aisdk/headers"; @@ -294,13 +294,14 @@ export const LocalStorageDemoApp = ({ setStoredSDCPNs((prev) => { const stored = prev[netId] ?? fallbackNet; + const next: SDCPNInLocalStorage = { + ...stored, + sdcpn: event.next, + lastUpdated, + }; return produce(prev, (draft) => { - draft[netId] = { - ...stored, - sdcpn: event.next, - lastUpdated, - }; + draft[netId] = castDraft(next); }); }); }); @@ -385,11 +386,18 @@ export const LocalStorageDemoApp = ({ setStoredSDCPNs((prev) => produce(prev, (draft) => { - draft[currentNetId] = { - ...(draft[currentNetId] ?? currentNet), - title, - lastUpdated, - }; + const existing = draft[currentNetId]; + if (existing) { + existing.title = title; + existing.lastUpdated = lastUpdated; + } else { + const next: SDCPNInLocalStorage = { + ...currentNet, + title, + lastUpdated, + }; + draft[currentNetId] = castDraft(next); + } }), ); }; diff --git a/apps/petrinaut-website/turbo.json b/apps/petrinaut-website/turbo.json index 86914e4360a..0091e04c9ad 100644 --- a/apps/petrinaut-website/turbo.json +++ b/apps/petrinaut-website/turbo.json @@ -19,8 +19,7 @@ "inputs": [ "scripts/generate-example-artifacts.ts", "src/examples/catalog-metadata.ts", - "src/examples/models/**", - "src/examples/normalize-example.ts" + "src/examples/models/**" ], "outputs": ["src/examples/generated/**"] }, diff --git a/libs/@hashintel/petrinaut-core/src/examples/cafe-queue.ts b/libs/@hashintel/petrinaut-core/src/examples/cafe-queue.ts index 899bbb42a2a..2f2e1cfd1e6 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/cafe-queue.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/cafe-queue.ts @@ -28,10 +28,14 @@ import type { SDCPN } from "../types/sdcpn"; export const cafeQueue: { title: string; petriNetDefinition: SDCPN } = { title: "Café Queue", petriNetDefinition: { + description: + "Stochastic café service: customers arrive at a fixed rate, wait for a free member of staff, are served, and leave. The staff pool (FreeStaff plus Serving) is conserved, so whether the Waiting queue drains or grows depends on the arrival rate against the two service rates.", places: [ { id: "place__waiting", name: "Waiting", + description: + "Customers queued for service. Arrive feeds it and BeginService drains it, so it grows whenever arrivals outpace the staff.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -42,6 +46,8 @@ export const cafeQueue: { title: string; petriNetDefinition: SDCPN } = { { id: "place__free_staff", name: "FreeStaff", + description: + "Staff members free to take the next customer. FreeStaff plus Serving is the fixed staff pool.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -52,6 +58,8 @@ export const cafeQueue: { title: string; petriNetDefinition: SDCPN } = { { id: "place__serving", name: "Serving", + description: + "Customer and staff pairs at the counter mid-service; each completes at the service rate.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -61,6 +69,8 @@ export const cafeQueue: { title: string; petriNetDefinition: SDCPN } = { { id: "place__served", name: "Served", + description: + "Customers who have been served and left: a cumulative count of throughput.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -72,6 +82,8 @@ export const cafeQueue: { title: string; petriNetDefinition: SDCPN } = { { id: "transition__arrive", name: "Arrive", + description: + "Source transition with no inputs: adds one customer to Waiting at arrival_rate regardless of the queue's state.", inputArcs: [], outputArcs: [ { @@ -96,6 +108,8 @@ export default TransitionKernel(() => { { id: "transition__begin_service", name: "BeginService", + description: + "Pairs one Waiting customer with one FreeStaff member and moves them into Serving at begin_rate; stalls when either is empty.", inputArcs: [ { placeId: "place__waiting", @@ -131,6 +145,8 @@ export default TransitionKernel(() => { { id: "transition__finish_service", name: "FinishService", + description: + "A service in progress completes at service_rate: the customer moves to Served and the staff member returns to FreeStaff.", inputArcs: [ { placeId: "place__serving", diff --git a/libs/@hashintel/petrinaut-core/src/examples/deployment-pipeline.ts b/libs/@hashintel/petrinaut-core/src/examples/deployment-pipeline.ts index 51fe7c517d7..cb6f1245bc3 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/deployment-pipeline.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/deployment-pipeline.ts @@ -23,10 +23,14 @@ export const deploymentPipelineSDCPN: { } = { title: "Deployment Pipeline", petriNetDefinition: { + description: + "Software release pipeline with a single-deployment safety gate and an incident feedback loop: releases queue up, deploy one at a time while no incident is open, and either complete or fail and open a new incident that closes the gate.", places: [ { id: "place__deployment-ready", name: "DeploymentReady", + description: + "Release queue: deployments waiting for the safety gate to open, each with a sampled size and risk.", colorId: "type__deployment", dynamicsEnabled: true, differentialEquationId: "dynamics__deployment_age", @@ -65,6 +69,8 @@ export default Visualization(({ tokens }) => { { id: "place__incident-being-investigated", name: "IncidentBeingInvestigated", + description: + "Open incidents under investigation. Any token here closes the deployment gate through the inhibitor arc into Start Deployment.", colorId: "type__incident", dynamicsEnabled: true, differentialEquationId: "dynamics__incident_age", @@ -103,6 +109,8 @@ export default Visualization(({ tokens }) => { { id: "place__deployment-in-progress", name: "DeploymentInProgress", + description: + "The deployment currently rolling out — at most one, enforced by this place's inhibitor arc into Start Deployment. It either finishes successfully or fails and opens an incident.", colorId: "type__deployment", dynamicsEnabled: true, differentialEquationId: "dynamics__deployment_age", @@ -141,6 +149,8 @@ export default Visualization(({ tokens }) => { { id: "place__completed-deployments", name: "CompletedDeployments", + description: + "Archive of releases that finished without causing an incident, keeping their size, risk, and run time for metrics.", colorId: "type__deployment", dynamicsEnabled: false, differentialEquationId: null, @@ -175,6 +185,8 @@ export default Visualization(({ tokens }) => { { id: "place__resolved-incidents", name: "ResolvedIncidents", + description: + "Archive of closed incidents; they no longer block deployments but keep their severity and age as a record of operational load.", colorId: "type__incident", dynamicsEnabled: false, differentialEquationId: null, @@ -208,6 +220,8 @@ export default Visualization(({ tokens }) => { { id: "place__failed-deployments", name: "FailedDeployments", + description: + "Archive of releases that failed mid-rollout and opened an incident, kept for failure-share metrics.", colorId: "type__deployment", dynamicsEnabled: false, differentialEquationId: null, @@ -247,6 +261,8 @@ export default Visualization(({ tokens }) => { { id: "transition__create-deployment", name: "Create Deployment", + description: + "External arrival of release candidates into the queue, with lognormal size and Gaussian risk sampled per deployment.", inputArcs: [], outputArcs: [ { @@ -291,6 +307,8 @@ return { { id: "transition__incident-raised", name: "Incident Raised", + description: + "External incidents (infrastructure failures, customer-impacting issues) that open independently of deployments and close the release gate.", inputArcs: [], outputArcs: [ { @@ -330,6 +348,8 @@ return { { id: "transition__start-deployment", name: "Start Deployment", + description: + "The safety gate: takes one queued release into the deployment lane, but only while no incident is open and no other deployment is running (both enforced by inhibitor arcs).", inputArcs: [ { placeId: "place__deployment-ready", @@ -387,6 +407,8 @@ return { { id: "transition__finish-deployment", name: "Finish Deployment", + description: + "The running deployment completes successfully; larger and riskier releases finish more slowly.", inputArcs: [ { placeId: "place__deployment-in-progress", @@ -436,6 +458,8 @@ return { { id: "transition__close-incident", name: "Close Incident", + description: + "Resolves an open incident; higher-severity incidents take longer. Closing the last one reopens the deployment gate.", inputArcs: [ { placeId: "place__incident-being-investigated", @@ -482,6 +506,8 @@ return { { id: "transition__deployment-causes-incident", name: "Deployment Causes Incident", + description: + "The failure outcome competing with Finish Deployment: the running release rolls back into the failed archive and opens a new incident whose severity scales with the release's risk.", inputArcs: [ { placeId: "place__deployment-in-progress", @@ -549,6 +575,8 @@ return { { id: "type__deployment", name: "Deployment", + description: + "A software release, described by its size (lognormal), risk in [0.02, 0.95], and age since creation or start.", iconSlug: "rocket", displayColor: "#2563eb", elements: [ @@ -572,6 +600,8 @@ return { { id: "type__incident", name: "Incident", + description: + "An operational incident, described by its severity in [0.05, 1] (slower to resolve when higher) and age since it opened.", iconSlug: "alert-triangle", displayColor: "#dc2626", elements: [ diff --git a/libs/@hashintel/petrinaut-core/src/examples/drone-patrol.ts b/libs/@hashintel/petrinaut-core/src/examples/drone-patrol.ts index 00b9614287a..28863063467 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/drone-patrol.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/drone-patrol.ts @@ -25,10 +25,14 @@ export const dronePatrol: { } = { title: "Drone Patrol", petriNetDefinition: { + description: + "A small drone fleet cycling between hangar and air. Drones launch at a rate scaled by their battery, drain charge while airborne so the pull back to base grows, and recharge on landing. Sweep launch_rate against drain_rate to trade patrol coverage against battery wear.", places: [ { id: "place__hangar", name: "Hangar", + description: + "Drones on the ground ready to launch, each carrying its battery level; landing resets it to a full charge. Holds up to 16 drones.", colorId: "type__drone", dynamicsEnabled: false, differentialEquationId: null, @@ -39,6 +43,8 @@ export const dronePatrol: { { id: "place__airborne", name: "Airborne", + description: + "Drones on patrol. The Flight Battery Drain dynamics lower each drone's battery at drain_rate while its cruise altitude holds. Holds up to 16 drones.", colorId: "type__drone", dynamicsEnabled: true, differentialEquationId: "de__flight_battery_drain", @@ -51,6 +57,8 @@ export const dronePatrol: { { id: "transition__launch", name: "Launch", + description: + "A hangar drone lifts off at launch_rate scaled by its battery fraction, so a full battery launches at the full rate and a drained one not at all. The kernel keeps the battery and draws a cruise altitude around 100 units with a floor of 50.", inputArcs: [ { placeId: "place__hangar", @@ -90,6 +98,8 @@ export default TransitionKernel((tokens) => { { id: "transition__return_to_base", name: "ReturnToBase", + description: + "An airborne drone heads home at return_rate, rising as its battery drains: a full drone returns at the base rate, a fully drained one at five times that. Landing recharges it to 100 and sets its altitude to zero.", inputArcs: [ { placeId: "place__airborne", @@ -123,6 +133,8 @@ export default TransitionKernel(() => { { id: "type__drone", name: "Drone", + description: + "A patrol drone, tracked by its battery charge (100 when full) and its current altitude (0 on the ground).", iconSlug: "circle", displayColor: "#1E90FF", elements: [ diff --git a/libs/@hashintel/petrinaut-core/src/examples/production-with-machine-failure.ts b/libs/@hashintel/petrinaut-core/src/examples/production-with-machine-failure.ts index c86ddbd7a17..feee304583d 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/production-with-machine-failure.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/production-with-machine-failure.ts @@ -21,10 +21,14 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { title: "Production With Machine Failure", petriNetDefinition: { + description: + "Manufacturing line where machines turn raw material into products, accumulate damage as they run, break down, and are repaired by travelling technicians. Production is a race between finishing a batch and a damage-driven failure hazard.", places: [ { id: "place__d662407f-c56d-4a96-bcbb-ead785a9c594", name: "RawMaterial", + description: + "Stock of unprocessed material; each production run consumes one unit.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -35,6 +39,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__2bdd959f-a5bc-404a-bd03-34fafcef66b8", name: "AvailableMachines", + description: + "Idle machines ready to start a production run, each carrying the damage it has accumulated so far.", colorId: "type__1762560152725", dynamicsEnabled: false, differentialEquationId: null, @@ -45,6 +51,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__81e551b4-11dc-4781-9cd7-dd882fd7e947", name: "MachinesProducing", + description: + "Machines working on a batch. The Production Dynamics advance their transformation progress while adding wear, so a long run raises the failure hazard.", colorId: "type__1762560154179", dynamicsEnabled: true, differentialEquationId: "ca26e5e2-0373-46a9-920e-a6eacadd92e8", @@ -54,6 +62,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__d5f92ae2-c8c4-49cb-935e-4a35e4f7b5fe", name: "BadProduct", + description: + "Defective output: one token per batch ruined by a machine failure mid-run.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -63,6 +73,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__7b695ff5-a397-4237-8e30-ddf8cbc9e2c4", name: "GoodProduct", + description: + "Finished output: one token per batch completed without a failure.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -72,6 +84,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__e5af0410-d80a-4c8b-b3bf-692918b98e6c", name: "BrokenMachines", + description: + "Machines that failed mid-run and are waiting for a technician to be dispatched.", colorId: "type__1762560152725", dynamicsEnabled: false, differentialEquationId: null, @@ -81,6 +95,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__17c65d6e-0c3e-48e6-a677-2914e28131ac", name: "MachinesBeingRepaired", + description: + "Machines paired with a technician. The Reparation Dynamics drive their damage ratio down to zero.", colorId: "type__1762560152725", dynamicsEnabled: true, differentialEquationId: "5bfea547-faaf-4626-8662-6400d07c049e", @@ -90,6 +106,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__4b72cf19-907b-4fc0-ac0a-555453e95d4b", name: "TechniciansComing", + description: + "Technicians travelling to the site. The Technician Travel Dynamics count their remaining distance down to zero.", colorId: "type__1762560159263", dynamicsEnabled: true, differentialEquationId: "887245c3-183c-4dac-a1aa-d602d21b6450", @@ -99,6 +117,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__eaca89b8-1db1-45fa-8c3a-6eb6f0419ffa", name: "AvailableTechnicians", + description: + "Technicians on site and ready to be paired with a machine that needs repair.", colorId: "type__1762560159263", dynamicsEnabled: false, differentialEquationId: null, @@ -108,6 +128,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "place__9cb073fb-f1d7-4613-8b10-8d1b08796f24", name: "MachinesToRepair", + description: + "Broken machines whose technician has been dispatched, queued until one is free to start the repair.", colorId: "type__1762560152725", dynamicsEnabled: false, differentialEquationId: null, @@ -119,6 +141,8 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = { id: "transition__76f23aa1-404a-4696-ac14-5a634af01221", name: "Production Success", + description: + "The batch completes once transformation progress reaches 100%: a good product ships and the machine returns to the available pool, keeping its wear.", inputArcs: [ { placeId: "place__81e551b4-11dc-4781-9cd7-dd882fd7e947", @@ -154,6 +178,8 @@ return { { id: "transition__b524484d-263e-4065-b8b2-7a8e49529260", name: "Machine Fail", + description: + "A producing machine breaks down, ruining the batch. The hazard is the damage ratio raised to the 100th power, so failure is rare until a machine is badly worn.", inputArcs: [ { placeId: "place__81e551b4-11dc-4781-9cd7-dd882fd7e947", @@ -192,6 +218,8 @@ return { { id: "transition__c4b30ba4-da08-4407-b97b-41e2db5d6879", name: "Start Production", + description: + "Pairs one unit of raw material with an idle machine and starts a batch, resetting the machine's transformation progress to zero.", inputArcs: [ { placeId: "place__d662407f-c56d-4a96-bcbb-ead785a9c594", @@ -230,6 +258,8 @@ return { { id: "transition__cc61df1f-00f3-456f-8a80-03e8b68f3007", name: "Finish Repair", + description: + "Once the repair dynamics have driven a machine's damage to zero, it returns to the available pool fully repaired.", inputArcs: [ { placeId: "place__17c65d6e-0c3e-48e6-a677-2914e28131ac", @@ -260,6 +290,8 @@ return { { id: "transition__11f0b21a-d0f2-4bd5-b4c1-d23627f921c5", name: "Call Technician", + description: + "Dispatches a technician for a broken machine (starting 10 distance units away) and queues the machine for repair.", inputArcs: [ { placeId: "place__e5af0410-d80a-4c8b-b3bf-692918b98e6c", @@ -295,6 +327,8 @@ return { { id: "transition__514730c0-7ac5-47d5-8def-91446a248a83", name: "Technician Ready", + description: + "A travelling technician arrives on site (remaining distance zero) and becomes available for repair work.", inputArcs: [ { placeId: "place__4b72cf19-907b-4fc0-ac0a-555453e95d4b", @@ -324,6 +358,8 @@ return { { id: "transition__0efcd1bf-b1ff-466f-8a8f-c329ddce0ce8", name: "Start Repair", + description: + "Pairs an available technician with a waiting machine and begins working its damage back down.", inputArcs: [ { placeId: "place__eaca89b8-1db1-45fa-8c3a-6eb6f0419ffa", @@ -362,6 +398,8 @@ return { { id: "type__1762560152725", name: "Machine", + description: + "A machine idle, broken, or under repair, tracked by its accumulated damage ratio (0 = pristine, 1 = certain failure).", iconSlug: "circle", displayColor: "#3b82f6", elements: [ @@ -375,6 +413,8 @@ return { { id: "type__1762560154179", name: "Machine Producing Product", + description: + "A machine mid-batch: its damage ratio plus the batch's transformation progress (complete at 1).", iconSlug: "circle", displayColor: "#733bf6ff", elements: [ @@ -393,6 +433,8 @@ return { { id: "type__1762560159263", name: "Technician", + description: + "A repair technician, tracked by their remaining travel distance to the site.", iconSlug: "circle", displayColor: "#3bf689ff", elements: [ diff --git a/libs/@hashintel/petrinaut-core/src/examples/satellites-launcher.ts b/libs/@hashintel/petrinaut-core/src/examples/satellites-launcher.ts index 76ca1c12bea..5ef65b0164a 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/satellites-launcher.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/satellites-launcher.ts @@ -26,10 +26,14 @@ export const probabilisticSatellitesSDCPN: { } = { title: "Probabilistic Satellite Launcher", petriNetDefinition: { + description: + "Orbital mechanics simulation: satellites launch stochastically into orbit around a central body, a gravitational ODE integrates their position and velocity, and collisions between satellites or crashes into the planet turn them into debris.", places: [ { id: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", name: "Space", + description: + "Satellites in orbit. The orbit dynamics integrate each one's position and velocity under the planet's gravity, and a custom visualizer draws them around the planet.", colorId: "f8e9d7c6-b5a4-3210-fedc-ba9876543210", dynamicsEnabled: true, differentialEquationId: "1a2b3c4d-5e6f-7890-abcd-1234567890ab", @@ -123,6 +127,8 @@ export const probabilisticSatellitesSDCPN: { { id: "ea42ba61-03ea-4940-b2e2-b594d5331a71", name: "Debris", + description: + "Defunct objects left by collisions and crashes, frozen at the point of impact with zero velocity.", colorId: "f8e9d7c6-b5a4-3210-fedc-ba9876543210", dynamicsEnabled: false, differentialEquationId: null, @@ -134,6 +140,8 @@ export const probabilisticSatellitesSDCPN: { { id: "d25015d8-7aac-45ff-82b0-afd943f1b7ec", name: "Collision", + description: + "Two satellites whose surfaces come within the collision threshold collide and become two pieces of stationary debris at the impact point.", inputArcs: [ { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", @@ -184,6 +192,8 @@ return { { id: "716fe1e5-9b35-413f-83fe-99b28ba73945", name: "Crash", + description: + "A satellite that falls within the crash threshold of the planet surface crashes and becomes debris at the crash site.", inputArcs: [ { placeId: "3cbc7944-34cb-4eeb-b779-4e392a171fe1", @@ -227,6 +237,8 @@ return { { id: "transition__c7008acb-b0e7-468e-a5d3-d56eaa1fe806", name: "LaunchSatellite", + description: + "Launches a new satellite at the configured rate, placing it at a uniformly sampled angle on the launch altitude with a Gaussian initial velocity.", inputArcs: [], outputArcs: [ { @@ -259,6 +271,8 @@ return { { id: "f8e9d7c6-b5a4-3210-fedc-ba9876543210", name: "Satellite", + description: + "An orbiting object described by its position relative to the planet centre (x, y), heading, and speed.", iconSlug: "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", displayColor: "#1E90FF", elements: [ diff --git a/libs/@hashintel/petrinaut-core/src/examples/sir-model.ts b/libs/@hashintel/petrinaut-core/src/examples/sir-model.ts index ca0899a5dee..386557bb519 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/sir-model.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/sir-model.ts @@ -19,10 +19,14 @@ import type { SDCPN } from "../types/sdcpn"; export const sirModel: { title: string; petriNetDefinition: SDCPN } = { title: "SIR Epidemic Model", petriNetDefinition: { + description: + "Susceptible-Infected-Recovered (SIR) compartmental epidemic model as a stochastic Petri net. Infection follows mass-action S + I -> 2I dynamics; the infection and recovery rates are global parameters, so R0 can be explored by changing parameters alone.", places: [ { id: "place__susceptible", name: "Susceptible", + description: + "Individuals who have not yet caught the disease and can be infected by contact with an infected individual.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -33,6 +37,8 @@ export const sirModel: { title: string; petriNetDefinition: SDCPN } = { { id: "place__infected", name: "Infected", + description: + "Individuals currently carrying the disease and able to transmit it to susceptible contacts.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -43,6 +49,8 @@ export const sirModel: { title: string; petriNetDefinition: SDCPN } = { { id: "place__recovered", name: "Recovered", + description: + "Individuals who have recovered and are immune; they never return to the susceptible or infected pools.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -54,6 +62,8 @@ export const sirModel: { title: string; petriNetDefinition: SDCPN } = { { id: "transition__infection", name: "Infection", + description: + "Contact between a susceptible and an infected individual infects the susceptible: consumes one of each and produces two infected (S + I -> 2I), firing at the infection_rate parameter.", inputArcs: [ { placeId: "place__susceptible", @@ -88,6 +98,8 @@ return { { id: "transition__recovery", name: "Recovery", + description: + "An infected individual recovers and becomes immune, moving to Recovered at the recovery_rate parameter.", inputArcs: [ { placeId: "place__infected", diff --git a/libs/@hashintel/petrinaut-core/src/examples/supply-chain-profit.ts b/libs/@hashintel/petrinaut-core/src/examples/supply-chain-profit.ts index bf5b1d08355..22c8ab9eaee 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/supply-chain-profit.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/supply-chain-profit.ts @@ -26,10 +26,14 @@ export const supplyChainProfit: { } = { title: "Supply Chain Profit Model", petriNetDefinition: { + description: + "Single-stage supply chain framed as a profit-optimization problem: a reorder policy replenishes raw inventory, production converts it into finished goods, and arriving demand is either sold from stock or lost to a stockout. Six decision parameters feed a single profit metric.", places: [ { id: "place_raw_inventory", name: "RawInventory", + description: + "Raw-material stock on hand, replenished in batches by the reorder policy and consumed one unit per production run.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -41,6 +45,8 @@ export const supplyChainProfit: { { id: "place_finished_goods", name: "FinishedGoods", + description: + "Sellable stock produced from raw material; demand arriving while this is empty becomes a lost sale.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -52,6 +58,8 @@ export const supplyChainProfit: { { id: "place_customer_demand", name: "CustomerDemand", + description: + "Open customer orders waiting to be fulfilled from stock; each accrues a backlog cost in the profit metric until it is sold or lost.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -63,6 +71,8 @@ export const supplyChainProfit: { { id: "place_sold_orders", name: "SoldOrders", + description: + "Cumulative count of orders fulfilled from stock; the revenue side of the profit metric.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -74,6 +84,8 @@ export const supplyChainProfit: { { id: "place_lost_sales", name: "LostSales", + description: + "Cumulative count of demand lost to stockouts; each carries a penalty cost in the profit metric.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -87,6 +99,8 @@ export const supplyChainProfit: { { id: "trans_replenish_raw", name: "Replenish raw inventory", + description: + "External supplier deliveries: each firing adds a batch of 10 raw units, at a rate set by the replenishment aggressiveness, batch size, and expedited-shipping fraction.", inputArcs: [], outputArcs: [ { @@ -112,6 +126,8 @@ return {};`, { id: "trans_produce_finished", name: "Produce finished goods", + description: + "Converts one unit of raw material into one finished good at the configured production rate.", inputArcs: [ { placeId: "place_raw_inventory", @@ -139,6 +155,8 @@ return {};`, { id: "trans_create_demand", name: "Customer demand arrives", + description: + "External demand source: arrival rate scales with the market demand multiplier and marketing spend, and falls as the selling price rises.", inputArcs: [], outputArcs: [ { @@ -163,6 +181,8 @@ return {};`, { id: "trans_sell_order", name: "Sell order", + description: + "Fulfils one open order from finished-goods stock; its high rate means selling wins over losing the sale whenever stock is available.", inputArcs: [ { placeId: "place_finished_goods", @@ -196,6 +216,8 @@ return {};`, { id: "trans_lost_sale", name: "Demand lost to stockout", + description: + "An open order goes unmet: the inhibitor arc from FinishedGoods lets this fire only while stock is zero, turning waiting demand into lost sales.", inputArcs: [ { placeId: "place_customer_demand", diff --git a/libs/@hashintel/petrinaut-core/src/examples/supply-chain-with-disruption.ts b/libs/@hashintel/petrinaut-core/src/examples/supply-chain-with-disruption.ts index 916aae955b8..0ce27979690 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/supply-chain-with-disruption.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/supply-chain-with-disruption.ts @@ -27,10 +27,14 @@ export const supplyChainWithDisruption: { } = { title: "Supply Chain With Disruption", petriNetDefinition: { + description: + "End-to-end supply chain under disruption: two suppliers that can go down and recover feed inbound shipments to a factory whose machine wears out and breaks, while customer orders are fulfilled from stock, converted to backorders, cancelled, or delivered through a lossy outbound leg.", places: [ { id: "place_supplier_a_available", name: "SupplierAAvailable", + description: + "Reliable supplier A is operating and can take purchase orders; the token moves to SupplierADown during a disruption.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -42,6 +46,8 @@ export const supplyChainWithDisruption: { { id: "place_supplier_a_down", name: "SupplierADown", + description: + "Supplier A is disrupted and takes no orders until it recovers.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -53,6 +59,8 @@ export const supplyChainWithDisruption: { { id: "place_supplier_b_available", name: "SupplierBAvailable", + description: + "Low-cost supplier B is operating and can take purchase orders; cheaper than A but riskier and slower.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -64,6 +72,8 @@ export const supplyChainWithDisruption: { { id: "place_supplier_b_down", name: "SupplierBDown", + description: + "Supplier B is disrupted and takes no orders until it recovers.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -75,6 +85,8 @@ export const supplyChainWithDisruption: { { id: "place_raw_materials", name: "RawMaterials", + description: + "Warehouse stock of usable material from cleanly received inbound shipments, consumed by production batches.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -86,6 +98,8 @@ export const supplyChainWithDisruption: { { id: "place_damaged_inbound", name: "DamagedInbound", + description: + "Inbound shipments that arrived too damaged to use, written off on receipt.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -97,6 +111,8 @@ export const supplyChainWithDisruption: { { id: "place_finished_goods", name: "FinishedGoods", + description: + "Sellable stock: production batches that passed quality, waiting to fulfil open orders and backorders.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -108,6 +124,8 @@ export const supplyChainWithDisruption: { { id: "place_scrap", name: "Scrap", + description: + "Production batches that failed the quality check and were scrapped.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -119,6 +137,8 @@ export const supplyChainWithDisruption: { { id: "place_delivered", name: "Delivered", + description: + "Orders that completed the outbound leg and reached the customer.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -130,6 +150,8 @@ export const supplyChainWithDisruption: { { id: "place_lost", name: "Lost", + description: + "Outbound shipments lost or damaged in transit and never delivered.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -141,6 +163,8 @@ export const supplyChainWithDisruption: { { id: "place_cancelled", name: "Cancelled", + description: + "Backorders whose customers gave up waiting and cancelled.", colorId: null, dynamicsEnabled: false, differentialEquationId: null, @@ -152,6 +176,8 @@ export const supplyChainWithDisruption: { { id: "place_inbound", name: "InboundShipments", + description: + "Purchase orders in transit from a supplier. The ETA-countdown dynamics run each one down to arrival, where it is received cleanly or written off as damaged.", colorId: "type_shipment", dynamicsEnabled: true, differentialEquationId: "dyn_shipment_eta", @@ -190,6 +216,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_wip", name: "WorkInProcess", + description: + "Batches on the factory floor. Dynamics count their remaining processing time down while quality decays; each finishes as a good batch or scrap.", colorId: "type_batch", dynamicsEnabled: true, differentialEquationId: "dyn_batch_processing", @@ -229,6 +257,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_orders", name: "OpenOrders", + description: + "New customer orders awaiting same-day fulfilment from stock; those that outlive their promised lead time convert to backorders.", colorId: "type_order", dynamicsEnabled: true, differentialEquationId: "dyn_order_age", @@ -262,6 +292,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_backorders", name: "Backorders", + description: + "Orders that missed their promised lead time and now wait for replenished stock, at growing risk of cancellation as they age.", colorId: "type_order", dynamicsEnabled: true, differentialEquationId: "dyn_order_age", @@ -291,6 +323,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_outbound", name: "OutboundShipments", + description: + "Fulfilled orders on the last-mile leg to the customer; each arrives and is delivered, or is lost or damaged in transit.", colorId: "type_shipment", dynamicsEnabled: true, differentialEquationId: "dyn_shipment_eta", @@ -326,6 +360,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_machine_up", name: "MachineUp", + description: + "The factory machine while operational. Its health and wear drift over time and gate whether production can start.", colorId: "type_machine", dynamicsEnabled: true, differentialEquationId: "dyn_machine_health", @@ -353,6 +389,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "place_machine_down", name: "MachineDown", + description: + "The factory machine while broken down, halting production until it is repaired.", colorId: "type_machine", dynamicsEnabled: false, differentialEquationId: null, @@ -366,6 +404,8 @@ export default Visualization(({ tokens, parameters }) => { { id: "trans_order_supplier_a", name: "Order from reliable supplier A", + description: + "While supplier A is available, places purchase orders that become inbound shipments with A's lead time, cost, and low damage risk.", inputArcs: [ { placeId: "place_supplier_a_available", @@ -408,6 +448,8 @@ return { { id: "trans_order_supplier_b", name: "Order from low-cost supplier B", + description: + "While supplier B is available, places purchase orders that become inbound shipments with B's cheaper cost but longer lead time and higher damage risk.", inputArcs: [ { placeId: "place_supplier_b_available", @@ -447,6 +489,8 @@ return { { id: "trans_supplier_a_disrupts", name: "Supplier A disruption", + description: + "Supplier A randomly goes down, stopping its order flow until it recovers.", inputArcs: [ { placeId: "place_supplier_a_available", @@ -474,6 +518,8 @@ return {};`, { id: "trans_supplier_a_recovers", name: "Supplier A recovers", + description: + "A disrupted supplier A comes back online and resumes taking orders.", inputArcs: [ { placeId: "place_supplier_a_down", @@ -499,6 +545,8 @@ return {};`, { id: "trans_supplier_b_disrupts", name: "Supplier B disruption", + description: + "Supplier B randomly goes down, stopping its order flow until it recovers.", inputArcs: [ { placeId: "place_supplier_b_available", @@ -524,6 +572,8 @@ return {};`, { id: "trans_supplier_b_recovers", name: "Supplier B recovers", + description: + "A disrupted supplier B comes back online and resumes taking orders.", inputArcs: [ { placeId: "place_supplier_b_down", @@ -549,6 +599,8 @@ return {};`, { id: "trans_inbound_good", name: "Inbound shipment received", + description: + "An arrived inbound shipment (ETA counted down to zero) whose damage risk is below the threshold is received into raw-materials stock.", inputArcs: [ { placeId: "place_inbound", @@ -577,6 +629,8 @@ return {};`, { id: "trans_inbound_damaged", name: "Inbound shipment damaged", + description: + "The mutually exclusive receipt outcome: an arrived shipment at or above the damage threshold is written off instead of stocked.", inputArcs: [ { placeId: "place_inbound", @@ -604,6 +658,8 @@ return {};`, { id: "trans_start_production", name: "Start production batch", + description: + "Consumes raw material on a sufficiently healthy machine to start a work-in-process batch, adding wear to the machine and inheriting quality from its condition.", inputArcs: [ { placeId: "place_raw_materials", @@ -656,6 +712,8 @@ return { { id: "trans_finish_good_batch", name: "Batch passes quality", + description: + "A batch whose processing is complete and whose quality is at or above the minimum becomes finished goods.", inputArcs: [ { placeId: "place_wip", @@ -683,6 +741,8 @@ return {};`, { id: "trans_scrap_bad_batch", name: "Batch fails quality", + description: + "The competing outcome: a completed batch below the minimum quality is scrapped.", inputArcs: [ { placeId: "place_wip", @@ -710,6 +770,8 @@ return {};`, { id: "trans_machine_breakdown_random", name: "Machine breakdown", + description: + "The running machine breaks down, losing health in the process; the hazard rises with wear and poor health.", inputArcs: [ { placeId: "place_machine_up", @@ -738,6 +800,8 @@ return { MachineDown: [{ health: Math.max(0, machine.health - 0.15), wear: machi { id: "trans_machine_repair", name: "Repair machine", + description: + "A broken machine is repaired, restoring most of its health and removing most of its wear.", inputArcs: [ { placeId: "place_machine_down", @@ -764,6 +828,8 @@ return { MachineUp: [{ health: Math.min(1, machine.health + 0.55), wear: Math.ma { id: "trans_preventive_maintenance", name: "Preventive maintenance", + description: + "Maintenance on the running machine boosts health and shaves off wear without taking it offline, and is more likely once wear builds up.", inputArcs: [ { placeId: "place_machine_up", @@ -793,6 +859,8 @@ return { MachineUp: [{ health: Math.min(1, machine.health + parameters.maintenan { id: "trans_customer_demand", name: "Customer demand arrives", + description: + "External demand source: new customer orders arrive at a constant rate, classified as VIP or standard priority.", inputArcs: [], outputArcs: [ { @@ -822,6 +890,8 @@ return { { id: "trans_fulfill_open_order", name: "Fulfill open order from stock", + description: + "Pairs an open order with finished-goods stock and dispatches it as an outbound shipment, favouring VIP orders.", inputArcs: [ { placeId: "place_orders", @@ -864,6 +934,8 @@ return { { id: "trans_convert_to_backorder", name: "Convert aging order to backorder", + description: + "An open order that has outlived its promised lead time while stock is empty becomes a backorder.", inputArcs: [ { placeId: "place_orders", @@ -899,6 +971,8 @@ return { Backorders: [{ age: order.age, priority: order.priority, promised_lead_ { id: "trans_fulfill_backorder", name: "Fulfill backorder", + description: + "Once stock is replenished, ships a waiting backorder (VIPs first), with an age surcharge on its cost.", inputArcs: [ { placeId: "place_backorders", @@ -940,6 +1014,8 @@ return { { id: "trans_cancel_backorder", name: "Customer cancels backorder", + description: + "A waiting customer gives up: the cancellation hazard grows with the backorder's age, and VIP customers are more patient.", inputArcs: [ { placeId: "place_backorders", @@ -969,6 +1045,8 @@ return {};`, { id: "trans_outbound_delivered", name: "Outbound delivered", + description: + "An arrived outbound shipment below the loss threshold reaches the customer.", inputArcs: [ { placeId: "place_outbound", @@ -997,6 +1075,8 @@ return {};`, { id: "trans_outbound_lost", name: "Outbound lost or damaged", + description: + "The competing last-mile outcome: an arrived shipment at or above the loss threshold never reaches the customer.", inputArcs: [ { placeId: "place_outbound", @@ -1026,6 +1106,8 @@ return {};`, { id: "type_shipment", name: "Shipment", + description: + "Goods in transit, inbound or outbound: time to arrival (eta), damage/loss risk score, originating supplier, and accumulated cost.", iconSlug: "truck", displayColor: "#2563eb", elements: [ @@ -1054,6 +1136,8 @@ return {};`, { id: "type_order", name: "Customer order", + description: + "A customer order: its age since arrival, VIP-or-standard priority, and the lead time promised to the customer.", iconSlug: "clipboard", displayColor: "#f97316", elements: [ @@ -1077,6 +1161,8 @@ return {};`, { id: "type_batch", name: "Production batch", + description: + "A batch on the factory floor: remaining processing time, decaying quality, the supplier mix of its material, and accumulated cost.", iconSlug: "box", displayColor: "#16a34a", elements: [ @@ -1105,6 +1191,8 @@ return {};`, { id: "type_machine", name: "Factory machine", + description: + "The production machine, tracked by health (gates production and drops on breakdown) and wear (raises the breakdown hazard).", iconSlug: "cog", displayColor: "#64748b", elements: [ diff --git a/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.test.ts b/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.test.ts index d9e5cd380f1..ad5a9483e6b 100644 --- a/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.test.ts +++ b/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.test.ts @@ -290,6 +290,112 @@ describe("parseSDCPNFile", () => { }); }); + describe("description and metadata", () => { + it("preserves description and metadata on every element that carries them", () => { + const result = parseSDCPNFile({ + version: 1, + meta: { generator: "Petrinaut" }, + ...minimalSDCPN, + description: "A test net", + metadata: { source: "flow-1", tags: ["a", "b"], nested: { n: 1 } }, + places: [{ ...minimalPlace, description: "A place" }], + transitions: [ + { + ...minimalTransition, + description: "A transition", + metadata: { stepKind: "action" }, + }, + ], + types: [ + { + id: "c1", + name: "Colour 1", + description: "A colour", + iconSlug: "circle", + displayColor: "#FF0000", + elements: [], + }, + ], + componentInstances: [ + { + id: "instance-1", + name: "Instance1", + description: "An instance", + metadata: { origin: "imported" }, + subnetId: "subnet-1", + parameterValues: {}, + x: 0, + y: 0, + }, + ], + subnets: [ + { + id: "subnet-1", + name: "Subnet 1", + description: "A subnet", + metadata: { origin: "imported" }, + places: [], + transitions: [], + types: [], + differentialEquations: [], + parameters: [], + }, + ], + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.sdcpn.description).toBe("A test net"); + expect(result.sdcpn.metadata).toEqual({ + source: "flow-1", + tags: ["a", "b"], + nested: { n: 1 }, + }); + expect(result.sdcpn.places[0]?.description).toBe("A place"); + expect(result.sdcpn.transitions[0]?.description).toBe("A transition"); + expect(result.sdcpn.transitions[0]?.metadata).toEqual({ + stepKind: "action", + }); + expect(result.sdcpn.types[0]?.description).toBe("A colour"); + expect(result.sdcpn.componentInstances?.[0]).toMatchObject({ + description: "An instance", + metadata: { origin: "imported" }, + }); + expect(result.sdcpn.subnets?.[0]).toMatchObject({ + description: "A subnet", + metadata: { origin: "imported" }, + }); + }); + + it("parses files without description or metadata", () => { + const result = parseSDCPNFile({ + version: 1, + meta: { generator: "Petrinaut" }, + ...minimalSDCPN, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.sdcpn.description).toBeUndefined(); + expect(result.sdcpn.metadata).toBeUndefined(); + expect(result.sdcpn.places[0]?.description).toBeUndefined(); + expect(result.sdcpn.transitions[0]?.metadata).toBeUndefined(); + }); + + it("preserves description and metadata in the legacy format", () => { + const result = parseSDCPNFile({ + ...minimalSDCPN, + description: "A test net", + metadata: { source: "flow-1" }, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.sdcpn.description).toBe("A test net"); + expect(result.sdcpn.metadata).toEqual({ source: "flow-1" }); + }); + }); + describe("legacy format (no version)", () => { it("parses a valid legacy file", () => { const result = parseSDCPNFile(minimalSDCPN); diff --git a/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.test.ts b/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.test.ts index 85efb8b011a..2ec279c327f 100644 --- a/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.test.ts +++ b/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.test.ts @@ -7,6 +7,8 @@ const sourceDocument = { version: 1, meta: { generator: "Petrinaut" }, title: "Test Net", + description: "A test net", + metadata: { source: "test" }, places: [ { id: "p1", @@ -31,7 +33,16 @@ const sourceDocument = { y: 200, }, ], - subnets: [{ id: "subnet1", name: "Subnet 1", places: [], transitions: [] }], + subnets: [ + { + id: "subnet1", + name: "Subnet 1", + description: "A subnet", + metadata: { origin: "test" }, + places: [], + transitions: [], + }, + ], componentInstances: [ { id: "instance1", name: "Instance 1", subnetId: "subnet1", x: 0, y: 0 }, ], @@ -122,6 +133,8 @@ describe("serializeSDCPN", () => { "version", "meta", "title", + "description", + "metadata", "parameters", "types", "differentialEquations", @@ -135,6 +148,8 @@ describe("serializeSDCPN", () => { expect(Object.keys(document.subnets![0]!)).toEqual([ "id", "name", + "description", + "metadata", "parameters", "types", "differentialEquations", diff --git a/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.ts b/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.ts index 449b4ecf339..a3fac5c890b 100644 --- a/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.ts +++ b/libs/@hashintel/petrinaut-core/src/file-format/serialize-sdcpn.ts @@ -17,6 +17,8 @@ const DOCUMENT_KEY_ORDER = [ "version", "meta", "title", + "description", + "metadata", "parameters", "types", "differentialEquations", @@ -31,6 +33,8 @@ const DOCUMENT_KEY_ORDER = [ const SUBNET_KEY_ORDER = [ "id", "name", + "description", + "metadata", "parameters", "types", "differentialEquations", diff --git a/libs/@hashintel/petrinaut-core/src/file-format/types.ts b/libs/@hashintel/petrinaut-core/src/file-format/types.ts index ace36df6bca..a5dcb2902c9 100644 --- a/libs/@hashintel/petrinaut-core/src/file-format/types.ts +++ b/libs/@hashintel/petrinaut-core/src/file-format/types.ts @@ -5,8 +5,10 @@ import { colorElementSchema as currentColorElementSchema, colorSchema as currentColorSchema, componentInstanceSchema as currentComponentInstanceSchema, + descriptionSchema, differentialEquationSchema as currentDifferentialEquationSchema, inputArcSchema as currentInputArcSchema, + metadataSchema, outputArcSchema as currentOutputArcSchema, parameterSchema as currentParameterSchema, placeSchema as currentPlaceSchema, @@ -137,6 +139,8 @@ const componentInstanceSchema = z.object({ const subnetSchema = z.object({ id: z.string(), name: z.string(), + description: descriptionSchema, + metadata: metadataSchema, places: z.array(placeSchema), transitions: z.array(transitionSchema), types: z.array(colorSchema).default([]), @@ -146,6 +150,8 @@ const subnetSchema = z.object({ }); export const sdcpnSchema = z.object({ + description: descriptionSchema, + metadata: metadataSchema, places: z.array(placeSchema), transitions: z.array(transitionSchema), types: z.array(colorSchema).default([]), diff --git a/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts b/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts index d986baa7213..3abf71c59fa 100644 --- a/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts +++ b/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts @@ -30,6 +30,15 @@ export const idSchema = z.string().min(1).meta({ "Stable identifier for an SDCPN entity. Use unique IDs within the net.", }); +export const descriptionSchema = z.string().optional().meta({ + description: "Optional human-readable summary shown to users.", +}); + +export const metadataSchema = z.record(z.string(), z.json()).optional().meta({ + description: + "Optional host-defined data. Petrinaut treats it as opaque and never renders it.", +}); + export const positionSchema = z .strictObject({ x: z.number().meta({ @@ -188,6 +197,7 @@ export const placeSchema = z description: "PascalCase identifier used DIRECTLY in user code: lambdas and kernels reference input/output places as `input.PlaceName` and `{ PlaceName: [...] }`, metrics access them as `state.places.PlaceName.count`, scenario code-mode initial state keys are place names, and visualizer scope is implicitly per-place. Renaming a place breaks every code reference, so rename only when you also update dependent lambda/kernel/dynamics/metric/visualizer/scenario code in the same batch.", }), + description: descriptionSchema, colorId: idSchema.nullable().meta({ description: "ID of the token colour/type accepted by this place, or null for uncoloured token counts. Uncoloured places have no token attributes and do not appear in lambda/kernel `input` objects.", @@ -234,6 +244,8 @@ export const transitionSchema = z name: displayNameSchema.meta({ description: "Human-readable transition name.", }), + description: descriptionSchema, + metadata: metadataSchema, inputArcs: z.array(inputArcSchema).meta({ description: "Input arcs that gate transition firing. Standard arcs consume tokens, read arcs observe tokens without consuming them, and inhibitor arcs block firing based on token counts.", @@ -289,6 +301,7 @@ export const colorSchema = z name: displayNameSchema.meta({ description: "Human-readable colour/type name.", }), + description: descriptionSchema, iconSlug: z.string().min(1).meta({ description: 'Short icon identifier used by the UI for this colour/type. Typical values are `"circle"` or `"square"`; the UI defaults to `"circle"`.', @@ -377,6 +390,8 @@ export const componentInstanceSchema = z description: "PascalCase name for the component instance (e.g. MainProcessor, Ward2). Used as a code-level identifier.", }), + description: descriptionSchema, + metadata: metadataSchema, subnetId: idSchema.meta({ description: "ID of the subnet definition this component instantiates.", }), @@ -402,6 +417,8 @@ export const subnetSchema = z name: displayNameSchema.meta({ description: "Human-readable subnet name.", }), + description: descriptionSchema, + metadata: metadataSchema, places: z.array(placeSchema).meta({ description: "Places local to this subnet.", }), diff --git a/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts index c8ba93488cf..c0cddd7574b 100644 --- a/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts +++ b/libs/@hashintel/petrinaut-core/src/types/sdcpn-input.ts @@ -12,6 +12,7 @@ import type { Scenario, SDCPN, Subnet, + Transition, } from "./sdcpn"; /** @@ -32,6 +33,8 @@ import type { * so existing callers are unaffected. */ export type SDCPNInput = { + description?: string; + metadata?: SDCPN["metadata"]; places: SDCPNPlaceInput[]; transitions: SDCPNTransitionInput[]; /** @default [] */ @@ -49,6 +52,7 @@ export type SDCPNInput = { export type SDCPNPlaceInput = { id: ID; name: string; + description?: string; x: number; y: number; /** @default null */ @@ -87,6 +91,8 @@ export type SDCPNOutputArcInput = SDCPNArcEndpointInput & { export type SDCPNTransitionInput = { id: ID; name: string; + description?: string; + metadata?: Transition["metadata"]; inputArcs: SDCPNInputArcInput[]; outputArcs: SDCPNOutputArcInput[]; x: number; @@ -137,6 +143,9 @@ export function normalizeSDCPN(input: SDCPNInput): SDCPN { x: place.x, y: place.y, }; + if (place.description !== undefined) { + normalized.description = place.description; + } if (place.isPort !== undefined) { normalized.isPort = place.isPort; } @@ -148,33 +157,48 @@ export function normalizeSDCPN(input: SDCPNInput): SDCPN { } return normalized; }), - transitions: input.transitions.map((transition) => ({ - id: transition.id, - name: transition.name, - inputArcs: transition.inputArcs.map( - (arc): InputArc => ({ - ...arcEndpointFields(arc), - weight: arc.weight ?? 1, - type: arc.type ?? "standard", - }), - ), - outputArcs: transition.outputArcs.map( - (arc): OutputArc => ({ - ...arcEndpointFields(arc), - weight: arc.weight ?? 1, - }), - ), - lambdaType: transition.lambdaType ?? "predicate", - lambdaCode: transition.lambdaCode ?? "", - transitionKernelCode: transition.transitionKernelCode ?? "", - x: transition.x, - y: transition.y, - })), + transitions: input.transitions.map((transition) => { + const normalized: SDCPN["transitions"][number] = { + id: transition.id, + name: transition.name, + inputArcs: transition.inputArcs.map( + (arc): InputArc => ({ + ...arcEndpointFields(arc), + weight: arc.weight ?? 1, + type: arc.type ?? "standard", + }), + ), + outputArcs: transition.outputArcs.map( + (arc): OutputArc => ({ + ...arcEndpointFields(arc), + weight: arc.weight ?? 1, + }), + ), + lambdaType: transition.lambdaType ?? "predicate", + lambdaCode: transition.lambdaCode ?? "", + transitionKernelCode: transition.transitionKernelCode ?? "", + x: transition.x, + y: transition.y, + }; + if (transition.description !== undefined) { + normalized.description = transition.description; + } + if (transition.metadata !== undefined) { + normalized.metadata = transition.metadata; + } + return normalized; + }), types: input.types ?? [], parameters: input.parameters ?? [], differentialEquations: input.differentialEquations ?? [], }; + if (input.description !== undefined) { + result.description = input.description; + } + if (input.metadata !== undefined) { + result.metadata = input.metadata; + } if (input.scenarios !== undefined) { result.scenarios = input.scenarios; } diff --git a/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts b/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts index 070f1f9e7d6..23454a7c68f 100644 --- a/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts +++ b/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts @@ -5,6 +5,14 @@ export type ID = string; +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + export type ColorElementType = | "real" | "integer" @@ -63,6 +71,9 @@ export type OutputArc = ArcEndpointReference & { export type Transition = { id: ID; name: string; + description?: string; + /** Host-defined data, opaque to Petrinaut. */ + metadata?: Record; inputArcs: InputArc[]; outputArcs: OutputArc[]; lambdaType: "predicate" | "stochastic"; @@ -76,6 +87,7 @@ export type Transition = { export type Place = { id: ID; name: string; + description?: string; colorId: null | ID; dynamicsEnabled: boolean; differentialEquationId: null | ID; @@ -104,6 +116,7 @@ export type Place = { export type Color = { id: ID; name: string; + description?: string; iconSlug: string; // e.g., "circle", "square" displayColor: string; // e.g., "#FF0000" elements: { @@ -325,6 +338,9 @@ export type ComponentInstance = { id: ID; /** Display name for this instance. */ name: string; + description?: string; + /** Host-defined data, opaque to Petrinaut. */ + metadata?: Record; /** ID of the subnet this instance instantiates. */ subnetId: ID; /** @@ -340,6 +356,9 @@ export type ComponentInstance = { export type Subnet = { id: ID; name: string; + description?: string; + /** Host-defined data, opaque to Petrinaut. */ + metadata?: Record; places: Place[]; transitions: Transition[]; types: Color[]; @@ -349,6 +368,9 @@ export type Subnet = { }; export type SDCPN = { + description?: string; + /** Host-defined data, opaque to Petrinaut. */ + metadata?: Record; places: Place[]; transitions: Transition[]; types: Color[]; diff --git a/libs/@hashintel/petrinaut/AGENTS.md b/libs/@hashintel/petrinaut/AGENTS.md index 59b045b6b4e..790beba0da8 100644 --- a/libs/@hashintel/petrinaut/AGENTS.md +++ b/libs/@hashintel/petrinaut/AGENTS.md @@ -85,7 +85,7 @@ Hand-written MDX in `libs/@local/petrinaut-arch-docs/content/` is optional. Give These apply to changes in `petrinaut`, `petrinaut-core`, and `petrinaut-cli`. -- Changesets: `@hashintel/petrinaut` and `@hashintel/petrinaut-core` always get a `patch` bump, never `minor`, whatever the change. One changeset per PR; pure refactors get none. `@local/*` and `@apps/*` packages never publish. +- Changesets: `@hashintel/petrinaut` and `@hashintel/petrinaut-core` always get a `patch` bump, never `minor`, whatever the change. Write one changeset per affected package, each describing only that package's consumer-facing changes; pure refactors get none. `@local/*` and `@apps/*` packages never publish. - Review fixes and scope additions land as dedicated commits on an open PR. - Never commit `mise.lock` changes or generated arch-docs output. Stage explicit paths, not `git add -A`. - Diagrams in Petrinaut documentation are D2, in `libs/@local/petrinaut-arch-docs/content/diagrams/`. Design and performance write-ups live in that package's `content/` tree, not in loose package `.md` files. A documented flaw that needs a refactor gets a `:::danger` aside and a Linear ticket. diff --git a/libs/@hashintel/petrinaut/docs/drawing-a-net.md b/libs/@hashintel/petrinaut/docs/drawing-a-net.md index 9e6363a8c21..aabb9b9d485 100644 --- a/libs/@hashintel/petrinaut/docs/drawing-a-net.md +++ b/libs/@hashintel/petrinaut/docs/drawing-a-net.md @@ -56,7 +56,7 @@ 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. +New nodes are named automatically (Place1, Place2, Transition1, etc.). Rename them by selecting the node and editing the name in the properties panel. Places, transitions, component instances, and types can also carry an optional **Description**, edited in the same panel below the name. add-place-transition-toolbar diff --git a/libs/@hashintel/petrinaut/src/ui/components/description-field.tsx b/libs/@hashintel/petrinaut/src/ui/components/description-field.tsx new file mode 100644 index 00000000000..14191738291 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/components/description-field.tsx @@ -0,0 +1,86 @@ +import { useLayoutEffect, useRef } from "react"; + +import { Form, TextArea, Tooltip } from "@hashintel/ds-components"; +import { css } from "@hashintel/ds-helpers/css"; + +import { useDraftField } from "../hooks/use-draft-field"; + +interface DescriptionTextAreaProps { + /** Stable identifier of the entity owning this field; switching it discards stale drafts. */ + sourceId: string; + /** Current canonical description. */ + sourceValue: string | undefined; + /** Called on blur when the draft differs from `sourceValue`; an empty draft passes `undefined`. */ + onCommit: (description: string | undefined) => void; + disabled?: boolean; + /** Tooltip shown when hovering the textarea itself (eg. a read-only hint). */ + tooltip?: string; +} + +const minHeight = 64; +const initialMaxHeight = 100; + +const textAreaStyle = css({ + minHeight: `[${minHeight}px]`, +}); + +/** + * Description textarea bound to a {@link useDraftField} draft. + * {@link DescriptionField} wraps it with a "Description" label. + */ +export const DescriptionTextArea: React.FC = ({ + sourceId, + sourceValue, + onCommit, + disabled = false, + tooltip, +}) => { + const canonicalValue = sourceValue ?? ""; + const field = useDraftField({ sourceId, sourceValue: canonicalValue }); + const textAreaRef = useRef(null); + + useLayoutEffect(() => { + const textArea = textAreaRef.current; + // The box around the textarea carries the resize handle, so its inline + // height is what a drag writes and what the initial fit sets. + const box = textArea?.parentElement; + if (!textArea || !box) { + return; + } + box.style.height = ""; + const borderHeight = box.offsetHeight - box.clientHeight; + const contentHeight = textArea.scrollHeight + borderHeight; + box.style.height = `${Math.min(Math.max(contentHeight, minHeight), initialMaxHeight)}px`; + }, [sourceId]); + + return ( + +