-
Notifications
You must be signed in to change notification settings - Fork 5
Specifying dependencies between modules
A module[1] is able to specify that it depends on functionality or properties defined by other modules using one or more of the class attributes INIT_DEPENDENCIES
, OPTIONAL_INIT_DEPENDENCIES
and ADDITIONAL_DEPENDENCIES
. These declarations are used both to check if any required modules have been omitted when registering the simulation modules, and to allow automatically sorting the order in which the modules are initialised to ensure modules, which depend on an initialisation method of one or other modules being called first, appear after their dependencies in the call order.
All of INIT_DEPENDENCIES
, OPTIONAL_INIT_DEPENDENCIES
and ADDITIONAL_DEPENDENCIES
are assigned an empty set in the definition of Module
[2] , and so if a module does not define overriding
values for these class attributes, they will default to these empty set values. In all cases dependencies are specified using the strings containing the (unqualified) name of the module, for example "Demography"
for the tlo.method.demography.Demography
module, rather than the class objects themselves.
The INIT_DEPENDENCIES
set should be used to specify any dependencies of the module which must be initialised before it. By default the dependencies declared in INIT_DEPENDENCIES
will be used to sort the modules registered in the simulation using Simulation.register
[3] such that any modules named in the INIT_DEPENDENCIES
set of a module appear before it in the ordering[4] , with this ordering then used when iterating over and calling the modules' read_parameters
, pre_initialise_population
, initialise_population
and initialise_simulation
methods. The read_parameters
methods of all modules are called before any of the pre_initialise_population
methods are called, the pre_initialise_population
methods of all modules are called before any of the initialise_population
methods are called, and the initialise_population
methods of all modules are called before any of the initialise_simulation
methods are called. That is, if modules
is the sequence of modules passed to Simulation.register
(with sort_modules=True
) and simulation
is the corresponding Simulation
instance, then the following code is roughly equivalent to what happens when calling Simulation.make_initial_population
then Simulation.simulate
# Sort modules using INIT_DEPENDENCIES attributes
sorted_modules = topologically_sort_modules(modules)
# Read module parameters
for module in sorted_modules:
module.read_parameters(resource_directory)
# Pre-initialise population
for module in sorted_modules:
module.pre_initialise_population()
# Initialise population
for module in sorted_modules:
module.initialise_population(simulation.population)
# Initialise simulation
for module in sorted_modules:
module.initialise_simulation(simulation)
# Begin processing simulation event queue
...
A consequence of this is that the read_parameters
methods of all registered modules will be guaranteed to have been called before any modules pre_initialise_population
method is called, irrespective of the dependencies specified by the modules' INIT_DEPENDENCIES
attributes, and likewise the the pre_initialise_population
methods of all registered modules will be guaranteed to have been called before any module's initialise_population
method is called and the initialise_population
methods of all registered modules will be guaranteed to have been called before any module's initialise_simulation
method is called. This means that a module only needs to declare another in its INIT_DEPENDENCIES
set if one of its read_parameters
, pre_initialise_population
, initialise_population
or initialise_simulation
methods requires the same method in the other module to be called first. For example, if a module's initialise_simulation
method only requires a dependency's initialise_population
method to have been called before it, but not the dependency's initialise_simulation
method, then there is no need to add it to the INIT_DEPENDENCIES
set (though it should still be listed as a dependency in ADDITIONAL_DEPENDENCIES
).
Some concrete examples of modules that may be required to be added to a disease module's INIT_DEPENDENCIES
set are
-
Demography
: TheDemography
module initialises the values in theis_alive
,age_*
andsex
columns (amongst others) in the population dataframe in itsinitialise_population
method. Any module which makes use of the values in any of the columns in the population dataframe in their owninitialise_population
method will therefore need to include"Demography"
in itsINIT_DEPENDENCIES
set. -
Lifestyle
: Similarly if a module'sinitialise_population
method makes use of any of the values in the population columns defined by properties of theLifestyle
module (for exampleli_urban
orli_wealth
), then it will need to include"Lifestyle"
in itsINIT_DEPENDENCIES
set. -
Contraception
: TheContraception
module initialises the value of theis_pregnant
column (amongst others) in the population dataframe in itsinitialise_population
method. Any module which differentiates by pregnancy status the values it it initialises its associated properties with ininitialise_population
will therefore need to include"Contraception"
in itsINIT_DEPENDENCIES
set. -
SymptomManager
: If a module usesSymptomManager.change_symptom
to update the initial values of a symptom column in itsinitialise_population
method, then it will need to include"SymptomManager"
in itsINIT_DEPENDENCIES
set (asSymptomManager.change_symptom
assumesSymptomManager.initialise_population
has been called before it).
If a module schedule events (standard or HSI) in its initialise_simulation
method which use properties or method defined by other modules in their apply
method, but do not require accessing the associated columns in the population dataframe or module attributes within their __init__
constructor, then there is no need for these other modules to be included as INIT_DEPENDENCIES
(as the event's apply method will only be run after all of the modules' initialisation methods have been called). The modules should however be included in the ADDITIONAL_DEPENDENCIES
set however.
The OPTIONAL_INIT_DEPENDENCIES
set should be used to specify any optional dependencies of the module which if present must be initialised before it. Unlike dependencies specified in INIT_DEPENDENCIES
, a dependency which is included in OPTIONAL_INIT_DEPENDENCIES
but not registered in the simulation will not result in a ModuleDependencyError
exception raised. The main example of a module which may need to be specified in the OPTIONAL_INIT_DEPENDENCIES
for a disease module is HealthBurden
; most disease modules use a pattern similar to
if "HealthBurden" in self.sim.modules:
self.daly_weights = {
condition: self.sim.modules["HealthBurden"].get_daly_weight(sequlae_code)
for condition, sequlae_code in sequlae_codes_dict.items()
}
in the read_parameters
method to query and locally store the DALY weights associated with conditions relevant to the module. By wrapping the calls to HealthBurden.get_daly_weight
in an if
statement which checks if the HealthBurden
module has been registered in the simulation first, the module will not require HealthBurden
to be present. However, if HealthBurden
is present, it must have its own read_parameters
method called before any disease module calls its get_daly_weight
method as this method makes use of the DALY weight database read from an external file in the HealthBurden.read_parameters
method. Therefore, "HealthBurden"
should be included in the OPTIONAL_INIT_DEPENDENCIES
set for the disease module.
The ADDITIONAL_DEPENDENCIES
set should be used to declare any other modules that this module requires to be registered in the simulation, for example due to using properties defined by these modules or accessing attributes or methods of these modules directly, but that do not need to be initialised before this module. Modules should only appear in one of the INIT_DEPENDENCIES
, OPTIONAL_INIT_DEPENDENCIES
and ADDITIONAL_DEPENDENCIES
sets (with there being tests in tests/test_module_dependencies.py
to check for this).
In addition to the *_DEPENDENCIES
class attributes described above, there is a related but functionally distinct class attribute ALTERNATIVE_TO
, which may need to be changed from its default value of an empty set by modules which act as 'proxies' or alternatives for other modules. The main use cases for this are simplified versions of modules used to reduce the computational demands of simulations (for example SimplifiedBirths
can be used as an alternative to registering the birth related modules) or as 'dummy' replacements for testing purposes (for example DummyHivModule
). If a module includes one or more other modules in its ALTERNATIVE_TO
class attribute set, then this module will be considered valid to be used in place of these ALTERNATIVE_TO
modules when checking that all required dependencies are registered.
1: We will use module here in the TLO specific sense of a subclass of tlo.core.Module
as opposed to the more typical meaning in Python. ↩
2: Specifically an empty frozenset
is used which due to its immutability ensures any dependencies added by subclasses do not get shared between all subclasses. ↩
3: If the keyword argument sort_modules
to Simulation.register
is set to False
rather than its default value of True
the modules will instead be iterated over in the order they are passed to Simulation.register
. ↩
4: A topological sorting algorithm is used to compute this ordering, with the requirement that there are no circular dependencies in the dependency graph defined by the INIT_DEPENDENCIES
sets. This precludes for example modules listing each other as INIT_DEPENDENCIES
. ↩
TLO Model Wiki