MISCore structure¶
This section presents an overview of the main elements of MISCore - and their relationships - which are relevant
for modellers when modifying or creating a Process.
The flowchart at the bottom of this page represents a simplified version of the inner workings
of MISCore and can be used as a reference to complement the explanations in the tutorials in
the Modellers section.
As described in the Run a model tutorial, a simulation model is
represented by a Model. It consists of at least one
Universe, which in turn are comprised of at least one
Process.
The parameters of each Process are process-specific and are the same for all simulated individuals.
For example, these parameters characterize the probability distributions from which random values are drawn, or the screening strategy.
At the beginning of a simulation, the properties() method of each Process is executed.
These methods generate all the random numbers that define the simulated individuals.
For example, their year of birth, risk of disease, or systematic lack of sensitivity.
In order to make the computation more efficient and less memory intensive, the simulation is divided
into several blocks of individuals.
The blocks have 2000 individuals by default, but this is modifiable with the block_size argument
in the run() method.
When the simulation uses more than one core (see Multiprocessing),
the blocks are distributed over multiple cores of the computer.
This parallelization is not shown in the diagram but the logic remains the same.
An Individual object is generated for each multiprocessing.Process.
This object is the heart of the simulation: it contains the event queue, the properties, the logged events,
the memory in which we store an individual’s details about their life, and other relevant information.
Then, the temporary properties are generated for the individuals in the current block by calling the properties_tmp() method of each Process.
These temporary properties are discarded after the simulation of the block of individuals is done.
This is memory-efficient, as we do not have to store these properties for millions of individuals at the same time.
However, it is therefore not possible to retrieve these properties after the simulation is done (see also
Random numbers and properties).
After the block has been initialised, we can now start simulating the individuals one-by-one.
For each individual in a block, we simulate each Universe and then we move on to the next individual.
At the start of a Universe, each Process is initialised
at an individual level by allowing it to respond to the __start__ message.
During the initialisation, some first events are added to the event queue.
For example, an oc_death event is always created at the beginning of each
Universe by the the OC process.
After that, the model starts running down that event queue.
It picks the earliest event in the queue and performs that event.
It does so by checking the callbacks of each Process to see what functions
belong to that event and it runs these functions, which may change an individual’s state, log an event, change information in an individual’s
memory, and/or add new events to the queue. In other words, the events in an individual’s lifetime are iteratively updated by
Process functions (see the starred inner loop at the center of the diagram, ✮).
The possible events that an individual will encounter are not all determined at the beginning of a
simulation but are mostly generated during the simulation as a response to other events.
Note that the simulation doesn’t proceed in fixed time steps but instead always “jumps” to the earliest
event scheduled in the queue, potentially adding new events.
It continues to do so until a death event is reached, leaving all future events in the queue untriggered.
At that point, the simulation starts all callback functions that are related to the __end__ message.
Note
Note that events popped from the queue are not logged to the events DataFrame.
Call the log_event() from within a callback function instead.
Once the simulation of the individual in a specific Universe is done, the above is repeated for the
same individual in the next Universe.
It is important to note that all the information stored in an individual’s memory is emptied after
completion of each Universe (see last purple box before moving to the next Universe
in the diagram).
This means that no information is conveyed from one Universe to another (except, optionally, the stratum:
Using stratum).
Once the individual has been simulated in every Universe, we move on to the next individual.
Once all individuals in the block have been simulated, the Model discards all the generated properties_tmp
and move on to the next block of individuals (see first blue box after
completion of the last individual in the diagram).
Finally, when all blocks are done, the simulation is completed.
Then the logged events, durations and snapshots are gathered and added to a Result object.
This object is the output of the simulation.