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 spawnable=True 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):
completed: sim.State
@sim.process(spawnable=True)
def patients(self: "Clinic", patient: Patient):
patient.wait_started = sim.now()
# The process can use its own fields throughout the visit.
model = Clinic("clinic")
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
The patients process above is decorated with spawnable=True. Spawn it
from a regular process:
import cimba.random as random
class Clinic(sim.Model):
arrival_rate: sim.Param
@sim.process(spawnable=True)
def patients(self: "Clinic", patient: Patient):
patient.wait_started = sim.now()
@sim.process
def arrivals(self: "Clinic"):
while True:
sim.hold(random.exponential(1.0 / self.arrival_rate))
handle = sim.spawn(self.patients, self)
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:
class Clinic(sim.Model):
@sim.process(spawnable=True)
def patients(self: "Clinic", patient: Patient):
sim.hold(1.0)
@sim.process
def arrivals(self: "Clinic"):
handle = sim.spawn(self.patients, self)
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):
departures: sim.Store
@sim.process
def cleanup(self: "Clinic"):
while True:
handle = self.departures.take()
sim.despawn(handle)
@sim.process(spawnable=True)
def patients(self: "Clinic", patient: Patient):
# ... patient journey ...
self.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 processes. This keeps dynamic agents close to the subsystem that creates them:
class Intake(sim.Component):
@sim.process
def arrivals(self, env):
handle = sim.spawn(self.patient, env)
Patient(handle).arrival = sim.now()
@sim.process(spawnable=True)
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.process(spawnable=True) when it crosses many domains.
For a larger worked example with dynamic agents and resources, see Tutorial: Modeling with Cimba Python.