9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172 | class SusceptibilityDistribution(Updateable):
def __init__(self,
ages_years: List[float],
susceptible_fraction: List[float]):
"""
A by-age susceptibility to infection distribution in fraction units 0 to 1. This is used whenever an agent is
created, such as during model initialization and when agents are born.
For Generic (GENERIC_SIM) simulations only.
The SusceptibilityDistribution provides a probability each agent will be initialized as susceptible to
infection (or not). It models the effect of natural immunity in preventing infection entirely in (1-fraction)
of the population. Those that are allowed to acquire an infection can also be affected by other interventions
or immunity derived from getting the disease. Agents are identified at creation time as 'susceptible to
infection' by a uniform random number draw that is compared to the susceptibility distribution value at the
corresponding agent age. If an agents age lies between two provided ages, its chances of being susceptible to
infection are linearly interpolated from the two closest corresponding ages. If the agents age lies beyond the
provided ages, the closest age-corresponding susceptibility will be used.
WARNING: This complex distribution is different than when using a SimpleDistribution for this feature. The
complex distribution makes people either completely susceptible or completely immune. In contrast, simple
distributions give each person a probability of acquiring an infection (i.e. value between 0 and 1 versus
just 0 or 1).
Args:
ages_years: (List[float]) A list of ages (in years) that susceptibility fraction data will be provided for.
Must be a list of monotonically increasing floats within range 0 <= age <= 200 years.
susceptible_fraction: (List[float]) A list of susceptibility fractions corresponding to the provided
ages_years list. These represent the chances an initialized agent at a given age will be susceptible to
infection. Must be a list of floats within range 0 <= fraction <= 1 .
Example:
ages_years: [0, 10, 20, 50, 100]
susceptible_fraction: [0.9, 0.7, 0.3, 0.5, 0.8]
Agent age 10 years
susceptible chance: 0.7
Agent age 15 years:
susceptible chance: 0.7 + (15 - 10) * ((0.3-0.7) / (20-10)) = 0.5
Agent age 1000 years (beyond provided age range)
susceptible chance: 0.8 (nearest corresponding fraction)
"""
super().__init__()
self.ages_years = ages_years
self.susceptible_fraction = susceptible_fraction
# This will convert the object to an susceptibility distribution dictionary and then validate it reporting
# object-relevant messages
self._validate(distribution_dict=self.to_dict(validate=False), source_is_dict=False)
@classmethod
def _rate_scale_factor(cls):
return 1
def to_dict(self, validate: bool = True) -> Dict:
# susceptibility distribution dicts MUST be in ages_days. objs must be in ages_years
distribution_dict = {
'ResultValues': self.susceptible_fraction,
'DistributionValues': [years * 365 for years in self.ages_years],
'ResultScaleFactor': self._rate_scale_factor()
}
if validate:
self._validate(distribution_dict=distribution_dict, source_is_dict=False)
return distribution_dict
@classmethod
def from_dict(cls, distribution_dict: Dict):
# susceptibility distribution dicts MUST be in ages_days. objs must be in ages_years
cls._validate(distribution_dict=distribution_dict, source_is_dict=True)
ages_years = [days / 365 for days in distribution_dict['DistributionValues']]
return cls(ages_years=ages_years,
susceptible_fraction=distribution_dict['ResultValues'])
_validation_messages = {
'fixed_value_check': {
True: "key: %s value: %s does not match expected value: %s",
False: None # These are all properties of the obj and cannot be made invalid
},
'data_dimensionality_check_ages': {
True: 'DistributionValues must be a 1-d array of floats',
False: 'ages_years must be a 1-d array of floats'
},
'data_dimensionality_check_susceptibility': {
True: 'ResultValues must be a 1-d array of floats',
False: 'susceptible_fraction must be a 1-d array of floats'
},
'age_and_susceptibility_length_check': {
True: 'DistributionValues and ResultValues must be the same length but are not',
False: 'ages_years and susceptible_fraction must be the same length but are not'
},
'age_range_check': {
True: "DistributionValues age values must be: 0 <= age <= 73000 in days. Out-of-range index:values : %s",
False: "All ages_years values must be: 0 <= age <= 200 in years. Out-of-range index:values : %s"
},
'susceptibility_range_check': {
True: "ResultValues susceptible fractions must be: 0 <= fraction <= 1. "
"Out-of-range index:values : %s",
False: "All susceptible_fraction values must be: 0 <= fraction <= 1. "
"Out-of-range index:values : %s"
},
'age_monotonicity_check': {
True: "DistributionValues ages in days must monotonically increase but do not, index: %d value: %s",
False: "ages_years values must monotonically increase but do not, index: %d value: %s"
}
}
@classmethod
def _validate(cls, distribution_dict: Dict, source_is_dict: bool):
"""
Validate a SusceptibilityDistribution in dict form
Args:
distribution_dict: (dict) the susceptibility distribution dict to validate
source_is_dict: (bool) If true, report dict-relevant error messages. If false, report obj-relevant messages.
Returns:
Nothing
"""
if source_is_dict is True:
expected_values = {
'ResultScaleFactor': cls._rate_scale_factor()
}
for key, expected_value in expected_values.items():
value = distribution_dict[key]
if value != expected_value:
message = cls._validation_messages['fixed_value_check'][source_is_dict] % (key, value, expected_value)
raise demog_ex.InvalidFixedValueException(message)
# ensure the ages and distribution values are both 1-d iterables of the same length
ages = distribution_dict['DistributionValues']
susceptible_values = distribution_dict['ResultValues']
is_1d = check_dimensionality(data=ages, dimensionality=1)
if not is_1d:
message = cls._validation_messages['data_dimensionality_check_ages'][source_is_dict]
raise demog_ex.InvalidDataDimensionality(message)
is_1d = check_dimensionality(data=susceptible_values, dimensionality=1)
if not is_1d:
message = cls._validation_messages['data_dimensionality_check_susceptibility'][source_is_dict]
raise demog_ex.InvalidDataDimensionality(message)
if len(ages) != len(susceptible_values):
message = cls._validation_messages['age_and_susceptibility_length_check'][source_is_dict]
raise demog_ex.InvalidDataDimensionLength(message)
# ensure the age and susceptibility value lists are ascending and in reasonable ranges
# record in days for dict-relevant messages, years for obj-relevant messages
factor = 1 if source_is_dict is True else 1 / 365.0
out_of_range = [f"{index}:{age * factor}" for index, age in enumerate(ages) if (age < 0 * 365) or (age > 200 * 365)]
if len(out_of_range) > 0:
oor_str = ', '.join(out_of_range)
message = cls._validation_messages['age_range_check'][source_is_dict] % oor_str
raise demog_ex.AgeOutOfRangeException(message)
out_of_range = [f"{index}:{value}" for index, value in enumerate(susceptible_values)
if (value < 0) or (value > 1)]
if len(out_of_range) > 0:
oor_str = ', '.join(out_of_range)
message = cls._validation_messages['susceptibility_range_check'][source_is_dict] % oor_str
raise demog_ex.DistributionOutOfRangeException(message)
for i in range(1, len(ages)):
if ages[i] - ages[i - 1] <= 0:
message = cls._validation_messages['age_monotonicity_check'][source_is_dict] % (i, ages[i])
raise demog_ex.NonMonotonicAgeException(message)
|