vivarium-public-health 2.1.4__py3-none-any.whl → 2.2.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- vivarium_public_health/_version.py +1 -1
- vivarium_public_health/plugins/__init__.py +1 -0
- vivarium_public_health/plugins/parser.py +864 -0
- vivarium_public_health/population/data_transformations.py +10 -9
- {vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/METADATA +4 -1
- {vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/RECORD +9 -7
- {vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/LICENSE.txt +0 -0
- {vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/WHEEL +0 -0
- {vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
|
|
1
|
-
__version__ = "2.
|
1
|
+
__version__ = "2.2.0"
|
@@ -0,0 +1 @@
|
|
1
|
+
from vivarium_public_health.plugins.parser import CausesConfigurationParser
|
@@ -0,0 +1,864 @@
|
|
1
|
+
"""
|
2
|
+
===============================
|
3
|
+
Component Configuration Parsers
|
4
|
+
===============================
|
5
|
+
|
6
|
+
Component Configuration Parsers in this module are specialized implementations of
|
7
|
+
:class:`ComponentConfigurationParser <vivarium.framework.components.parser.ComponentConfigurationParser>`
|
8
|
+
that can parse configurations of components specific to the Vivarium Public
|
9
|
+
Health package.
|
10
|
+
"""
|
11
|
+
from importlib import import_module
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
13
|
+
|
14
|
+
import pandas as pd
|
15
|
+
from pkg_resources import resource_filename
|
16
|
+
from vivarium import Component, ConfigTree
|
17
|
+
from vivarium.framework.components import ComponentConfigurationParser
|
18
|
+
from vivarium.framework.components.parser import ParsingError
|
19
|
+
from vivarium.framework.engine import Builder
|
20
|
+
from vivarium.framework.state_machine import Trigger
|
21
|
+
from vivarium.framework.utilities import import_by_path
|
22
|
+
|
23
|
+
from vivarium_public_health.disease import (
|
24
|
+
BaseDiseaseState,
|
25
|
+
DiseaseModel,
|
26
|
+
DiseaseState,
|
27
|
+
RecoveredState,
|
28
|
+
SusceptibleState,
|
29
|
+
TransientDiseaseState,
|
30
|
+
)
|
31
|
+
from vivarium_public_health.utilities import TargetString
|
32
|
+
|
33
|
+
|
34
|
+
class CausesParsingErrors(ParsingError):
|
35
|
+
"""
|
36
|
+
Error raised when there are any errors parsing a cause model configuration.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, messages: List[str]):
|
40
|
+
super().__init__("\n - " + "\n - ".join(messages))
|
41
|
+
|
42
|
+
|
43
|
+
class CausesConfigurationParser(ComponentConfigurationParser):
|
44
|
+
"""
|
45
|
+
Component configuration parser that acts the same as the standard vivarium
|
46
|
+
`ComponentConfigurationParser` but adds the additional ability to parse a
|
47
|
+
configuration to create `DiseaseModel` components. These DiseaseModel
|
48
|
+
configurations can either be specified directly in the configuration in a
|
49
|
+
`causes` key or in external configuration files that are specified in the
|
50
|
+
`external_configuration` key.
|
51
|
+
"""
|
52
|
+
|
53
|
+
DEFAULT_MODEL_CONFIG = {
|
54
|
+
"model_type": f"{DiseaseModel.__module__}.{DiseaseModel.__name__}",
|
55
|
+
"initial_state": None,
|
56
|
+
}
|
57
|
+
"""
|
58
|
+
If a cause model configuration does not specify a model type or initial
|
59
|
+
state, these default values will be used. The default model type is
|
60
|
+
`DiseaseModel` and the
|
61
|
+
default initial state is `None`. If the initial state is not specified,
|
62
|
+
the cause model must have a state named 'susceptible'.
|
63
|
+
"""
|
64
|
+
|
65
|
+
DEFAULT_STATE_CONFIG = {
|
66
|
+
"cause_type": "cause",
|
67
|
+
"transient": False,
|
68
|
+
"allow_self_transition": True,
|
69
|
+
"side_effect": None,
|
70
|
+
"cleanup_function": None,
|
71
|
+
"state_type": None,
|
72
|
+
}
|
73
|
+
"""
|
74
|
+
If a state configuration does not specify cause_type, transient,
|
75
|
+
allow_self_transition, side_effect, cleanup_function, or state_type,
|
76
|
+
these default values will be used. The default cause type is 'cause', the
|
77
|
+
default transient value is False, and the default allow_self_transition
|
78
|
+
value is True.
|
79
|
+
"""
|
80
|
+
|
81
|
+
DEFAULT_TRANSITION_CONFIG = {"triggered": "NOT_TRIGGERED"}
|
82
|
+
"""
|
83
|
+
If a transition configuration does not specify a triggered value, this
|
84
|
+
default value will be used. The default triggered value is 'NOT_TRIGGERED'.
|
85
|
+
"""
|
86
|
+
|
87
|
+
def parse_component_config(self, component_config: ConfigTree) -> List[Component]:
|
88
|
+
"""
|
89
|
+
Parses the component configuration and returns a list of components.
|
90
|
+
|
91
|
+
In particular, this method looks for an `external_configuration` key
|
92
|
+
and/or a `causes` key.
|
93
|
+
|
94
|
+
The `external_configuration` key should have names of packages that
|
95
|
+
contain cause model configuration files. Within that key should be a list
|
96
|
+
of paths to cause model configuration files relative to the package.
|
97
|
+
|
98
|
+
.. code-block:: yaml
|
99
|
+
|
100
|
+
external_configuration:
|
101
|
+
some_package:
|
102
|
+
- some/path/cause_model_1.yaml
|
103
|
+
- some/path/cause_model_2.yaml
|
104
|
+
|
105
|
+
The `causes` key should contain configuration information for cause
|
106
|
+
models.
|
107
|
+
|
108
|
+
.. code-block:: yaml
|
109
|
+
|
110
|
+
causes:
|
111
|
+
cause_1:
|
112
|
+
model_type: vivarium_public_health.disease.DiseaseModel
|
113
|
+
initial_state: susceptible
|
114
|
+
states:
|
115
|
+
susceptible:
|
116
|
+
cause_type: cause
|
117
|
+
data_sources: {}
|
118
|
+
infected:
|
119
|
+
cause_type: cause
|
120
|
+
transient: false
|
121
|
+
allow_self_transition: true
|
122
|
+
data_sources: {}
|
123
|
+
transitions:
|
124
|
+
transition_1:
|
125
|
+
source: susceptible
|
126
|
+
sink: infected
|
127
|
+
transition_type: rate
|
128
|
+
data_sources: {}
|
129
|
+
|
130
|
+
# todo add information about the data_sources configuration
|
131
|
+
|
132
|
+
Note that this method modifies the simulation's component configuration
|
133
|
+
by adding the contents of external configuration files to the
|
134
|
+
`model_override` layer and adding default cause model configuration
|
135
|
+
values for all cause models to the `component_config` layer.
|
136
|
+
|
137
|
+
Parameters
|
138
|
+
----------
|
139
|
+
component_config
|
140
|
+
A ConfigTree defining the components to initialize.
|
141
|
+
|
142
|
+
Returns
|
143
|
+
-------
|
144
|
+
List
|
145
|
+
A list of initialized components.
|
146
|
+
|
147
|
+
Raises
|
148
|
+
------
|
149
|
+
CausesParsingErrors
|
150
|
+
If the cause model configuration is invalid
|
151
|
+
"""
|
152
|
+
components = []
|
153
|
+
|
154
|
+
if "external_configuration" in component_config:
|
155
|
+
self._validate_external_configuration(component_config["external_configuration"])
|
156
|
+
for package, config_files in component_config["external_configuration"].items():
|
157
|
+
for config_file in config_files.get_value():
|
158
|
+
source = f"{package}::{config_file}"
|
159
|
+
config_file = resource_filename(package, config_file)
|
160
|
+
|
161
|
+
external_config = ConfigTree(config_file)
|
162
|
+
component_config.update(
|
163
|
+
external_config, layer="model_override", source=source
|
164
|
+
)
|
165
|
+
|
166
|
+
if "causes" in component_config:
|
167
|
+
causes_config = component_config["causes"]
|
168
|
+
self._validate_causes_config(causes_config)
|
169
|
+
self._add_default_config_layer(causes_config)
|
170
|
+
components += self._get_cause_model_components(causes_config)
|
171
|
+
|
172
|
+
# Parse standard components (i.e. not cause models)
|
173
|
+
standard_component_config = component_config.to_dict()
|
174
|
+
standard_component_config.pop("external_configuration", None)
|
175
|
+
standard_component_config.pop("causes", None)
|
176
|
+
standard_components = (
|
177
|
+
self.process_level(standard_component_config, [])
|
178
|
+
if standard_component_config
|
179
|
+
else []
|
180
|
+
)
|
181
|
+
|
182
|
+
return components + standard_components
|
183
|
+
|
184
|
+
#########################
|
185
|
+
# Configuration methods #
|
186
|
+
#########################
|
187
|
+
|
188
|
+
def _add_default_config_layer(self, causes_config: ConfigTree) -> None:
|
189
|
+
"""
|
190
|
+
Adds a default layer to the provided configuration that specifies
|
191
|
+
default values for the cause model configuration.
|
192
|
+
|
193
|
+
Parameters
|
194
|
+
----------
|
195
|
+
causes_config
|
196
|
+
A ConfigTree defining the cause model configurations
|
197
|
+
|
198
|
+
Returns
|
199
|
+
-------
|
200
|
+
None
|
201
|
+
"""
|
202
|
+
default_config = {}
|
203
|
+
for cause_name, cause_config in causes_config.items():
|
204
|
+
default_states_config = {}
|
205
|
+
default_transitions_config = {}
|
206
|
+
default_config[cause_name] = {
|
207
|
+
**self.DEFAULT_MODEL_CONFIG,
|
208
|
+
"states": default_states_config,
|
209
|
+
"transitions": default_transitions_config,
|
210
|
+
}
|
211
|
+
|
212
|
+
for state_name, state_config in cause_config.states.items():
|
213
|
+
default_states_config[state_name] = self.DEFAULT_STATE_CONFIG
|
214
|
+
|
215
|
+
for transition_name, transition_config in cause_config.transitions.items():
|
216
|
+
default_transitions_config[transition_name] = self.DEFAULT_TRANSITION_CONFIG
|
217
|
+
|
218
|
+
causes_config.update(
|
219
|
+
default_config, layer="component_configs", source="causes_configuration_parser"
|
220
|
+
)
|
221
|
+
|
222
|
+
################################
|
223
|
+
# Cause model creation methods #
|
224
|
+
################################
|
225
|
+
|
226
|
+
def _get_cause_model_components(self, causes_config: ConfigTree) -> List[Component]:
|
227
|
+
"""
|
228
|
+
Parses the cause model configuration and returns a list of
|
229
|
+
`DiseaseModel` components.
|
230
|
+
|
231
|
+
Parameters
|
232
|
+
----------
|
233
|
+
causes_config
|
234
|
+
A ConfigTree defining the cause model components to initialize
|
235
|
+
|
236
|
+
Returns
|
237
|
+
-------
|
238
|
+
List[Component]
|
239
|
+
A list of initialized `DiseaseModel` components
|
240
|
+
"""
|
241
|
+
cause_models = []
|
242
|
+
|
243
|
+
for cause_name, cause_config in causes_config.items():
|
244
|
+
states: Dict[str, BaseDiseaseState] = {
|
245
|
+
state_name: self._get_state(state_name, state_config, cause_name)
|
246
|
+
for state_name, state_config in cause_config.states.items()
|
247
|
+
}
|
248
|
+
|
249
|
+
for transition_config in cause_config.transitions.values():
|
250
|
+
self._add_transition(
|
251
|
+
states[transition_config.source],
|
252
|
+
states[transition_config.sink],
|
253
|
+
transition_config,
|
254
|
+
)
|
255
|
+
|
256
|
+
model_type = import_by_path(cause_config.model_type)
|
257
|
+
initial_state = states.get(cause_config.initial_state, None)
|
258
|
+
model = model_type(
|
259
|
+
cause_name, initial_state=initial_state, states=list(states.values())
|
260
|
+
)
|
261
|
+
cause_models.append(model)
|
262
|
+
|
263
|
+
return cause_models
|
264
|
+
|
265
|
+
def _get_state(
|
266
|
+
self, state_name: str, state_config: ConfigTree, cause_name: str
|
267
|
+
) -> BaseDiseaseState:
|
268
|
+
"""
|
269
|
+
Parses a state configuration and returns an initialized `BaseDiseaseState`
|
270
|
+
object.
|
271
|
+
|
272
|
+
Parameters
|
273
|
+
----------
|
274
|
+
state_name
|
275
|
+
The name of the state to initialize
|
276
|
+
state_config
|
277
|
+
A ConfigTree defining the state to initialize
|
278
|
+
cause_name
|
279
|
+
The name of the cause to which the state belongs
|
280
|
+
|
281
|
+
Returns
|
282
|
+
-------
|
283
|
+
BaseDiseaseState
|
284
|
+
An initialized `BaseDiseaseState` object
|
285
|
+
"""
|
286
|
+
state_id = cause_name if state_name in ["susceptible", "recovered"] else state_name
|
287
|
+
state_kwargs = {
|
288
|
+
"cause_type": state_config.cause_type,
|
289
|
+
"allow_self_transition": state_config.allow_self_transition,
|
290
|
+
}
|
291
|
+
if state_config.side_effect:
|
292
|
+
# todo handle side effects properly
|
293
|
+
state_kwargs["side_effect"] = lambda *x: x
|
294
|
+
if state_config.cleanup_function:
|
295
|
+
# todo handle cleanup functions properly
|
296
|
+
state_kwargs["cleanup_function"] = lambda *x: x
|
297
|
+
if "data_sources" in state_config:
|
298
|
+
data_sources_config = state_config.data_sources
|
299
|
+
state_kwargs["get_data_functions"] = {
|
300
|
+
name: self._get_data_source(name, data_sources_config[name])
|
301
|
+
for name in data_sources_config.keys()
|
302
|
+
}
|
303
|
+
|
304
|
+
if state_config.state_type is not None:
|
305
|
+
state_type = import_by_path(state_config.state_type)
|
306
|
+
elif state_config.transient:
|
307
|
+
state_type = TransientDiseaseState
|
308
|
+
elif state_name == "susceptible":
|
309
|
+
state_type = SusceptibleState
|
310
|
+
elif state_name == "recovered":
|
311
|
+
state_type = RecoveredState
|
312
|
+
else:
|
313
|
+
state_type = DiseaseState
|
314
|
+
|
315
|
+
state = state_type(state_id, **state_kwargs)
|
316
|
+
return state
|
317
|
+
|
318
|
+
def _add_transition(
|
319
|
+
self,
|
320
|
+
source_state: BaseDiseaseState,
|
321
|
+
sink_state: BaseDiseaseState,
|
322
|
+
transition_config: ConfigTree,
|
323
|
+
) -> None:
|
324
|
+
"""
|
325
|
+
Adds a transition between two states.
|
326
|
+
|
327
|
+
Parameters
|
328
|
+
----------
|
329
|
+
source_state
|
330
|
+
The state the transition starts from
|
331
|
+
sink_state
|
332
|
+
The state the transition ends at
|
333
|
+
transition_config
|
334
|
+
A `ConfigTree` defining the transition to add
|
335
|
+
|
336
|
+
Returns
|
337
|
+
-------
|
338
|
+
None
|
339
|
+
"""
|
340
|
+
triggered = Trigger[transition_config.triggered]
|
341
|
+
if "data_sources" in transition_config:
|
342
|
+
data_sources_config = transition_config.data_sources
|
343
|
+
data_sources = {
|
344
|
+
name: self._get_data_source(name, data_sources_config[name])
|
345
|
+
for name in data_sources_config.keys()
|
346
|
+
}
|
347
|
+
else:
|
348
|
+
data_sources = None
|
349
|
+
|
350
|
+
if transition_config["transition_type"] == "rate":
|
351
|
+
source_state.add_rate_transition(
|
352
|
+
sink_state, get_data_functions=data_sources, triggered=triggered
|
353
|
+
)
|
354
|
+
elif transition_config["transition_type"] == "proportion":
|
355
|
+
source_state.add_proportion_transition(
|
356
|
+
sink_state, get_data_functions=data_sources, triggered=triggered
|
357
|
+
)
|
358
|
+
elif transition_config["transition_type"] == "dwell_time":
|
359
|
+
source_state.add_dwell_time_transition(sink_state, triggered=triggered)
|
360
|
+
else:
|
361
|
+
raise ValueError(
|
362
|
+
f"Invalid transition data type '{transition_config.type}'"
|
363
|
+
f" provided for transition '{transition_config}'."
|
364
|
+
)
|
365
|
+
|
366
|
+
@staticmethod
|
367
|
+
def _get_data_source(
|
368
|
+
name: str, source: Union[str, float]
|
369
|
+
) -> Callable[[Builder, Any], Any]:
|
370
|
+
"""
|
371
|
+
Parses a data source and returns a callable that can be used to retrieve
|
372
|
+
the data.
|
373
|
+
|
374
|
+
Parameters
|
375
|
+
----------
|
376
|
+
name
|
377
|
+
The name of the data getter
|
378
|
+
source
|
379
|
+
The data source to parse
|
380
|
+
|
381
|
+
Returns
|
382
|
+
-------
|
383
|
+
Callable[[Builder, Any], Any]
|
384
|
+
A callable that can be used to retrieve the data
|
385
|
+
"""
|
386
|
+
if isinstance(source, float):
|
387
|
+
return lambda builder, *_: source
|
388
|
+
|
389
|
+
try:
|
390
|
+
timedelta = pd.Timedelta(source)
|
391
|
+
return lambda builder, *_: timedelta
|
392
|
+
except ValueError:
|
393
|
+
pass
|
394
|
+
|
395
|
+
if "::" in source:
|
396
|
+
module, method = source.split("::")
|
397
|
+
return getattr(import_module(module), method)
|
398
|
+
|
399
|
+
try:
|
400
|
+
target_string = TargetString(source)
|
401
|
+
return lambda builder, *_: builder.data.load(target_string)
|
402
|
+
except ValueError:
|
403
|
+
pass
|
404
|
+
|
405
|
+
raise ValueError(f"Invalid data source '{source}' for '{name}'.")
|
406
|
+
|
407
|
+
######################
|
408
|
+
# Validation methods #
|
409
|
+
######################
|
410
|
+
|
411
|
+
_CAUSE_KEYS = {"model_type", "initial_state", "states", "transitions"}
|
412
|
+
_STATE_KEYS = {
|
413
|
+
"state_type",
|
414
|
+
"cause_type",
|
415
|
+
"transient",
|
416
|
+
"allow_self_transition",
|
417
|
+
"side_effect",
|
418
|
+
"data_sources",
|
419
|
+
"cleanup_function",
|
420
|
+
}
|
421
|
+
|
422
|
+
_DATA_SOURCE_KEYS = {
|
423
|
+
"state": {
|
424
|
+
"prevalence",
|
425
|
+
"birth_prevalence",
|
426
|
+
"dwell_time",
|
427
|
+
"disability_weight",
|
428
|
+
"excess_mortality_rate",
|
429
|
+
},
|
430
|
+
"rate_transition": {
|
431
|
+
"incidence_rate",
|
432
|
+
"transition_rate",
|
433
|
+
"remission_rate",
|
434
|
+
},
|
435
|
+
"proportion_transition": {"proportion"},
|
436
|
+
}
|
437
|
+
_TRANSITION_KEYS = {"source", "sink", "transition_type", "triggered", "data_sources"}
|
438
|
+
_TRANSITION_TYPE_KEYS = {"rate", "proportion", "dwell_time"}
|
439
|
+
|
440
|
+
@staticmethod
|
441
|
+
def _validate_external_configuration(external_configuration: ConfigTree) -> None:
|
442
|
+
"""
|
443
|
+
Validates the external configuration.
|
444
|
+
|
445
|
+
Parameters
|
446
|
+
----------
|
447
|
+
external_configuration
|
448
|
+
A ConfigTree defining the external configuration
|
449
|
+
|
450
|
+
Returns
|
451
|
+
-------
|
452
|
+
None
|
453
|
+
|
454
|
+
Raises
|
455
|
+
------
|
456
|
+
CausesParsingErrors
|
457
|
+
If the external configuration is invalid
|
458
|
+
"""
|
459
|
+
external_configuration = external_configuration.to_dict()
|
460
|
+
error_messages = []
|
461
|
+
for package, config_files in external_configuration.items():
|
462
|
+
if not isinstance(package, str):
|
463
|
+
error_messages.append(
|
464
|
+
f"Key '{package}' must be a string definition of a package."
|
465
|
+
)
|
466
|
+
if not isinstance(config_files, list):
|
467
|
+
error_messages.append(
|
468
|
+
f"External configuration for package '{package}' must be a list."
|
469
|
+
)
|
470
|
+
else:
|
471
|
+
for config_file in config_files:
|
472
|
+
if not isinstance(config_file, str) or not config_file.endswith(".yaml"):
|
473
|
+
error_messages.append(
|
474
|
+
f"External configuration for package '{package}' must "
|
475
|
+
"be a list of paths to yaml files."
|
476
|
+
)
|
477
|
+
if error_messages:
|
478
|
+
raise CausesParsingErrors(error_messages)
|
479
|
+
|
480
|
+
def _validate_causes_config(self, causes_config: ConfigTree) -> None:
|
481
|
+
"""
|
482
|
+
Validates the cause model configuration.
|
483
|
+
|
484
|
+
Parameters
|
485
|
+
----------
|
486
|
+
causes_config
|
487
|
+
A ConfigTree defining the cause model configurations
|
488
|
+
|
489
|
+
Returns
|
490
|
+
-------
|
491
|
+
None
|
492
|
+
|
493
|
+
Raises
|
494
|
+
------
|
495
|
+
CausesParsingErrors
|
496
|
+
If the cause model configuration is invalid
|
497
|
+
"""
|
498
|
+
causes_config = causes_config.to_dict()
|
499
|
+
error_messages = []
|
500
|
+
for cause_name, cause_config in causes_config.items():
|
501
|
+
error_messages += self._validate_cause(cause_name, cause_config)
|
502
|
+
|
503
|
+
if error_messages:
|
504
|
+
raise CausesParsingErrors(error_messages)
|
505
|
+
|
506
|
+
def _validate_cause(self, cause_name: str, cause_config: Dict[str, Any]) -> List[str]:
|
507
|
+
"""
|
508
|
+
Validates a cause configuration and returns a list of error messages.
|
509
|
+
|
510
|
+
Parameters
|
511
|
+
----------
|
512
|
+
cause_name
|
513
|
+
The name of the cause to validate
|
514
|
+
cause_config
|
515
|
+
A ConfigTree defining the cause to validate
|
516
|
+
|
517
|
+
Returns
|
518
|
+
-------
|
519
|
+
List[str]
|
520
|
+
A list of error messages
|
521
|
+
"""
|
522
|
+
error_messages = []
|
523
|
+
if not isinstance(cause_config, dict):
|
524
|
+
error_messages.append(
|
525
|
+
f"Cause configuration for cause '{cause_name}' must be a dictionary."
|
526
|
+
)
|
527
|
+
return error_messages
|
528
|
+
|
529
|
+
if not set(cause_config.keys()).issubset(self._CAUSE_KEYS):
|
530
|
+
error_messages.append(
|
531
|
+
f"Cause configuration for cause '{cause_name}' may only"
|
532
|
+
" contain the following keys: "
|
533
|
+
f"{self._CAUSE_KEYS}."
|
534
|
+
)
|
535
|
+
|
536
|
+
if "model_type" in cause_config:
|
537
|
+
error_messages += self._validate_imported_type(
|
538
|
+
cause_config["model_type"], cause_name, "model"
|
539
|
+
)
|
540
|
+
|
541
|
+
states_config = cause_config.get("states", {})
|
542
|
+
if not states_config:
|
543
|
+
error_messages.append(
|
544
|
+
f"Cause configuration for cause '{cause_name}' must define "
|
545
|
+
"at least one state."
|
546
|
+
)
|
547
|
+
|
548
|
+
if not isinstance(states_config, dict):
|
549
|
+
error_messages.append(
|
550
|
+
f"States configuration for cause '{cause_name}' must be a dictionary."
|
551
|
+
)
|
552
|
+
else:
|
553
|
+
initial_state = cause_config.get("initial_state", None)
|
554
|
+
if initial_state is not None and initial_state not in states_config:
|
555
|
+
error_messages.append(
|
556
|
+
f"Initial state '{cause_config['initial_state']}' for cause "
|
557
|
+
f"'{cause_name}' must be present in the states for cause "
|
558
|
+
f"'{cause_name}."
|
559
|
+
)
|
560
|
+
for state_name, state_config in states_config.items():
|
561
|
+
error_messages += self._validate_state(cause_name, state_name, state_config)
|
562
|
+
|
563
|
+
transitions_config = cause_config.get("transitions", {})
|
564
|
+
if not isinstance(transitions_config, dict):
|
565
|
+
error_messages.append(
|
566
|
+
f"Transitions configuration for cause '{cause_name}' must be "
|
567
|
+
"a dictionary if it is present."
|
568
|
+
)
|
569
|
+
else:
|
570
|
+
for transition_name, transition_config in transitions_config.items():
|
571
|
+
error_messages += self._validate_transition(
|
572
|
+
cause_name, transition_name, transition_config, states_config
|
573
|
+
)
|
574
|
+
|
575
|
+
return error_messages
|
576
|
+
|
577
|
+
def _validate_state(
|
578
|
+
self, cause_name: str, state_name: str, state_config: Dict[str, Any]
|
579
|
+
) -> List[str]:
|
580
|
+
"""
|
581
|
+
Validates a state configuration and returns a list of error messages.
|
582
|
+
|
583
|
+
Parameters
|
584
|
+
----------
|
585
|
+
cause_name
|
586
|
+
The name of the cause to which the state belongs
|
587
|
+
state_name
|
588
|
+
The name of the state to validate
|
589
|
+
state_config
|
590
|
+
A ConfigTree defining the state to validate
|
591
|
+
|
592
|
+
Returns
|
593
|
+
-------
|
594
|
+
List[str]
|
595
|
+
A list of error messages
|
596
|
+
"""
|
597
|
+
error_messages = []
|
598
|
+
|
599
|
+
if not isinstance(state_config, dict):
|
600
|
+
error_messages.append(
|
601
|
+
f"State configuration for in cause '{cause_name}' and "
|
602
|
+
f"state '{state_name}' must be a dictionary."
|
603
|
+
)
|
604
|
+
return error_messages
|
605
|
+
|
606
|
+
allowable_keys = set(self._STATE_KEYS)
|
607
|
+
if state_name in ["susceptible", "recovered"]:
|
608
|
+
allowable_keys.remove("data_sources")
|
609
|
+
|
610
|
+
if not set(state_config.keys()).issubset(allowable_keys):
|
611
|
+
error_messages.append(
|
612
|
+
f"State configuration for in cause '{cause_name}' and "
|
613
|
+
f"state '{state_name}' may only contain the following "
|
614
|
+
f"keys: {allowable_keys}."
|
615
|
+
)
|
616
|
+
|
617
|
+
state_type = state_config.get("state_type", "")
|
618
|
+
error_messages += self._validate_imported_type(
|
619
|
+
state_type, cause_name, "state", state_name
|
620
|
+
)
|
621
|
+
|
622
|
+
if not isinstance(state_config.get("cause_type", ""), str):
|
623
|
+
error_messages.append(
|
624
|
+
f"Cause type for state '{state_name}' in cause '{cause_name}' "
|
625
|
+
f"must be a string. Provided {state_config['cause_type']}."
|
626
|
+
)
|
627
|
+
is_transient = state_config.get("transient", False)
|
628
|
+
if not isinstance(is_transient, bool):
|
629
|
+
error_messages.append(
|
630
|
+
f"Transient flag for state '{state_name}' in cause '{cause_name}' "
|
631
|
+
f"must be a boolean. Provided {state_config['transient']}."
|
632
|
+
)
|
633
|
+
|
634
|
+
if state_name in ["susceptible", "recovered"] and state_type:
|
635
|
+
error_messages.append(
|
636
|
+
f"The name '{state_name}' in cause '{cause_name}' concretely "
|
637
|
+
f"specifies the state type, so state_type is not an allowed "
|
638
|
+
"configuration."
|
639
|
+
)
|
640
|
+
|
641
|
+
if state_name in ["susceptible", "recovered"] and is_transient:
|
642
|
+
error_messages.append(
|
643
|
+
f"The name '{state_name}' in cause '{cause_name}' concretely "
|
644
|
+
f"specifies the state type, so transient is not an allowed "
|
645
|
+
"configuration."
|
646
|
+
)
|
647
|
+
|
648
|
+
if is_transient and state_type:
|
649
|
+
error_messages.append(
|
650
|
+
f"Specifying transient as True for state '{state_name}' in cause "
|
651
|
+
f"'{cause_name}' concretely specifies the state type, so "
|
652
|
+
"state_type is not an allowed configuration."
|
653
|
+
)
|
654
|
+
|
655
|
+
if not isinstance(state_config.get("allow_self_transition", True), bool):
|
656
|
+
error_messages.append(
|
657
|
+
f"Allow self transition flag for state '{state_name}' in cause "
|
658
|
+
f"'{cause_name}' must be a boolean. Provided "
|
659
|
+
f"'{state_config['allow_self_transition']}'."
|
660
|
+
)
|
661
|
+
|
662
|
+
error_messages += self._validate_data_sources(
|
663
|
+
state_config, cause_name, "state", state_name
|
664
|
+
)
|
665
|
+
|
666
|
+
return error_messages
|
667
|
+
|
668
|
+
def _validate_transition(
|
669
|
+
self,
|
670
|
+
cause_name: str,
|
671
|
+
transition_name: str,
|
672
|
+
transition_config: Dict[str, Any],
|
673
|
+
states_config: Dict[str, Any],
|
674
|
+
) -> List[str]:
|
675
|
+
"""
|
676
|
+
Validates a transition configuration and returns a list of error messages.
|
677
|
+
|
678
|
+
Parameters
|
679
|
+
----------
|
680
|
+
cause_name
|
681
|
+
The name of the cause to which the transition belongs
|
682
|
+
transition_name
|
683
|
+
The name of the transition to validate
|
684
|
+
transition_config
|
685
|
+
A ConfigTree defining the transition to validate
|
686
|
+
states_config
|
687
|
+
A ConfigTree defining the states for the cause
|
688
|
+
|
689
|
+
Returns
|
690
|
+
-------
|
691
|
+
List[str]
|
692
|
+
A list of error messages
|
693
|
+
"""
|
694
|
+
error_messages = []
|
695
|
+
|
696
|
+
if not isinstance(transition_config, dict):
|
697
|
+
error_messages.append(
|
698
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
699
|
+
f"transition '{transition_name}' must be a dictionary."
|
700
|
+
)
|
701
|
+
return error_messages
|
702
|
+
|
703
|
+
if not set(transition_config.keys()).issubset(
|
704
|
+
CausesConfigurationParser._TRANSITION_KEYS
|
705
|
+
):
|
706
|
+
error_messages.append(
|
707
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
708
|
+
f"transition '{transition_name}' may only contain the "
|
709
|
+
f"following keys: {self._TRANSITION_KEYS}."
|
710
|
+
)
|
711
|
+
source = transition_config.get("source", None)
|
712
|
+
sink = transition_config.get("sink", None)
|
713
|
+
if sink is None or source is None:
|
714
|
+
error_messages.append(
|
715
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
716
|
+
f"transition '{transition_name}' must contain both a source "
|
717
|
+
f"and a sink."
|
718
|
+
)
|
719
|
+
|
720
|
+
if source is not None and source not in states_config:
|
721
|
+
error_messages.append(
|
722
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
723
|
+
f"transition '{transition_name}' must contain a source that "
|
724
|
+
f"is present in the states."
|
725
|
+
)
|
726
|
+
|
727
|
+
if sink is not None and sink not in states_config:
|
728
|
+
error_messages.append(
|
729
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
730
|
+
f"transition '{transition_name}' must contain a sink that "
|
731
|
+
f"is present in the states."
|
732
|
+
)
|
733
|
+
|
734
|
+
if (
|
735
|
+
"triggered" in transition_config
|
736
|
+
and transition_config["triggered"] not in Trigger.__members__
|
737
|
+
):
|
738
|
+
error_messages.append(
|
739
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
740
|
+
f"transition '{transition_name}' may only have one of the following "
|
741
|
+
f"values: {Trigger.__members__}."
|
742
|
+
)
|
743
|
+
|
744
|
+
if "transition_type" not in transition_config:
|
745
|
+
error_messages.append(
|
746
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
747
|
+
f"transition '{transition_name}' must contain a transition type."
|
748
|
+
)
|
749
|
+
else:
|
750
|
+
transition_type = transition_config["transition_type"]
|
751
|
+
if transition_type not in self._TRANSITION_TYPE_KEYS:
|
752
|
+
error_messages.append(
|
753
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
754
|
+
f"transition '{transition_name}' may only contain the "
|
755
|
+
f"following values: {self._TRANSITION_TYPE_KEYS}."
|
756
|
+
)
|
757
|
+
if transition_type == "dwell_time" and "data_sources" in transition_config:
|
758
|
+
error_messages.append(
|
759
|
+
f"Transition configuration for in cause '{cause_name}' and "
|
760
|
+
f"transition '{transition_name}' is a dwell-time transition and "
|
761
|
+
f"may not have data sources as dwell-time is configured on the state."
|
762
|
+
)
|
763
|
+
elif transition_type in self._TRANSITION_TYPE_KEYS.difference({"dwell_time"}):
|
764
|
+
error_messages += self._validate_data_sources(
|
765
|
+
transition_config,
|
766
|
+
cause_name,
|
767
|
+
f"{transition_type}_transition",
|
768
|
+
transition_name,
|
769
|
+
)
|
770
|
+
return error_messages
|
771
|
+
|
772
|
+
@staticmethod
|
773
|
+
def _validate_imported_type(
|
774
|
+
import_path: str, cause_name: str, entity_type: str, entity_name: Optional[str] = None
|
775
|
+
) -> List[str]:
|
776
|
+
"""
|
777
|
+
Validates an imported type and returns a list of error messages.
|
778
|
+
|
779
|
+
Parameters
|
780
|
+
----------
|
781
|
+
import_path
|
782
|
+
The import path to validate
|
783
|
+
cause_name
|
784
|
+
The name of the cause to which the imported type belongs
|
785
|
+
entity_type
|
786
|
+
The type of the entity to which the imported type belongs
|
787
|
+
entity_name
|
788
|
+
The name of the entity to which the imported type belongs, if it is
|
789
|
+
not a cause
|
790
|
+
|
791
|
+
Returns
|
792
|
+
-------
|
793
|
+
List[str]
|
794
|
+
A list of error messages
|
795
|
+
"""
|
796
|
+
expected_type = {"model": DiseaseModel, "state": BaseDiseaseState}[entity_type]
|
797
|
+
|
798
|
+
error_messages = []
|
799
|
+
if not import_path:
|
800
|
+
return error_messages
|
801
|
+
|
802
|
+
try:
|
803
|
+
imported_type = import_by_path(import_path)
|
804
|
+
if not (
|
805
|
+
isinstance(imported_type, type) and issubclass(imported_type, expected_type)
|
806
|
+
):
|
807
|
+
raise TypeError
|
808
|
+
except (ModuleNotFoundError, AttributeError, TypeError, ValueError):
|
809
|
+
error_messages.append(
|
810
|
+
f"If '{entity_type}_type' is provided for cause '{cause_name}' "
|
811
|
+
f"{f'and {entity_type} {entity_name} ' if entity_name else ''}it "
|
812
|
+
f"must be the fully qualified import path to a {expected_type} "
|
813
|
+
f"implementation. Provided'{import_path}'."
|
814
|
+
)
|
815
|
+
return error_messages
|
816
|
+
|
817
|
+
def _validate_data_sources(
|
818
|
+
self, config: Dict[str, Any], cause_name: str, config_type: str, config_name: str
|
819
|
+
) -> List[str]:
|
820
|
+
"""
|
821
|
+
Validates the data sources in a configuration and returns a list of
|
822
|
+
error messages.
|
823
|
+
|
824
|
+
Parameters
|
825
|
+
----------
|
826
|
+
config
|
827
|
+
A ConfigTree defining the configuration to validate
|
828
|
+
cause_name
|
829
|
+
The name of the cause to which the configuration belongs
|
830
|
+
config_type
|
831
|
+
The type of the configuration to validate
|
832
|
+
config_name
|
833
|
+
The name of the configuration being validated
|
834
|
+
|
835
|
+
Returns
|
836
|
+
-------
|
837
|
+
List[str]
|
838
|
+
A list of error messages
|
839
|
+
"""
|
840
|
+
error_messages = []
|
841
|
+
data_sources_config = config.get("data_sources", {})
|
842
|
+
if not isinstance(data_sources_config, dict):
|
843
|
+
error_messages.append(
|
844
|
+
f"Data sources configuration for {config_type} '{config}' in "
|
845
|
+
f"cause '{cause_name}' must be a dictionary if it is present."
|
846
|
+
)
|
847
|
+
return error_messages
|
848
|
+
|
849
|
+
if not set(data_sources_config.keys()).issubset(self._DATA_SOURCE_KEYS[config_type]):
|
850
|
+
error_messages.append(
|
851
|
+
f"Data sources configuration for {config_type} '{config_name}' "
|
852
|
+
f"in cause '{cause_name}' may only contain the following keys: "
|
853
|
+
f"{self._DATA_SOURCE_KEYS[config_type]}."
|
854
|
+
)
|
855
|
+
|
856
|
+
for config_name, source in data_sources_config.items():
|
857
|
+
try:
|
858
|
+
self._get_data_source(config_name, source)
|
859
|
+
except ValueError:
|
860
|
+
error_messages.append(
|
861
|
+
f"Configuration for {config_type} '{config_name}' in cause "
|
862
|
+
f"'{cause_name}' has an invalid data source at '{source}'."
|
863
|
+
)
|
864
|
+
return error_messages
|
@@ -7,6 +7,7 @@ This module contains tools for handling raw demographic data and transforming
|
|
7
7
|
it into different distributions for sampling.
|
8
8
|
|
9
9
|
"""
|
10
|
+
|
10
11
|
from collections import namedtuple
|
11
12
|
from typing import Tuple, Union
|
12
13
|
|
@@ -54,23 +55,23 @@ def assign_demographic_proportions(
|
|
54
55
|
|
55
56
|
population_data["P(sex, location, age| year)"] = (
|
56
57
|
population_data.groupby("year_start", as_index=False)
|
57
|
-
.apply(lambda sub_pop: sub_pop
|
58
|
-
.reset_index(level=0)
|
59
|
-
.
|
58
|
+
.apply(lambda sub_pop: sub_pop[["value"]] / sub_pop["value"].sum())
|
59
|
+
.reset_index(level=0)["value"]
|
60
|
+
.fillna(0.0)
|
60
61
|
)
|
61
62
|
|
62
63
|
population_data["P(sex, location | age, year)"] = (
|
63
64
|
population_data.groupby(["age", "year_start"], as_index=False)
|
64
|
-
.apply(lambda sub_pop: sub_pop
|
65
|
-
.reset_index(level=0)
|
66
|
-
.
|
65
|
+
.apply(lambda sub_pop: sub_pop[["value"]] / sub_pop["value"].sum())
|
66
|
+
.reset_index(level=0)["value"]
|
67
|
+
.fillna(0.0)
|
67
68
|
)
|
68
69
|
|
69
70
|
population_data["P(age | year, sex, location)"] = (
|
70
71
|
population_data.groupby(["year_start", "sex", "location"], as_index=False)
|
71
|
-
.apply(lambda sub_pop: sub_pop
|
72
|
-
.reset_index(level=0)
|
73
|
-
.
|
72
|
+
.apply(lambda sub_pop: sub_pop[["value"]] / sub_pop["value"].sum())
|
73
|
+
.reset_index(level=0)["value"]
|
74
|
+
.fillna(0.0)
|
74
75
|
)
|
75
76
|
|
76
77
|
return population_data.sort_values(_SORT_ORDER).reset_index(drop=True)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: vivarium_public_health
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.2.0
|
4
4
|
Summary: Components for modelling diseases, risks, and interventions with ``vivarium``
|
5
5
|
Home-page: https://github.com/ihmeuw/vivarium_public_health
|
6
6
|
Author: The vivarium developers
|
@@ -27,6 +27,7 @@ Classifier: Topic :: Scientific/Engineering :: Physics
|
|
27
27
|
Classifier: Topic :: Software Development :: Libraries
|
28
28
|
License-File: LICENSE.txt
|
29
29
|
Requires-Dist: vivarium >=2.0.0
|
30
|
+
Requires-Dist: loguru
|
30
31
|
Requires-Dist: numpy
|
31
32
|
Requires-Dist: pandas
|
32
33
|
Requires-Dist: scipy
|
@@ -41,6 +42,7 @@ Requires-Dist: matplotlib ; extra == 'dev'
|
|
41
42
|
Requires-Dist: pytest ; extra == 'dev'
|
42
43
|
Requires-Dist: pytest-mock ; extra == 'dev'
|
43
44
|
Requires-Dist: hypothesis ; extra == 'dev'
|
45
|
+
Requires-Dist: pyyaml ; extra == 'dev'
|
44
46
|
Provides-Extra: docs
|
45
47
|
Requires-Dist: sphinx <7.0 ; extra == 'docs'
|
46
48
|
Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
|
@@ -51,6 +53,7 @@ Provides-Extra: test
|
|
51
53
|
Requires-Dist: pytest ; extra == 'test'
|
52
54
|
Requires-Dist: pytest-mock ; extra == 'test'
|
53
55
|
Requires-Dist: hypothesis ; extra == 'test'
|
56
|
+
Requires-Dist: pyyaml ; extra == 'test'
|
54
57
|
|
55
58
|
Vivarium Public Health
|
56
59
|
======================
|
@@ -1,6 +1,6 @@
|
|
1
1
|
vivarium_public_health/__about__.py,sha256=RgWycPypKZS80TpSX7o41cREnG8PfguNHDHLuLyl820,487
|
2
2
|
vivarium_public_health/__init__.py,sha256=tomMOl3PI7O8GdxDWGBiBjT0Bwd31GpyQTYTzwIv108,361
|
3
|
-
vivarium_public_health/_version.py,sha256=
|
3
|
+
vivarium_public_health/_version.py,sha256=DKk-1b-rZsJFxFi1JoJ7TmEvIEQ0rf-C9HAZWwvjuM0,22
|
4
4
|
vivarium_public_health/utilities.py,sha256=_X9sZQ7flsi2sVWQ9zrf8GJw8QwZsPZm3NUjx1gu7bM,2555
|
5
5
|
vivarium_public_health/disease/__init__.py,sha256=RuuiRcvAJfX9WQGt_WZZjxN7Cu3E5rMTmuaRS-UaFPM,419
|
6
6
|
vivarium_public_health/disease/model.py,sha256=7xFGo6JqPjQjm2VUZ3u3ThXWSzmitTOCZ8N9PTTL6MU,8253
|
@@ -21,10 +21,12 @@ vivarium_public_health/mslt/intervention.py,sha256=q6-ZHYFuV9o7WKlHMpiFAsEH2fYzw
|
|
21
21
|
vivarium_public_health/mslt/magic_wand_components.py,sha256=7sy_7fa0R5pk5XPdt9-AfK005JV3em4Tvu4L4xeg4g0,3980
|
22
22
|
vivarium_public_health/mslt/observer.py,sha256=Z0aWLrlSjxLuYqzXarNunJ2Xqei4R9nX03dnE6uvr4o,13940
|
23
23
|
vivarium_public_health/mslt/population.py,sha256=R5Z7aBf63LDbasPVMMI0HTh41FIL_OAhYk0qQWf_-lU,7285
|
24
|
+
vivarium_public_health/plugins/__init__.py,sha256=oBW_zfgG_LbwfgTDjUe0btfy9FaDvAbtXho1zQFnz0Y,76
|
25
|
+
vivarium_public_health/plugins/parser.py,sha256=uhBw5t-Lmb8YDN2GvVG93l50ZuCIsg4VocSA5T_wz3w,31789
|
24
26
|
vivarium_public_health/population/__init__.py,sha256=17rtbcNVK5LtCCxAex7P7Q_vYpwbeTepyf3nazS90Yc,225
|
25
27
|
vivarium_public_health/population/add_new_birth_cohorts.py,sha256=qNsZjvaJ7Et8_Kw7JNyRshHHRA3pEQMM4TSqCp48Gr4,9092
|
26
28
|
vivarium_public_health/population/base_population.py,sha256=U9FibBoPuYWvUqPFCUIVwjBxQVuhTb58cUy-2EJSzio,15345
|
27
|
-
vivarium_public_health/population/data_transformations.py,sha256=
|
29
|
+
vivarium_public_health/population/data_transformations.py,sha256=3MNDrfaZMMGM3crNzucGfYWZOTGHoavIcu8wxq1xIU8,21838
|
28
30
|
vivarium_public_health/population/mortality.py,sha256=w1Oxb958LjUkNwxJ0vdA3TZndpeNiaH3d7RukLas_oQ,10085
|
29
31
|
vivarium_public_health/risks/__init__.py,sha256=XvX12RgD0iF5PBoc2StsOhxJmK1FP-RaAYrjIT9MfDs,232
|
30
32
|
vivarium_public_health/risks/base_risk.py,sha256=6D7YlxQOdQm-Kw5_vjpQmFqU7spF-lTy14WEEefRQlA,6494
|
@@ -40,8 +42,8 @@ vivarium_public_health/treatment/__init__.py,sha256=wONElu9aJbBYwpYIovYPYaN_GYfV
|
|
40
42
|
vivarium_public_health/treatment/magic_wand.py,sha256=iPKFN3VjfiMy_XvN94UqM-FUrGuI0ULwmOdAGdOepYQ,1979
|
41
43
|
vivarium_public_health/treatment/scale_up.py,sha256=7QKBgAII4dwkds9gdbQ5d6oDaD02iwcQCVcYRN-B4Mg,7573
|
42
44
|
vivarium_public_health/treatment/therapeutic_inertia.py,sha256=VwZ7t90zzfGoBusduIvcE4lDe5zTvzmHiUNB3u2I52Y,2339
|
43
|
-
vivarium_public_health-2.
|
44
|
-
vivarium_public_health-2.
|
45
|
-
vivarium_public_health-2.
|
46
|
-
vivarium_public_health-2.
|
47
|
-
vivarium_public_health-2.
|
45
|
+
vivarium_public_health-2.2.0.dist-info/LICENSE.txt,sha256=mN4bNLUQNcN9njYRc_3jCZkfPySVpmM6MRps104FxA4,1548
|
46
|
+
vivarium_public_health-2.2.0.dist-info/METADATA,sha256=AyFXCJ4aiPYreEVtAkAFpOODBMkUCIs0pXiTp5W-dy8,3531
|
47
|
+
vivarium_public_health-2.2.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
48
|
+
vivarium_public_health-2.2.0.dist-info/top_level.txt,sha256=VVInlpzCFD0UNNhjOq_j-a29odzjwUwYFTGfvqbi4dY,23
|
49
|
+
vivarium_public_health-2.2.0.dist-info/RECORD,,
|
{vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/LICENSE.txt
RENAMED
File without changes
|
File without changes
|
{vivarium_public_health-2.1.4.dist-info → vivarium_public_health-2.2.0.dist-info}/top_level.txt
RENAMED
File without changes
|