Your first model#
An OSDL model is a JSON document. This page builds the smallest useful one, the M/M/1 queue, and runs it in your browser.
The finished document is spec/examples/mm1-queue.osdl.json, one of the specification's validated examples. Every excerpt below is taken from that file at build time.
A minimal document#
A model document is a single JSON object. Two fields are required: osdl pins the specification version, and model holds the model itself.
{
"osdl": "0.1",
"model": {
"name": "mm1-queue",
"components": [],
"connections": []
}
}Save it with the extension .osdl.json. An empty component list is already a valid document; it just has nothing to do.
Add components#
A component is an instance of a type from a component library, such as des.source or des.queue. The library definition determines what its params mean, what ports it exposes, and what state it publishes.
The M/M/1 queue needs four components: something that creates customers, a line for them to wait in, a server, and a place for them to leave.
[
{
"id": "arrivals",
"type": "des.source",
"params": {
"interarrival": {
"dist": "exponential",
"rate": 0.9
}
}
},
{
"id": "waiting",
"type": "des.queue",
"params": {
"discipline": "fifo"
}
},
{
"id": "teller",
"type": "des.server",
"params": {
"serviceTime": {
"dist": "exponential",
"rate": 1
}
}
},
{
"id": "done",
"type": "des.sink"
}
]Two of the params are distributions rather than numbers. interarrival samples a fresh exponential delay for every arrival, and serviceTime samples one for every customer served. Rates are per model time unit, so this queue receives 0.9 arrivals per time unit and serves 1.0 when busy.
Connect the ports#
Connections join an out port to an in port, named as componentId.portId. Direction is the direction entities flow.
[
{
"from": "arrivals.out",
"to": "waiting.in"
},
{
"from": "waiting.out",
"to": "teller.in"
},
{
"from": "teller.out",
"to": "done.in"
}
]The engine validates every connection against the library's port declarations before a run starts. A connection to a port that does not exist, or between ports of different kinds, is rejected at load time.
Declare an experiment#
An experiment names a run configuration: how long to run, how many replications, which seed, and what to record.
{
"name": "baseline",
"duration": 10000,
"replications": 30,
"seed": 7,
"outputs": [
{
"path": "waiting.length",
"records": [
{
"type": "summary"
}
]
},
{
"path": "teller.utilization",
"records": [
{
"type": "summary"
}
]
}
]
}Outputs address observable state by path. waiting.length is the queue's published length; teller.utilization is the fraction of time the server was busy. A summary recorder computes time-weighted statistics over the run.
Replication r uses an RNG stream derived deterministically from the experiment seed, so the full experiment is reproducible from the document alone.
Run it#
This is the model running in your browser: the engine compiled to WebAssembly, driving the scene and chart from the typed event stream.
View the model document
{
"$schema": "https://osdl.dev/schemas/0.1/osdl.schema.json",
"osdl": "0.1",
"model": {
"name": "mm1-queue",
"label": "M/M/1 Queue",
"description": "The classic single-server queue: Poisson arrivals (rate 0.9), exponential service (rate 1.0), one teller, FIFO discipline. Expected steady-state utilization 0.9.",
"components": [
{
"id": "arrivals",
"type": "des.source",
"params": {
"interarrival": {
"dist": "exponential",
"rate": {
"param": "arrivalRate"
}
}
}
},
{
"id": "waiting",
"type": "des.queue",
"params": {
"discipline": "fifo"
}
},
{
"id": "teller",
"type": "des.server",
"params": {
"serviceTime": {
"dist": "exponential",
"rate": {
"param": "serviceRate"
}
}
}
},
{
"id": "done",
"type": "des.sink"
}
],
"connections": [
{
"from": "arrivals.out",
"to": "waiting.in"
},
{
"from": "waiting.out",
"to": "teller.in"
},
{
"from": "teller.out",
"to": "done.in"
}
],
"parameters": [
{
"name": "arrivalRate",
"description": "Poisson arrival rate.",
"default": 0.9,
"min": 0.1,
"max": 1.4,
"unit": "1/t"
},
{
"name": "serviceRate",
"description": "Exponential service rate.",
"default": 1,
"min": 0.5,
"max": 2,
"unit": "1/t"
}
]
},
"experiments": [
{
"name": "baseline",
"duration": 10000,
"replications": 30,
"seed": 7,
"outputs": [
{
"path": "waiting.length",
"records": [
{
"type": "summary"
}
]
},
{
"path": "teller.utilization",
"records": [
{
"type": "summary"
}
]
}
]
},
{
"name": "browser",
"duration": 150,
"replications": 1,
"seed": 7,
"outputs": [
{
"path": "waiting.length",
"records": [
{
"type": "timeseries"
}
]
},
{
"path": "waiting.waitTime",
"records": [
{
"type": "timeseries"
}
]
}
]
}
],
"views": [
{
"id": "main",
"label": "Process flow",
"type": "canvas",
"elements": [
{
"ref": "arrivals",
"x": 0,
"y": 0
},
{
"ref": "waiting",
"x": 220,
"y": 0
},
{
"ref": "teller",
"x": 440,
"y": 0
},
{
"ref": "done",
"x": 660,
"y": 0
}
]
}
]
}At an arrival rate of 0.9 and a service rate of 1.0, the queue stays finite but bursty, and the time-weighted utilization recorded by the baseline experiment settles near 0.9. Push the arrival rate past the service rate and the queue, and every customer's wait, grows without bound. That behavior, and the exact numbers, are what the experiment records.
Where next#
- M/M/1 queue: the same model, annotated component by component.
- SIR epidemic: continuous stock-and-flow dynamics.
- Hybrid clinic: agents, a queue, and a fatigue stock in one model.
- The OSDL specification: the document structure and execution semantics in full.
- Validate a model: check a document against the schemas and library definitions.