LASER architecture overview
Architecture of laser-core and laser-generic packages
1. Purpose
This document describes the structural architecture and core responsibilities of:
laser-core— low-level data and memory structures used to build high-performance disease models.laser-generic— a library of modular, composable components implementing generic disease transmission models such as SI, SIR, SIRS, SEIR, and SEIRS.
This document intentionally excludes details of:
- Statistical distributions
- Spatial connectivity kernels
- Demographic initialization utilities
Those are documented separately.
2. High-level system structure
At a high level:
laser-coreprovides the data substrate (memory, properties, layout).laser-genericprovides the simulation model, components, and execution loop.
flowchart LR
subgraph Core[laser-core]
LF[LaserFrame]
Utils["Low-level utilities (distributions, spatial, demographics)"]
end
subgraph Generic[laser-generic]
Model[Model]
Comp["Components (Transmission, Progression, etc.)"]
Kernels[Numba Kernels]
end
LF --> Model
Model --> Comp
Comp --> Kernels
The architecture is designed so that:
laser-coreknows nothing about disease modeling, andlaser-genericrelies heavily on the memory layout & speed provided bylaser-core.
3. laser-core architecture
3.1 Responsibilities
laser-core is responsible for:
- Managing efficient, columnar memory layouts for agent/node properties.
- Providing a schema-like interface for defining scalar and vector properties per entity.
- Enabling parallel execution by keeping values contiguous and Numba-friendly.
- Supporting dynamic population changes (primarily births and, optionally, resizing).
3.2 LaserFrame
LaserFrame behaves conceptually like a high-performance DataFrame designed for:
- Constant mutation
- Parallel processing
- Zero Python-object overhead
Core concepts
- Each row = one agent or node.
- Each column = a property.
-
Properties may be:
-
Scalars (e.g., age, state)
- Fixed-length vectors (e.g., per-tick recording of S/E/I/R counts for nodes)
Key responsibilities
- Allocate and manage contiguous memory for each property.
- Provide direct access to underlying NumPy arrays.
- Ensure layout is friendly to Numba kernels and multicore CPUs.
- Resize automatically when agents are added.
Class diagram
classDiagram
class LaserFrame {
+int count
+int capacity
+dict properties
+add_scalar_property(name, dtype, default)
+add_vector_property(name, length, dtype, default)
+add_array_property(name, shape, dtype, default)
+add(count)
+squash(indices)
}
4. laser-generic architecture
4.1 Responsibilities
laser-generic provides:
- A Model container orchestrating simulation flow.
- A library of components representing distinct parts of a disease process (e.g., transmission, incubation, recovery).
- A component execution pipeline ([optional pre-step validation] → step → [optional post-step validation]).
- Integration with Numba kernels for high-performance agent updates.
4.2 The Model class
The Model object is the conductor of the simulation orchestra:
- Holds one or more LaserFrames.
- Holds a list of components.
- Maintains simulation time (
tick). - Defines the run loop, calling component hooks in sequence.
Simulation loop (conceptual)
1 2 3 4 5 6 7 | |
This consistent execution order ensures deterministic progression, assuming your random number draws are deterministic as well. The laser.generic.random module provides PRNG seeding across NumPy, Numba, and SciPy to help with deterministic random number draws. Note, though, that while Numba generally does provide repeatable random number sequences, it does not guarantee this behavior.
4.3 Component architecture
Components represent modular behaviors. Examples:
- Transmission
- Incubation
- Infectious progression
- Recovery
- Births / deaths
- Vaccinations
Component responsibilities
- Own and manage their own model-specific properties.
- Implement a
step(self, tick)method containing the core dynamics. -
Optionally implement:
-
pre_step(self, tick) post_step(self, tick)on_birth(self, tick, newborn_indices)plot(self)
Component interface
1 2 3 4 5 6 | |
Kernel delegation
The step() method typically:
- Extracts needed properties from the
LaserFrame. - Passes them to a Numba kernel.
- Lets the kernel perform parallel updates.
5. Execution flow diagram
sequenceDiagram
participant Model
participant Component as Component[i]
participant Kernel as NumbaKernel
loop timestep t
loop components
Model->>Component: pre_step(model)
end
loop components
Model->>Component: step(model)
Component->>Kernel: call kernel(arrays...)
Kernel-->>Component: updates arrays
end
loop components
Model->>Component: post_step(model)
end
Model->>Model: t += dt
end
6. SEIR model: detailed class diagram
Below is a detailed wiring diagram for a generic SEIR model using laser-generic components. This example uses:
SusceptiblecomponentExposedcomponent (E → I)Infectiouscomponent (I → R)Recoveredcomponent (terminal state or may loop back via waning in SIRS or SEIRS)TransmissioncomponentBirthscomponent (optional)Mortalitycomponent (optional)
(You may have bells, whistles, or mutations, but this is the canonical SEIR template.)
Component wiring diagram
classDiagram
class Model {
+LaserFrame people
+LaserFrame nodes
+list~Component~ components
+int tick
+int nticks
+run()
}
class Susceptible {
+\_\_init__()
+step()
}
class Exposed {
+\_\_init__()
+step()
}
class Infectious {
+\_\_init__()
+step()
}
class Recovered {
+\_\_init__()
+step()
}
class Transmission {
+pre_step()
+step()
+post_step()
}
class Births {
+\_\_init__()
+step()
}
class Mortality {
+\_\_init__()
+step()
}
class LaserFrame {
+add_scalar_property()
+add_vector_property()
}
%% Relationships
Model --> LaserFrame : uses
Model "1" o-- "1" Susceptible : has
Model "1" o-- "1" Exposed : has
Model "1" o-- "1" Infectious : has
Model "1" o-- "1" Recovered : has
Model "1" o-- "1" Transmission : has
Model "1" o-- "1" Births : has
Model "1" o-- "1" Mortality : has
Exposed --> LaserFrame : reads/writes
Infectious --> LaserFrame : reads/writes
Recovered --> LaserFrame : reads/writes
Transmission --> LaserFrame : reads/writes
Births --> LaserFrame : writes
Mortality --> LaserFrame : reads/writes
Property flow (narrative)
-
Susceptible
-
Adds:
nodeidandstateproperties tomodel.people,Sproperty tomodel.nodes - Reads: none
-
Writes: none
-
Exposed
-
Adds:
etimerproperty tomodel.people,Eandnewly_infectiousproperties tomodel.nodes - Reads:
state,etimer -
Writes:
state(E → I) -
Infectious
-
Adds:
itimerproperty tomodel.people,Iandnewly_recoveredproperties tomodel.nodes - Reads:
state,itimer -
Writes:
state(I → R) -
Recovered
-
Adds:
Rproperty tomodel.nodes - Reads:
state -
Writes: nothing unless waning immunity exists (in SIRS or SEIRS)
-
Transmission
-
Adds:
forcesandnewly_infectedproperties tomodel.nodes - Reads:
state -
Writes:
state(S → E), sets incubation durations -
Births
-
Adds:
birthstomodel.nodesand optionallydobtomodel.people - Reads: nothing
-
Writes: optionally
dob, adds agents, triggerson_birth()on relevant components -
Mortality
-
Adds:
deathstomodel.nodes - Reads: possibly
dob(date of birth), survival-related properties - Writes:
dod(date of death),state
7. Extensibility structure
The architecture supports extension in three directions:
7.1 Adding new properties
Add new per-agent or per-node attributes by modifying the LaserFrame schema.
7.2 Adding new components
Implement a new class conforming to the Component interface.
7.3 Creating custom models
Compose components into a Model in the correct execution order.
8. Summary
This architectural split provides:
- Modularity — Components rely only on
LaserFrameand model lifecycle hooks. - Performance — Data is laid out explicitly for multicore, Numba-driven kernels.
- Flexibility — New models and components can be composed without touching core infrastructure.
- Stability —
laser-coreprovides a durable, minimal foundation;laser-genericbuilds everything else on top.