Structs and Spawnables

Core models often treat arrivals as counts. Agent-heavy models need each arrival to carry its own state: arrival time, priority, service choice, patience, or accumulated outcomes. Use sim.Struct for per-process fields and sim.Spawnable for processes created during a trial.

Per-process fields

A sim.Struct declares fields stored with a process. Fields may be int or float:

class Patient(sim.Struct):
    arrival: float
    acuity: int
    wait_started: float


class Clinic(sim.Model):
    patients: sim.Spawnable
    completed: sim.State


model = Clinic("clinic")


@model.process
def patients(env: Clinic, patient: Patient):
    patient.wait_started = sim.now()
    # The process can use its own fields throughout the visit.

The final annotated process parameter receives the current process’s struct view. Multi-copy static processes can also receive a copy index before the struct view.

Dynamic process creation

Declare a sim.Spawnable field with the same name as the process that should be created dynamically:

import cimba.random as random

@model.process
def arrivals(env: Clinic):
    while True:
        sim.hold(random.exponential(1.0 / env.arrival_rate))
        handle = sim.spawn(env.patients, env)
        patient = Patient(handle)
        patient.arrival = sim.now()
        patient.acuity = 1 if random.uniform() < 0.2 else 0

The spawned process begins only after the current process blocks. That gives the spawning process a clean initialization window: create the process, write its struct fields through the handle, and then let simulated time continue.

Joining and reclaiming

sim.wait_process(handle) waits for a spawned process to finish. sim.despawn(handle) reclaims a finished spawned process:

@model.process
def arrivals(env: Clinic):
    handle = sim.spawn(env.patients, env)
    Patient(handle).arrival = sim.now()
    sim.wait_process(handle)
    sim.despawn(handle)

Long-running models should reclaim finished dynamic processes when they are no longer needed. A common pattern is to put finished handles into a sim.Store and have a cleanup process despawn them:

class Clinic(sim.Model):
    patients: sim.Spawnable
    departures: sim.Store


@model.process
def cleanup(env: Clinic):
    while True:
        handle = env.departures.take()
        sim.despawn(handle)


@model.process
def patients(env: Clinic, patient: Patient):
    # ... patient journey ...
    env.departures.put(sim.current())

Leftover spawned processes are stopped and reclaimed at the end of a trial, but explicit cleanup keeps long trials from accumulating completed agents.

Component-owned spawnables

Components can own spawnable fields. This keeps dynamic agents close to the subsystem that creates them:

class Intake(sim.Component):
    patient: sim.Spawnable

    @sim.process
    def arrivals(self, env):
        handle = sim.spawn(self.patient, env)
        Patient(handle).arrival = sim.now()

    @sim.process
    def patient(self, env, p: Patient):
        sim.hold(random.exponential(env.mean_service))

Use this when the dynamic process is naturally part of a component. Use a model-level sim.Spawnable when the dynamic process crosses many domains.

For a larger worked example with dynamic agents and resources, see Tutorial: Modeling with Cimba Python.