Simcraft Docs

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.

json
{
  "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.

spec/examples/mm1-queue.osdl.json · model.components
[
  {
    "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"
  }
]
The four components. Types are namespaced by the library that defines them; these all come from the standard des library.

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.

spec/examples/mm1-queue.osdl.json · model.connections
[
  {
    "from": "arrivals.out",
    "to": "waiting.in"
  },
  {
    "from": "waiting.out",
    "to": "teller.in"
  },
  {
    "from": "teller.out",
    "to": "done.in"
  }
]
A linear flow: arrivals into the queue, the queue into the teller, the teller into the sink.

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.

spec/examples/mm1-queue.osdl.json · experiments[0]
{
  "name": "baseline",
  "duration": 10000,
  "replications": 30,
  "seed": 7,
  "outputs": [
    {
      "path": "waiting.length",
      "records": [
        {
          "type": "summary"
        }
      ]
    },
    {
      "path": "teller.utilization",
      "records": [
        {
          "type": "summary"
        }
      ]
    }
  ]
}
The baseline experiment: 30 replications of 10,000 time units, recording time-weighted summaries of queue length and server utilization.

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.

M/M/1 queueloading…
The browser build of the specification document: the two rates are lifted into model parameters so you can drag them, and a shorter experiment with timeseries recorders replaces the 30-replication baseline. Drag the arrival rate toward the service rate and watch the queue lose stability.
View the model document
osdl · json · browser build
{
  "$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#