Skip to content

Specifying dependencies between modules

Matt Graham edited this page Jan 26, 2022 · 4 revisions

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.

INIT_DEPENDENCIES

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: The Demography module initialises the values in the is_alive, age_* and sex columns (amongst others) in the population dataframe in its initialise_population method. Any module which makes use of the values in any of the columns in the population dataframe in their own initialise_population method will therefore need to include "Demography" in its INIT_DEPENDENCIES set.
  • Lifestyle: Similarly if a module's initialise_population method makes use of any of the values in the population columns defined by properties of the Lifestyle module (for example li_urban or li_wealth), then it will need to include "Lifestyle" in its INIT_DEPENDENCIES set.
  • Contraception: The Contraception module initialises the value of the is_pregnant column (amongst others) in the population dataframe in its initialise_population method. Any module which differentiates by pregnancy status the values it it initialises its associated properties with in initialise_population will therefore need to include "Contraception" in its INIT_DEPENDENCIES set.
  • SymptomManager: If a module uses SymptomManager.change_symptom to update the initial values of a symptom column in its initialise_population method, then it will need to include "SymptomManager" in its INIT_DEPENDENCIES set (as SymptomManager.change_symptom assumes SymptomManager.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.

OPTIONAL_INIT_DEPENDENCIES

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.

ADDITIONAL_DEPENDENCIES

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).

ALTERNATIVE_TO

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.