Version: 1.0.0 Format: JSON (UTF-8)
A .pvm file is a complete, self-contained description of a PathSim simulation model. It contains the block diagram (nodes, connections, subsystems), event definitions, user-defined Python code, simulation solver settings, and optional canvas annotations.
This document is the authoritative reference for anyone building tools that read or write .pvm files (e.g., code generators, importers, analyzers).
- 1. Root Structure
- 2. Metadata
- 3. Graph
- 4. Subsystems
- 5. Events
- 6. Code Context
- 7. Simulation Settings
- 8. Block Registry
- 9. UI-Only Fields
- 10. Component Files (.blk, .sub)
- 11. Worked Example
{
"version": "1.0.0",
"metadata": { ... },
"graph": {
"nodes": [ ... ],
"connections": [ ... ],
"annotations": [ ... ]
},
"events": [ ... ],
"codeContext": {
"code": "..."
},
"simulationSettings": { ... }
}| Field | Type | Required | Description |
|---|---|---|---|
version |
string | yes | File format version. Currently "1.0.0". |
metadata |
object | yes | File metadata (name, timestamps). |
graph |
object | yes | The block diagram. |
graph.nodes |
array | yes | Block instances. |
graph.connections |
array | yes | Wires between ports. |
graph.annotations |
array | no | Canvas text labels. UI-only, no simulation semantics. |
events |
array | no | Root-level event instances. |
codeContext |
object | no | User-defined Python code executed before simulation. |
simulationSettings |
object | no | Solver and timestepping configuration. |
{
"created": "2026-01-25T00:44:51.546Z",
"modified": "2026-01-25T00:44:51.546Z",
"name": "my-model",
"description": "Optional description"
}| Field | Type | Required | Description |
|---|---|---|---|
created |
string | yes | ISO 8601 timestamp. |
modified |
string | yes | ISO 8601 timestamp. |
name |
string | yes | Model display name. |
description |
string | no | Free-text description. |
Each node is an instance of a PathSim block class.
{
"id": "c03d0b76-28b8-4406-8f0a-ef81b732854d",
"type": "Adder",
"name": "Difference",
"position": { "x": 840, "y": 400 },
"inputs": [ ... ],
"outputs": [ ... ],
"params": { "operations": "\"+-\"" },
"pinnedParams": ["operations"],
"color": "#0070C0"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique node ID (typically UUID v4). |
type |
string | yes | PathSim block class name (e.g., "Integrator", "Adder", "Scope"). See Block Registry. Special values: "Subsystem", "Interface". |
name |
string | yes | User-editable display name. Used as variable name in code generation (after sanitization). |
position |
object | yes | Canvas position { x: number, y: number }. UI-only. |
inputs |
array | yes | Input port instances (can be empty []). |
outputs |
array | yes | Output port instances (can be empty []). |
params |
object | yes | Parameter values. Keys are parameter names, values are Python expressions stored as strings or numbers. See Parameters. |
pinnedParams |
array | no | Parameter names to show inline on the node. UI-only. |
color |
string | no | Custom node color (hex). UI-only. |
graph |
object | no | Only present on Subsystem nodes. See Subsystems. |
Ports define the input/output connection points on a node.
{
"id": "c03d0b76-28b8-4406-8f0a-ef81b732854d-input-0",
"nodeId": "c03d0b76-28b8-4406-8f0a-ef81b732854d",
"name": "in 0",
"direction": "input",
"index": 0,
"color": "#969696"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique port ID. Convention: ${nodeId}-${direction}-${index}. |
nodeId |
string | yes | ID of the owning node. |
name |
string | yes | Port display name (e.g., "in 0", "out 0", or a custom label). |
direction |
string | yes | "input" or "output". |
index |
number | yes | Zero-based port index within the direction. This is the index used by connections. |
color |
string | no | Port color (hex). Default: "#969696". UI-only. |
Port semantics for code generation: The index field maps directly to PathSim's port indexing. In generated Python code, node[0] refers to the first output port, and connections use the syntax Connection(source[sourcePortIndex], target[targetPortIndex]).
Parameter values are Python expressions stored as JSON values (usually strings, sometimes numbers or booleans). They are passed verbatim to PathSim constructors at runtime. PathSim handles all type checking and validation.
{
"gain": "2.5",
"initial_value": "x0",
"operations": "\"+-\"",
"func": "lambda x: x**2",
"amplitude": 1,
"_rotation": 2
}Key rules:
- String values are the most common:
"2.5","x0","np.pi". These are Python expressions. - Numeric values may appear (e.g.,
1,0.5). Treat as equivalent to their string form. - Values can reference variables defined in the Code Context (e.g.,
"x0","g"). - Values can be Python expressions:
"np.random.rand()","lambda t: np.sin(t)". - String-typed params are double-quoted inside the JSON string:
"\"+-\""represents the Python string"+-". - Empty string
""means "use PathSim default" (parameter not specified by the user). - Keys starting with
_are UI-internal (e.g.,_rotation,_color). Code generators should skip these.
A connection is a directed wire from a source output port to a target input port.
{
"id": "c47a35bc-3030-4699-ab4a-d7aabca093d8",
"sourceNodeId": "c03d0b76-28b8-4406-8f0a-ef81b732854d",
"sourcePortIndex": 0,
"targetNodeId": "353e895d-8dfa-4c17-a752-043d1fc38749",
"targetPortIndex": 0,
"waypoints": [ ... ]
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique connection ID. |
sourceNodeId |
string | yes | ID of the source node. |
sourcePortIndex |
number | yes | Index into the source node's outputs array. |
targetNodeId |
string | yes | ID of the target node. |
targetPortIndex |
number | yes | Index into the target node's inputs array. |
waypoints |
array | no | Route control points. UI-only, no simulation semantics. |
Fan-out: A single output port can connect to multiple input ports (multiple connections share the same sourceNodeId/sourcePortIndex).
No fan-in: Each input port receives at most one connection.
Waypoints (UI-only, can be ignored by code generators):
{
"id": "wp_001",
"position": { "x": 150, "y": 100 },
"isUserWaypoint": true
}Canvas text labels with Markdown/LaTeX support. Purely visual — no simulation semantics.
{
"id": "a0d9ddea-c4f8-4363-9c0a-5e19e6c52491",
"position": { "x": 660, "y": 260 },
"content": "# Linear Feedback System\n\nA description with $\\LaTeX$.",
"width": 500,
"height": 80,
"color": "#0070C0",
"fontSize": 12
}Code generators should ignore annotations entirely.
Subsystems are nodes that contain a nested block diagram. They enable hierarchical model composition.
A node with "type": "Subsystem" has a graph field containing its internal block diagram:
{
"id": "sub1",
"type": "Subsystem",
"name": "PID Controller",
"position": { "x": 300, "y": 150 },
"inputs": [
{ "id": "sub1-input-0", "nodeId": "sub1", "name": "error", "direction": "input", "index": 0 }
],
"outputs": [
{ "id": "sub1-output-0", "nodeId": "sub1", "name": "control", "direction": "output", "index": 0 }
],
"params": {},
"graph": {
"nodes": [ ... ],
"connections": [ ... ],
"annotations": [],
"events": []
}
}The subsystem's external inputs and outputs define its interface to the parent graph. Internal connections are fully contained in graph.connections.
Every subsystem graph contains exactly one Interface node. This node acts as the bridge between the subsystem's external ports and its internal graph.
Port direction is inverted relative to the parent subsystem:
- Parent subsystem's inputs become Interface's outputs (data flows into the subsystem, out of the Interface to internal blocks)
- Parent subsystem's outputs become Interface's inputs (data flows from internal blocks into the Interface, out of the subsystem)
Parent graph: Inside subsystem:
┌─────────────────────────┐
──[input 0]──> Subsystem │ Interface ──[output 0]──> internal blocks
│ Interface <──[input 0]── internal blocks
<──[output 0]── Subsystem │ │
└─────────────────────────┘
Interface node example:
{
"id": "iface1",
"type": "Interface",
"name": "Interface",
"position": { "x": 50, "y": 50 },
"inputs": [
{ "id": "iface1-input-0", "nodeId": "iface1", "name": "control", "direction": "input", "index": 0 }
],
"outputs": [
{ "id": "iface1-output-0", "nodeId": "iface1", "name": "error", "direction": "output", "index": 0 }
],
"params": {}
}Subsystems can be nested arbitrarily deep. A subsystem's graph.nodes can contain other Subsystem nodes, each with their own graph and Interface.
In PathSim Python code, subsystems map to Subsystem(blocks=[...], connections=[...]) constructors. The Interface maps to Interface(). See scripts/pvm2py.py for a reference implementation.
Events define discrete-time behavior (zero-crossing detection, scheduled actions, etc.).
Events can appear in two places:
- Root level:
file.events[]— global events for the top-level simulation. - Inside subsystems:
node.graph.events[]— events scoped to that subsystem.
{
"id": "1455e789-9e7d-4139-8a4e-896feecff3d3",
"type": "pathsim.events.ZeroCrossing",
"name": "Bounce",
"position": { "x": 1320, "y": 200 },
"params": {
"func_evt": "bounce_detect",
"func_act": "bounce_resolve"
},
"color": "#FF6B6B"
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique event ID. |
type |
string | yes | Fully qualified event type (e.g., "pathsim.events.ZeroCrossing"). The class name is the part after the last .. |
name |
string | yes | User-defined name. Used as variable name in code generation. |
position |
object | yes | Canvas position. UI-only. |
params |
object | yes | Event parameters (Python expressions). |
color |
string | no | Custom color. UI-only. |
Common event types:
| Type | Description | Key Parameters |
|---|---|---|
pathsim.events.ZeroCrossing |
Triggers when a function crosses zero (bidirectional) | func_evt, func_act, tolerance |
pathsim.events.ZeroCrossingUp |
Triggers on positive-going zero crossing | func_evt, func_act, tolerance |
pathsim.events.ZeroCrossingDown |
Triggers on negative-going zero crossing | func_evt, func_act, tolerance |
pathsim.events.Condition |
Triggers when a boolean condition becomes true | func_evt, func_act |
pathsim.events.Schedule |
Time-based periodic event | func_act, t_start, t_end, t_period |
Event parameters reference Python callables, typically defined in the Code Context. For example, "func_evt": "bounce_detect" references a function bounce_detect defined in codeContext.code.
User-defined Python code that runs before the simulation. Used to define variables, helper functions, and event callbacks.
{
"code": "# gravity\ng = 9.81\n\n# event callback\ndef bounce_detect(t):\n return pos.engine.state\n"
}This code is executed in the simulation namespace and can:
- Define variables referenced by node parameters (e.g., a node param
"gain": "g"referencesg = 9.81) - Import additional libraries
- Define functions used by events
- Access block instances by their sanitized variable names
The code runs after import numpy as np and import matplotlib.pyplot as plt are already available.
{
"duration": "10",
"dt": "0.01",
"solver": "RKBS32",
"adaptive": true,
"atol": "1e-6",
"rtol": "1e-4",
"ftol": "1e-9",
"dt_min": "",
"dt_max": "0.1",
"ghostTraces": 6,
"plotResults": true
}| Field | Type | Required | Description |
|---|---|---|---|
duration |
string | yes | Simulation duration. Python expression. |
dt |
string | yes | Initial/fixed time step. Python expression. |
solver |
string | yes | Solver type (see below). |
adaptive |
boolean | yes | Enable adaptive timestepping. |
atol |
string | yes | Absolute local truncation error tolerance. Python expression. |
rtol |
string | yes | Relative local truncation error tolerance. Python expression. |
ftol |
string | yes | Fixed-point iteration convergence tolerance. Python expression. |
dt_min |
string | yes | Minimum timestep (adaptive only). Python expression. |
dt_max |
string | yes | Maximum timestep (adaptive only). Python expression. |
ghostTraces |
number | no | Number of previous results to overlay. UI-only. |
plotResults |
boolean | no | Auto-open plot panel after simulation. UI-only. |
Empty string "" means "use PathSim default." Default values used by pvm2py:
| Setting | Default |
|---|---|
duration |
10.0 |
dt |
0.01 |
solver |
SSPRK22 |
atol |
1e-6 |
rtol |
1e-3 |
ftol |
1e-12 |
dt_min |
1e-12 |
dt_max |
(none) |
Available solvers:
| Solver | Type | Description |
|---|---|---|
SSPRK22 |
Explicit | Strong stability preserving Runge-Kutta (2,2). Default. |
RK4 |
Explicit | Classic 4th-order Runge-Kutta. Fixed step only. |
RKBS32 |
Explicit | Bogacki-Shampine (3,2). Adaptive capable. |
RKCK54 |
Explicit | Cash-Karp (5,4). Adaptive capable. |
BDF2 |
Implicit | Backward differentiation formula, 2nd order. For stiff systems. |
GEAR52A |
Implicit | Gear's method (5,2). For stiff systems. |
ESDIRK43 |
Implicit | Explicit singly diagonally implicit Runge-Kutta (4,3). |
The file scripts/generated/registry.json maps block type names to their PathSim import paths and valid parameter names. It is generated from PathSim source by npm run extract (or python scripts/extract.py).
{
"blocks": {
"Constant": {
"blockClass": "Constant",
"importPath": "pathsim.blocks",
"params": ["value"]
},
"Integrator": {
"blockClass": "Integrator",
"importPath": "pathsim.blocks",
"params": ["initial_value"]
},
"Adder": {
"blockClass": "Adder",
"importPath": "pathsim.blocks",
"params": ["operations"]
}
},
"events": {
"ZeroCrossing": {
"eventClass": "ZeroCrossing",
"importPath": "pathsim.events",
"params": ["func_evt", "func_act", "tolerance"]
}
}
}For code generators: Use this registry to:
- Resolve
node.typeto the correct PathSim class name and import path - Filter
node.paramsto only include keys listed inregistry.blocks[type].params(skip_-prefixed keys and any keys not in the list)
The full registry is checked into the repo at scripts/generated/registry.json.
These fields are used by the PathView editor for layout and display. Code generators and analysis tools should ignore them:
| Field | Where | Description |
|---|---|---|
node.position |
nodes | Canvas x/y position |
node.color |
nodes | Custom node color |
node.pinnedParams |
nodes | Inline parameter display |
port.color |
ports | Port color |
connection.waypoints |
connections | Wire routing control points |
annotations |
graph | Canvas text labels |
event.position |
events | Canvas position |
event.color |
events | Custom color |
params._rotation |
params | Node rotation (0-3, quarter turns) |
params._color |
params | Node color (legacy) |
simulationSettings.ghostTraces |
settings | Ghost trace display count |
simulationSettings.plotResults |
settings | Auto-open plot panel |
PathView also supports single-component files for sharing individual blocks or subsystems.
{
"version": "1.0",
"type": "block",
"metadata": { "name": "MyBlock", "created": "...", "modified": "..." },
"content": {
"node": { ... }
}
}{
"version": "1.0",
"type": "subsystem",
"metadata": { "name": "MySubsystem", "created": "...", "modified": "..." },
"content": {
"node": { ... }
}
}The content.node field contains a single NodeInstance in the same format as nodes in the .pvm graph. For subsystems, the node includes the graph field.
A minimal model with a step input feeding an integrator, recorded by a scope:
{
"version": "1.0.0",
"metadata": {
"created": "2026-01-01T00:00:00.000Z",
"modified": "2026-01-01T00:00:00.000Z",
"name": "step-response"
},
"graph": {
"nodes": [
{
"id": "src1",
"type": "StepSource",
"name": "Step",
"position": { "x": 100, "y": 200 },
"inputs": [],
"outputs": [
{ "id": "src1-output-0", "nodeId": "src1", "name": "out 0", "direction": "output", "index": 0 }
],
"params": { "amplitude": "1.0", "tau": "1.0" }
},
{
"id": "int1",
"type": "Integrator",
"name": "Integrator",
"position": { "x": 300, "y": 200 },
"inputs": [
{ "id": "int1-input-0", "nodeId": "int1", "name": "in 0", "direction": "input", "index": 0 }
],
"outputs": [
{ "id": "int1-output-0", "nodeId": "int1", "name": "out 0", "direction": "output", "index": 0 }
],
"params": { "initial_value": "0.0" }
},
{
"id": "scope1",
"type": "Scope",
"name": "Output",
"position": { "x": 500, "y": 200 },
"inputs": [
{ "id": "scope1-input-0", "nodeId": "scope1", "name": "in 0", "direction": "input", "index": 0 }
],
"outputs": [],
"params": {}
}
],
"connections": [
{
"id": "conn1",
"sourceNodeId": "src1",
"sourcePortIndex": 0,
"targetNodeId": "int1",
"targetPortIndex": 0
},
{
"id": "conn2",
"sourceNodeId": "int1",
"sourcePortIndex": 0,
"targetNodeId": "scope1",
"targetPortIndex": 0
}
]
},
"events": [],
"codeContext": { "code": "" },
"simulationSettings": {
"duration": "10",
"dt": "0.01",
"solver": "SSPRK22",
"adaptive": false,
"atol": "1e-6",
"rtol": "1e-3",
"ftol": "1e-12",
"dt_min": "",
"dt_max": "",
"ghostTraces": 0,
"plotResults": true
}
}The equivalent Python code generated by pvm2py:
from pathsim import Simulation, Connection
from pathsim.blocks import StepSource, Integrator, Scope
from pathsim.solvers import SSPRK22
step = StepSource(amplitude=1.0, tau=1.0)
integrator = Integrator(initial_value=0.0)
output = Scope()
sim = Simulation(
[step, integrator, output],
[
Connection(step[0], integrator[0]),
Connection(integrator[0], output[0]),
],
Solver=SSPRK22,
dt=0.01,
)
sim.run(duration=10)-
Parameters are Python expressions. The values in
paramsare not guaranteed to be numeric literals. They can be variable references ("x0"), expressions ("np.pi / 4"), or callables ("lambda t: np.sin(t)"). A C code generator will need to either evaluate these at export time or map a supported subset. -
The registry is your friend. Use
scripts/generated/registry.jsonto know which parameters are valid for each block type, and to resolve import paths. Parameters not in the registry are either dead or UI-internal. -
Scope and Spectrum blocks are recording sinks. They have inputs but no outputs. A C code generator might map these to data logging or output arrays.
-
Source blocks have no inputs.
Constant,StepSource,SinusoidalSource, etc., are pure signal sources with only output ports. -
Reference implementation. The
scripts/pvm2py.pyscript is a complete, working.pvm-to-Python code generator. It demonstrates how to traverse the graph, handle subsystems, resolve imports, and generate simulation code.