Simcraft Docs

Hybrid clinic#

A walk-in clinic serving a population of 500. People fall sick at random, walk in, queue for treatment, and go home to recover. Meanwhile the doctors tire: a continuous fatigue stock rises with utilization and lowers the clinic's effective capacity. Agents, a queue, and a feedback loop, in one document on one clock.

Generated from spec/examples/clinic-hybrid.osdl.json

abm des sdparadigms
7components
5connections
1experiments
hourstime unit

Three paradigms, one calendar#

Each part of this model uses the paradigm that fits it. The population is agent-based, because falling sick is a per-person state change. The clinic is discrete-event, because queues and service are event-driven. Fatigue is system dynamics, because it accumulates continuously.

In OSDL these are not three models coupled together. Agent transitions, entity transfers, and integration boundaries are all events on one calendar, ordered by time and insertion sequence. The paradigms interact through ordinary references: the fatigue stock reads the servers' utilization, and the servers' capacity reads the fatigue stock.

The component contracts are in the abm, des, and sd library references.

The document, piece by piece#

The population is a single abm.population component. Each agent carries a statechart: well, sick, recovering. Falling sick is a rate-triggered transition, and entering the sick state sends the agent out of the population as an entity, into the clinic:

spec/examples/clinic-hybrid.osdl.json · model.components[0].params.agent.behavior
{
  "initial": "well",
  "states": [
    {
      "id": "well"
    },
    {
      "id": "sick",
      "description": "Departs to the clinic on entry; remains in this state while queued and treated.",
      "onEnter": [
        {
          "type": "depart",
          "port": "out"
        }
      ]
    },
    {
      "id": "recovering"
    }
  ],
  "transitions": [
    {
      "from": "well",
      "to": "sick",
      "trigger": {
        "type": "rate",
        "rate": {
          "param": "sicknessRate"
        }
      }
    },
    {
      "from": "sick",
      "to": "recovering",
      "trigger": {
        "type": "arrive",
        "port": "in"
      },
      "actions": [
        {
          "type": "set",
          "target": "visits",
          "value": {
            "expr": "visits + 1"
          }
        }
      ]
    },
    {
      "from": "recovering",
      "to": "well",
      "trigger": {
        "type": "timeout",
        "delay": 48
      }
    }
  ]
}
The statechart. depart on entering sick turns an agent state change into a discrete entity flow; arriving back on the in port moves the agent to recovering.

The clinic itself is two des components. The servers' capacity is not a constant; it is an expression over the continuous side of the model:

spec/examples/clinic-hybrid.osdl.json · model.components[2].params
{
  "serviceTime": {
    "dist": "exponential",
    "rate": {
      "expr": "1 / treatmentTime"
    }
  },
  "capacity": {
    "expr": "effectiveDoctors.value"
  }
}
Capacity follows the effectiveDoctors auxiliary, so continuous fatigue throttles discrete service.

Fatigue is a stock filled by utilization and drained by rest. This closes the feedback loop: busy doctors tire, tired doctors serve fewer patients, the queue grows, and the doctors stay busy:

spec/examples/clinic-hybrid.osdl.json · model.components[4].params
{
  "rate": {
    "expr": "fatigueGain * doctors.utilization"
  }
}
The tiring flow: fatigue accumulates in proportion to the servers' utilization.

Run it#

Hybrid clinicloading…
The browser build runs a single replication of the one-week experiment; the model is the specification document, verbatim. Raise the sickness rate and watch the feedback loop close: the queue grows, fatigue climbs, and capacity falls just when it is needed most.
View the model document
osdl · json · browser build
{
  "$schema": "https://osdl.dev/schemas/0.1/osdl.schema.json",
  "osdl": "0.1",
  "model": {
    "name": "clinic-hybrid",
    "label": "Urgent-Care Clinic (hybrid DES + ABM + SD)",
    "description": "All three paradigms in one model. Patients are agents (ABM) who fall sick stochastically and depart into the clinic's discrete-event process (DES): a triage queue feeding doctors. Doctor workload accumulates as fatigue (SD), which feeds back to reduce effective treatment capacity. Treated patients return to the population and eventually recover to a well state.",
    "time": {
      "unit": "hours",
      "integration": {
        "method": "euler",
        "dt": 0.1
      }
    },
    "parameters": [
      {
        "name": "nPatients",
        "description": "Population size served by the clinic.",
        "type": "integer",
        "default": 500
      },
      {
        "name": "sicknessRate",
        "description": "Hazard of a well person falling sick.",
        "default": 0.005,
        "unit": "1/hour"
      },
      {
        "name": "treatmentTime",
        "description": "Mean treatment duration per patient.",
        "default": 0.75,
        "unit": "hours"
      },
      {
        "name": "baseDoctors",
        "description": "Doctors on shift when fully rested.",
        "type": "integer",
        "default": 3
      },
      {
        "name": "fatigueGain",
        "description": "Fatigue accumulated per hour at full utilization.",
        "default": 0.05,
        "unit": "1/hour"
      },
      {
        "name": "fatigueRecovery",
        "description": "Fractional fatigue recovery per hour.",
        "default": 0.1,
        "unit": "1/hour"
      }
    ],
    "components": [
      {
        "id": "patients",
        "type": "abm.population",
        "label": "Patient population",
        "params": {
          "size": {
            "param": "nPatients"
          },
          "agent": {
            "attributes": [
              {
                "name": "visits",
                "init": 0,
                "description": "Completed clinic visits."
              }
            ],
            "behavior": {
              "initial": "well",
              "states": [
                {
                  "id": "well"
                },
                {
                  "id": "sick",
                  "description": "Departs to the clinic on entry; remains in this state while queued and treated.",
                  "onEnter": [
                    {
                      "type": "depart",
                      "port": "out"
                    }
                  ]
                },
                {
                  "id": "recovering"
                }
              ],
              "transitions": [
                {
                  "from": "well",
                  "to": "sick",
                  "trigger": {
                    "type": "rate",
                    "rate": {
                      "param": "sicknessRate"
                    }
                  }
                },
                {
                  "from": "sick",
                  "to": "recovering",
                  "trigger": {
                    "type": "arrive",
                    "port": "in"
                  },
                  "actions": [
                    {
                      "type": "set",
                      "target": "visits",
                      "value": {
                        "expr": "visits + 1"
                      }
                    }
                  ]
                },
                {
                  "from": "recovering",
                  "to": "well",
                  "trigger": {
                    "type": "timeout",
                    "delay": 48
                  }
                }
              ]
            }
          }
        }
      },
      {
        "id": "triage",
        "type": "des.queue",
        "label": "Triage queue",
        "params": {
          "discipline": "fifo"
        }
      },
      {
        "id": "doctors",
        "type": "des.server",
        "label": "Doctors",
        "params": {
          "serviceTime": {
            "dist": "exponential",
            "rate": {
              "expr": "1 / treatmentTime"
            }
          },
          "capacity": {
            "expr": "effectiveDoctors.value"
          }
        }
      },
      {
        "id": "fatigue",
        "type": "sd.stock",
        "label": "Doctor fatigue",
        "params": {
          "initial": 0,
          "min": 0,
          "max": 1
        }
      },
      {
        "id": "tiring",
        "type": "sd.flow",
        "params": {
          "rate": {
            "expr": "fatigueGain * doctors.utilization"
          }
        }
      },
      {
        "id": "resting",
        "type": "sd.flow",
        "params": {
          "rate": {
            "expr": "fatigueRecovery * fatigue.value"
          }
        }
      },
      {
        "id": "effectiveDoctors",
        "type": "sd.auxiliary",
        "label": "Effective doctors",
        "description": "Fatigue feedback: rested doctors work at full capacity; exhausted doctors at half. Never below one doctor.",
        "params": {
          "value": {
            "expr": "max(1, round(baseDoctors * (1 - 0.5 * fatigue.value)))"
          }
        }
      }
    ],
    "connections": [
      {
        "id": "toTriage",
        "from": "patients.out",
        "to": "triage.in"
      },
      {
        "id": "toDoctors",
        "from": "triage.out",
        "to": "doctors.in"
      },
      {
        "id": "discharge",
        "from": "doctors.out",
        "to": "patients.in"
      },
      {
        "id": "tiringIn",
        "from": "tiring.out",
        "to": "fatigue.in"
      },
      {
        "id": "restingOut",
        "from": "fatigue.out",
        "to": "resting.in"
      }
    ]
  },
  "experiments": [
    {
      "name": "one-week",
      "description": "A week of clinic operation after a day of warmup.",
      "duration": 168,
      "warmup": 24,
      "replications": 10,
      "seed": 42,
      "outputs": [
        {
          "path": "triage.length",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "doctors.utilization",
          "records": [
            {
              "type": "summary",
              "statistics": [
                "mean",
                "max"
              ]
            }
          ]
        },
        {
          "path": "fatigue.value",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "patients.inState.sick",
          "records": [
            {
              "type": "summary"
            }
          ]
        }
      ]
    },
    {
      "name": "browser",
      "description": "A week of clinic operation after a day of warmup.",
      "duration": 168,
      "warmup": 24,
      "replications": 1,
      "seed": 42,
      "outputs": [
        {
          "path": "triage.length",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "doctors.utilization",
          "records": [
            {
              "type": "summary",
              "statistics": [
                "mean",
                "max"
              ]
            }
          ]
        },
        {
          "path": "fatigue.value",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "patients.inState.sick",
          "records": [
            {
              "type": "summary"
            }
          ]
        }
      ]
    }
  ],
  "views": [
    {
      "id": "docs-canvas",
      "label": "Documentation layout",
      "type": "canvas",
      "elements": [
        {
          "ref": "patients",
          "x": 0,
          "y": 40
        },
        {
          "ref": "triage",
          "x": 280,
          "y": 40
        },
        {
          "ref": "doctors",
          "x": 560,
          "y": 40
        },
        {
          "ref": "effectiveDoctors",
          "x": 780,
          "y": 210
        },
        {
          "ref": "tiring",
          "x": 280,
          "y": 350
        },
        {
          "ref": "fatigue",
          "x": 480,
          "y": 350
        },
        {
          "ref": "resting",
          "x": 680,
          "y": 350
        }
      ],
      "routes": [
        {
          "ref": "discharge",
          "vertices": [
            {
              "x": 350,
              "y": 260
            }
          ]
        }
      ]
    }
  ]
}

Watch the two charts against each other. The queue length is a step series that moves at event instants; fatigue is a smooth curve integrated between them. Their interaction, discrete load driving a continuous stock that throttles discrete capacity, is the point of the model.

Reference#

Parameters#

NameTypeDefaultDescription
nPatients integer 500 Population size served by the clinic.
sicknessRate number 0.005 Hazard of a well person falling sick. unit: 1/hour
treatmentTime number 0.75 Mean treatment duration per patient. unit: hours
baseDoctors integer 3 Doctors on shift when fully rested.
fatigueGain number 0.05 Fatigue accumulated per hour at full utilization. unit: 1/hour
fatigueRecovery number 0.1 Fractional fatigue recovery per hour. unit: 1/hour

Components#

IdTypeParams
patients
Patient population
abm.population {"size":{"param":"nPatients"},"agent":{"attributes":[{"name":"visits","init":0,"description":"Completed clini…
triage
Triage queue
des.queue {"discipline":"fifo"}
doctors
Doctors
des.server {"serviceTime":{"dist":"exponential","rate":{"expr":"1 / treatmentTime"}},"capacity":{"expr":"effectiveDoctor…
fatigue
Doctor fatigue
sd.stock {"initial":0,"min":0,"max":1}
tiring sd.flow {"rate":{"expr":"fatigueGain * doctors.utilization"}}
resting sd.flow {"rate":{"expr":"fatigueRecovery * fatigue.value"}}
effectiveDoctors
Effective doctors
sd.auxiliary {"value":{"expr":"max(1, round(baseDoctors * (1 - 0.5 * fatigue.value)))"}}

Connections#

  • patients.outtriage.in
  • triage.outdoctors.in
  • doctors.outpatients.in
  • tiring.outfatigue.in
  • fatigue.outresting.in

Experiments#

NameDurationReplicationsSweepOutputs
one-week 168 warmup 24 10 triage.length, doctors.utilization, fatigue.value, patients.inState.sick

Source#

spec/examples/clinic-hybrid.osdl.json · 7.9 KB
json
{
  "$schema": "https://osdl.dev/schemas/0.1/osdl.schema.json",
  "osdl": "0.1",
  "model": {
    "name": "clinic-hybrid",
    "label": "Urgent-Care Clinic (hybrid DES + ABM + SD)",
    "description": "All three paradigms in one model. Patients are agents (ABM) who fall sick stochastically and depart into the clinic's discrete-event process (DES): a triage queue feeding doctors. Doctor workload accumulates as fatigue (SD), which feeds back to reduce effective treatment capacity. Treated patients return to the population and eventually recover to a well state.",
    "time": {
      "unit": "hours",
      "integration": {
        "method": "euler",
        "dt": 0.1
      }
    },
    "parameters": [
      {
        "name": "nPatients",
        "description": "Population size served by the clinic.",
        "type": "integer",
        "default": 500
      },
      {
        "name": "sicknessRate",
        "description": "Hazard of a well person falling sick.",
        "default": 0.005,
        "unit": "1/hour"
      },
      {
        "name": "treatmentTime",
        "description": "Mean treatment duration per patient.",
        "default": 0.75,
        "unit": "hours"
      },
      {
        "name": "baseDoctors",
        "description": "Doctors on shift when fully rested.",
        "type": "integer",
        "default": 3
      },
      {
        "name": "fatigueGain",
        "description": "Fatigue accumulated per hour at full utilization.",
        "default": 0.05,
        "unit": "1/hour"
      },
      {
        "name": "fatigueRecovery",
        "description": "Fractional fatigue recovery per hour.",
        "default": 0.1,
        "unit": "1/hour"
      }
    ],
    "components": [
      {
        "id": "patients",
        "type": "abm.population",
        "label": "Patient population",
        "params": {
          "size": {
            "param": "nPatients"
          },
          "agent": {
            "attributes": [
              {
                "name": "visits",
                "init": 0,
                "description": "Completed clinic visits."
              }
            ],
            "behavior": {
              "initial": "well",
              "states": [
                {
                  "id": "well"
                },
                {
                  "id": "sick",
                  "description": "Departs to the clinic on entry; remains in this state while queued and treated.",
                  "onEnter": [
                    {
                      "type": "depart",
                      "port": "out"
                    }
                  ]
                },
                {
                  "id": "recovering"
                }
              ],
              "transitions": [
                {
                  "from": "well",
                  "to": "sick",
                  "trigger": {
                    "type": "rate",
                    "rate": {
                      "param": "sicknessRate"
                    }
                  }
                },
                {
                  "from": "sick",
                  "to": "recovering",
                  "trigger": {
                    "type": "arrive",
                    "port": "in"
                  },
                  "actions": [
                    {
                      "type": "set",
                      "target": "visits",
                      "value": {
                        "expr": "visits + 1"
                      }
                    }
                  ]
                },
                {
                  "from": "recovering",
                  "to": "well",
                  "trigger": {
                    "type": "timeout",
                    "delay": 48
                  }
                }
              ]
            }
          }
        }
      },
      {
        "id": "triage",
        "type": "des.queue",
        "label": "Triage queue",
        "params": {
          "discipline": "fifo"
        }
      },
      {
        "id": "doctors",
        "type": "des.server",
        "label": "Doctors",
        "params": {
          "serviceTime": {
            "dist": "exponential",
            "rate": {
              "expr": "1 / treatmentTime"
            }
          },
          "capacity": {
            "expr": "effectiveDoctors.value"
          }
        }
      },
      {
        "id": "fatigue",
        "type": "sd.stock",
        "label": "Doctor fatigue",
        "params": {
          "initial": 0,
          "min": 0,
          "max": 1
        }
      },
      {
        "id": "tiring",
        "type": "sd.flow",
        "params": {
          "rate": {
            "expr": "fatigueGain * doctors.utilization"
          }
        }
      },
      {
        "id": "resting",
        "type": "sd.flow",
        "params": {
          "rate": {
            "expr": "fatigueRecovery * fatigue.value"
          }
        }
      },
      {
        "id": "effectiveDoctors",
        "type": "sd.auxiliary",
        "label": "Effective doctors",
        "description": "Fatigue feedback: rested doctors work at full capacity; exhausted doctors at half. Never below one doctor.",
        "params": {
          "value": {
            "expr": "max(1, round(baseDoctors * (1 - 0.5 * fatigue.value)))"
          }
        }
      }
    ],
    "connections": [
      {
        "id": "toTriage",
        "from": "patients.out",
        "to": "triage.in"
      },
      {
        "id": "toDoctors",
        "from": "triage.out",
        "to": "doctors.in"
      },
      {
        "id": "discharge",
        "from": "doctors.out",
        "to": "patients.in"
      },
      {
        "id": "tiringIn",
        "from": "tiring.out",
        "to": "fatigue.in"
      },
      {
        "id": "restingOut",
        "from": "fatigue.out",
        "to": "resting.in"
      }
    ]
  },
  "experiments": [
    {
      "name": "one-week",
      "description": "A week of clinic operation after a day of warmup.",
      "duration": 168,
      "warmup": 24,
      "replications": 10,
      "seed": 42,
      "outputs": [
        {
          "path": "triage.length",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "doctors.utilization",
          "records": [
            {
              "type": "summary",
              "statistics": [
                "mean",
                "max"
              ]
            }
          ]
        },
        {
          "path": "fatigue.value",
          "records": [
            {
              "type": "timeseries",
              "interval": 1
            },
            {
              "type": "summary"
            }
          ]
        },
        {
          "path": "patients.inState.sick",
          "records": [
            {
              "type": "summary"
            }
          ]
        }
      ]
    }
  ],
  "views": [
    {
      "id": "main",
      "label": "Clinic overview",
      "type": "canvas",
      "elements": [
        {
          "ref": "patients",
          "x": 0,
          "y": 100
        },
        {
          "ref": "triage",
          "x": 260,
          "y": 100
        },
        {
          "ref": "doctors",
          "x": 500,
          "y": 100
        },
        {
          "ref": "tiring",
          "x": 380,
          "y": 300
        },
        {
          "ref": "fatigue",
          "x": 560,
          "y": 300
        },
        {
          "ref": "resting",
          "x": 740,
          "y": 300
        },
        {
          "ref": "effectiveDoctors",
          "x": 560,
          "y": 200
        }
      ],
      "routes": [
        {
          "ref": "discharge",
          "vertices": [
            {
              "x": 580,
              "y": 40
            },
            {
              "x": 80,
              "y": 40
            }
          ]
        }
      ]
    }
  ]
}