emod-api 3.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emod_api/__init__.py +1 -0
- emod_api/campaign.py +170 -0
- emod_api/channelreports/__init__.py +0 -0
- emod_api/channelreports/channels.py +433 -0
- emod_api/channelreports/icj_to_csv.py +65 -0
- emod_api/channelreports/plot_icj_means.py +149 -0
- emod_api/channelreports/plot_prop_report.py +205 -0
- emod_api/channelreports/utils.py +326 -0
- emod_api/config/__init__.py +0 -0
- emod_api/config/default_from_schema.py +16 -0
- emod_api/config/default_from_schema_no_validation.py +177 -0
- emod_api/config/from_overrides.py +135 -0
- emod_api/demographics/__init__.py +0 -0
- emod_api/demographics/age_distribution.py +163 -0
- emod_api/demographics/base_input_file.py +28 -0
- emod_api/demographics/calculators.py +159 -0
- emod_api/demographics/demographic_exceptions.py +54 -0
- emod_api/demographics/demographics.py +249 -0
- emod_api/demographics/demographics_base.py +752 -0
- emod_api/demographics/demographics_overlay.py +41 -0
- emod_api/demographics/fertility_distribution.py +235 -0
- emod_api/demographics/implicit_functions.py +112 -0
- emod_api/demographics/mortality_distribution.py +227 -0
- emod_api/demographics/node.py +456 -0
- emod_api/demographics/overlay_node.py +16 -0
- emod_api/demographics/properties_and_attributes.py +737 -0
- emod_api/demographics/service/__init__.py +0 -0
- emod_api/demographics/service/grid_construction.py +143 -0
- emod_api/demographics/service/service.py +55 -0
- emod_api/demographics/susceptibility_distribution.py +170 -0
- emod_api/demographics/updateable.py +58 -0
- emod_api/legacy/__init__.py +0 -0
- emod_api/legacy/plotAllCharts.py +230 -0
- emod_api/migration/__init__.py +0 -0
- emod_api/migration/__main__.py +22 -0
- emod_api/migration/migration.py +782 -0
- emod_api/multidim_plotter.py +80 -0
- emod_api/schema_to_class.py +440 -0
- emod_api/serialization/__init__.py +0 -0
- emod_api/serialization/census_and_mod_pop.py +48 -0
- emod_api/serialization/dtk_file_support.py +61 -0
- emod_api/serialization/dtk_file_tools.py +1378 -0
- emod_api/serialization/dtk_file_utility.py +141 -0
- emod_api/serialization/serialized_population.py +205 -0
- emod_api/spatialreports/__init__.py +0 -0
- emod_api/spatialreports/__main__.py +67 -0
- emod_api/spatialreports/plot_spat_means.py +99 -0
- emod_api/spatialreports/spatial.py +210 -0
- emod_api/utils/__init__.py +26 -0
- emod_api/utils/distributions/__init__.py +0 -0
- emod_api/utils/distributions/base_distribution.py +38 -0
- emod_api/utils/distributions/bimodal_distribution.py +64 -0
- emod_api/utils/distributions/constant_distribution.py +58 -0
- emod_api/utils/distributions/demographic_distribution_flag.py +16 -0
- emod_api/utils/distributions/distribution_type.py +15 -0
- emod_api/utils/distributions/dual_constant_distribution.py +68 -0
- emod_api/utils/distributions/dual_exponential_distribution.py +75 -0
- emod_api/utils/distributions/exponential_distribution.py +63 -0
- emod_api/utils/distributions/gaussian_distribution.py +69 -0
- emod_api/utils/distributions/log_normal_distribution.py +61 -0
- emod_api/utils/distributions/poisson_distribution.py +59 -0
- emod_api/utils/distributions/uniform_distribution.py +70 -0
- emod_api/utils/distributions/weibull_distribution.py +69 -0
- emod_api/utils/str_enum.py +6 -0
- emod_api/weather/__init__.py +0 -0
- emod_api/weather/weather.py +428 -0
- emod_api-3.0.2.dist-info/METADATA +131 -0
- emod_api-3.0.2.dist-info/RECORD +71 -0
- emod_api-3.0.2.dist-info/WHEEL +5 -0
- emod_api-3.0.2.dist-info/licenses/LICENSE +21 -0
- emod_api-3.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Union
|
|
6
|
+
import warnings
|
|
7
|
+
import emod_api.schema_to_class as s2c
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _set_defaults_for_schema_group(dc_param,
|
|
11
|
+
schema_section,
|
|
12
|
+
schema):
|
|
13
|
+
"""
|
|
14
|
+
By making this part of write_default_from_schema its own function, it becomes reusable for the purposes
|
|
15
|
+
of Malaria_Drug_params which is a pretty funky part of the schema honestly.
|
|
16
|
+
"""
|
|
17
|
+
for param in schema_section:
|
|
18
|
+
if param == "class":
|
|
19
|
+
continue
|
|
20
|
+
if 'default' in schema_section[param]:
|
|
21
|
+
value = schema_section[param]['default']
|
|
22
|
+
dc_param[param] = value
|
|
23
|
+
dc_param["schema"][param] = schema_section[param]
|
|
24
|
+
# Our vectors don't seem to have defaults but we think an empty array is a valid default based on type alone?
|
|
25
|
+
elif "type" in schema_section[param] and "Vector" in schema_section[param]["type"]:
|
|
26
|
+
dc_param[param] = list()
|
|
27
|
+
dc_param["schema"][param] = schema_section[param]
|
|
28
|
+
else:
|
|
29
|
+
dc_param[param] = {} # hack for Drug Params.... and for the nested ones.
|
|
30
|
+
keys = [x for x in schema_section[param].keys()]
|
|
31
|
+
# This is all too "special-casey"
|
|
32
|
+
if len(keys) == 1:
|
|
33
|
+
key = keys[0] # HACK
|
|
34
|
+
dc_param["schema"][param] = schema_section[param][key]
|
|
35
|
+
else:
|
|
36
|
+
# e.g., Fractional_Dose_xxx map which has comlex type
|
|
37
|
+
# Want to call this but we need the full json so we can access the idmType section
|
|
38
|
+
# print( "Don't actually want to do this but just testing the concept." )
|
|
39
|
+
dc_param[param] = s2c.get_class_with_defaults(schema_section[param]["type"], schema_json=schema)
|
|
40
|
+
dc_param["schema"][param] = schema_section[param]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_default_config_from_schema(path_to_schema,
|
|
44
|
+
schema_node=True,
|
|
45
|
+
as_rod=False,
|
|
46
|
+
output_filename=None):
|
|
47
|
+
"""
|
|
48
|
+
This returns a default config object as defined from reading a schema file.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
output_filename (str): if not None, the path to write the loaded config to
|
|
52
|
+
"""
|
|
53
|
+
default_config = {"parameters": dict()}
|
|
54
|
+
dc_param = default_config["parameters"]
|
|
55
|
+
dc_param["schema"] = dict()
|
|
56
|
+
|
|
57
|
+
with open(path_to_schema) as fid01:
|
|
58
|
+
schema = json.load(fid01)
|
|
59
|
+
|
|
60
|
+
for group in schema["config"]:
|
|
61
|
+
_set_defaults_for_schema_group(dc_param, schema["config"][group], schema)
|
|
62
|
+
|
|
63
|
+
if not schema_node:
|
|
64
|
+
print("Removing schema node.")
|
|
65
|
+
dc_param.pop("schema")
|
|
66
|
+
|
|
67
|
+
if as_rod:
|
|
68
|
+
default_config = json.loads(json.dumps(default_config), object_hook=s2c.ReadOnlyDict)
|
|
69
|
+
|
|
70
|
+
if output_filename is not None:
|
|
71
|
+
with open(output_filename, "w") as outfile:
|
|
72
|
+
json.dump(default_config, outfile, sort_keys=True, indent=4)
|
|
73
|
+
print(f"Wrote '{output_filename}' file.")
|
|
74
|
+
|
|
75
|
+
return default_config
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_default_from_schema(path_to_schema,
|
|
79
|
+
output_filename='default_config.json',
|
|
80
|
+
schema_node=True):
|
|
81
|
+
"""
|
|
82
|
+
DEPRECATED: This function simply calls get_default_config_from_schema with specific arguments.
|
|
83
|
+
|
|
84
|
+
This function writes out a default config file as defined from reading a schema file.
|
|
85
|
+
It's as good as the schema it's given. Note that this is designed to work with a schema from
|
|
86
|
+
a disease-specific build, otherwise it may contain a lot of params from other disease types.
|
|
87
|
+
"""
|
|
88
|
+
warnings.warn("Calls to write_default_from_schema() should be updated to use get_default_config_from_schema()",
|
|
89
|
+
DeprecationWarning)
|
|
90
|
+
get_default_config_from_schema(path_to_schema=path_to_schema, output_filename=output_filename,
|
|
91
|
+
schema_node=schema_node)
|
|
92
|
+
return output_filename
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_default_config_as_rod(config) -> s2c.ReadOnlyDict:
|
|
96
|
+
"""
|
|
97
|
+
Parameters:
|
|
98
|
+
config (string/path): path to default or base config.json
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
config (as ReadOnlyDict) with schema ready for schema-verified param sets.
|
|
102
|
+
"""
|
|
103
|
+
if not os.path.exists(config):
|
|
104
|
+
print(f"{config} not found.")
|
|
105
|
+
return None
|
|
106
|
+
config_rod = None
|
|
107
|
+
with open(config) as conf:
|
|
108
|
+
config_rod = json.load(conf, object_hook=s2c.ReadOnlyDict)
|
|
109
|
+
return config_rod
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_config_from_default_and_params(config_path: Union[str, os.PathLike, None] = None,
|
|
113
|
+
set_fn=None,
|
|
114
|
+
config: s2c.ReadOnlyDict = None,
|
|
115
|
+
verbose: bool = False) -> s2c.ReadOnlyDict:
|
|
116
|
+
"""
|
|
117
|
+
Use this function to create a valid config.json file from a schema-derived
|
|
118
|
+
base config, a callback that sets your parameters of interest
|
|
119
|
+
|
|
120
|
+
Parameters:
|
|
121
|
+
config_path (string/path): Path to valid config.json
|
|
122
|
+
set_fn (function): Callback that sets params with implicit schema enforcement.
|
|
123
|
+
config: Read-only dict configuration object. Pass this XOR the config_path.
|
|
124
|
+
verbose: Flag to print debug statements
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
config: read-only dict
|
|
128
|
+
"""
|
|
129
|
+
if not ((config_path is None) ^ (config is None)):
|
|
130
|
+
raise Exception('Must specify either a default config_path or config, not neither or both.')
|
|
131
|
+
|
|
132
|
+
if verbose:
|
|
133
|
+
print(f"DEBUG: get_config_from_default_and_params invoked with "
|
|
134
|
+
f"config_path: {config_path}, config: {config is not None}, {set_fn}.")
|
|
135
|
+
|
|
136
|
+
# load default config from file if a path was given
|
|
137
|
+
if config_path is not None:
|
|
138
|
+
config = load_default_config_as_rod(config_path)
|
|
139
|
+
if verbose:
|
|
140
|
+
print("DEBUG: Calling set_fn.")
|
|
141
|
+
|
|
142
|
+
# now that we have a config (either given or loaded from file), call the (possibly given) callback on it
|
|
143
|
+
if set_fn is not None:
|
|
144
|
+
config = set_fn(config)
|
|
145
|
+
|
|
146
|
+
return config
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def write_config_from_default_and_params(config_path,
|
|
150
|
+
set_fn,
|
|
151
|
+
config_out_path: Union[str, os.PathLike],
|
|
152
|
+
verbose: bool = False):
|
|
153
|
+
"""
|
|
154
|
+
Use this function to create a valid config.json file from a schema-derived
|
|
155
|
+
base config, a callback that sets your parameters of interest, and an output path.
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
config_path (string/path): Path to valid config.json
|
|
159
|
+
set_fn (function): Callback that sets params with implicit schema enforcement
|
|
160
|
+
config_out_path: Path to write new config.json
|
|
161
|
+
verbose: Flag to print debug statements
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
if verbose:
|
|
167
|
+
print(f"DEBUG: write_config_from_default_and_params invoked with {config_path}, {set_fn}, and {config_out_path}.")
|
|
168
|
+
config = get_config_from_default_and_params(config_path=config_path, set_fn=set_fn)
|
|
169
|
+
|
|
170
|
+
if verbose:
|
|
171
|
+
print("DEBUG: Calling finalize.")
|
|
172
|
+
config.parameters.finalize()
|
|
173
|
+
|
|
174
|
+
if verbose:
|
|
175
|
+
print("DEBUG: Writing output file.")
|
|
176
|
+
with open(config_out_path, "w") as outfile:
|
|
177
|
+
json.dump(config, outfile, sort_keys=True, indent=4)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_json(filepath, post_process=None, ignore_notfound=True):
|
|
10
|
+
"""Load json from a file, with optional post processing of contents prior to parsing"""
|
|
11
|
+
if os.path.exists(filepath):
|
|
12
|
+
try:
|
|
13
|
+
with open(filepath, 'r', encoding='utf-8') as json_file:
|
|
14
|
+
if post_process:
|
|
15
|
+
return json.loads(post_process(json_file.read()))
|
|
16
|
+
else:
|
|
17
|
+
return json.loads(json_file.read())
|
|
18
|
+
except ValueError:
|
|
19
|
+
print(f"JSON decode error from file {filepath} ")
|
|
20
|
+
raise
|
|
21
|
+
except IOError:
|
|
22
|
+
print(f"Error accessing json file {filepath} ")
|
|
23
|
+
raise
|
|
24
|
+
else:
|
|
25
|
+
if not ignore_notfound:
|
|
26
|
+
# should this raise an error?
|
|
27
|
+
print(f"JSON file not found: {filepath}")
|
|
28
|
+
raise ValueError
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _recursive_json_overrider(ref_json, flat_input_json):
|
|
33
|
+
"""
|
|
34
|
+
Useful function that recursively navigates a pretty arbitrarily structured json config looking
|
|
35
|
+
for key-value parameters in the leaves.
|
|
36
|
+
"""
|
|
37
|
+
special_nodes = ["Vector_Species_Params", "Malaria_Drug_Params", "TB_Drug_Params", "HIV_Drug_Params", "STI_Network_Params_By_Property", "TBHIV_Drug_Params"]
|
|
38
|
+
if ref_json is None:
|
|
39
|
+
print("Null ref_json (param1) passed into _recursive_json_overrider.")
|
|
40
|
+
raise ValueError
|
|
41
|
+
|
|
42
|
+
for val in ref_json:
|
|
43
|
+
# if not leaf, call recursive_json_leaf_reader
|
|
44
|
+
if isinstance(ref_json[val], dict) and val not in special_nodes:
|
|
45
|
+
_recursive_json_overrider(ref_json[val], flat_input_json)
|
|
46
|
+
# do VSP and MDP as special case. Sigh sigh sigh. Also TBHIV params now, also sigh.
|
|
47
|
+
elif val in special_nodes:
|
|
48
|
+
# could "genericize" this if we need to... happens to work for now, since both VSP and MDP are 3-levels deep...
|
|
49
|
+
if val not in flat_input_json:
|
|
50
|
+
flat_input_json[val] = {}
|
|
51
|
+
elif val == "STI_Network_Params_By_Property" and not ("NONE" in flat_input_json[val].keys()):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
for species in ref_json[val]:
|
|
55
|
+
if species not in flat_input_json[val]:
|
|
56
|
+
if (isinstance(ref_json[val][species], dict)):
|
|
57
|
+
flat_input_json[val][species] = {}
|
|
58
|
+
for param in ref_json[val][species]:
|
|
59
|
+
if (isinstance(ref_json[val][species], dict)):
|
|
60
|
+
if param not in flat_input_json[val][species]:
|
|
61
|
+
flat_input_json[val][species][param] = ref_json[val][species][param]
|
|
62
|
+
else:
|
|
63
|
+
flat_input_json[val][species] = ref_json[val][species]
|
|
64
|
+
else:
|
|
65
|
+
if val not in flat_input_json:
|
|
66
|
+
flat_input_json[val] = ref_json[val]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def flattenConfig(configjson_path, new_config_name="config.json", use_full_out_path=False):
|
|
70
|
+
"""
|
|
71
|
+
Historically called 'flattening' but really a function that takes a parameter override
|
|
72
|
+
json config that includes a Default_Config_Path and produces a config.json from the two.
|
|
73
|
+
"""
|
|
74
|
+
if not os.path.exists(configjson_path):
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
configjson_flat = {}
|
|
78
|
+
configjson = _load_json(configjson_path)
|
|
79
|
+
|
|
80
|
+
_recursive_json_overrider(configjson, configjson_flat)
|
|
81
|
+
|
|
82
|
+
# get defaults from config.json and synthesize output from default and overrides
|
|
83
|
+
if "Default_Config_Path" in configjson_flat:
|
|
84
|
+
default_config_path = configjson_flat["Default_Config_Path"]
|
|
85
|
+
stripped_path = default_config_path.strip()
|
|
86
|
+
if stripped_path != default_config_path:
|
|
87
|
+
print(f"Warning: config parameter 'Default_Config_Path' has leading or trailing whitespace in value \"{default_config_path}\"."
|
|
88
|
+
f" Trimming whitespace and continuing.")
|
|
89
|
+
default_config_path = stripped_path
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# This code has always worked by treating the default_configpath as relative the Regression directory.
|
|
93
|
+
# No longer doing that, but preserving that capability for back-compat. Going forward, relative to the
|
|
94
|
+
# configjson_path.
|
|
95
|
+
simdir = Path(configjson_path).parent
|
|
96
|
+
default_config_json = None
|
|
97
|
+
if Path(os.path.join(str(simdir), default_config_path)).exists():
|
|
98
|
+
default_config_json = _load_json(os.path.join(str(simdir), default_config_path))
|
|
99
|
+
else:
|
|
100
|
+
default_config_json = _load_json(os.path.join('.', default_config_path))
|
|
101
|
+
_recursive_json_overrider(default_config_json, configjson_flat)
|
|
102
|
+
except Exception as ex:
|
|
103
|
+
print(f"Exception opening default config {default_config_path}: {ex}.")
|
|
104
|
+
raise ex
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
print(f"Didn't find 'Default_Config_Path' in '{configjson_path}'")
|
|
108
|
+
raise RuntimeError("Bad Default_Config_Path!!!")
|
|
109
|
+
|
|
110
|
+
# still need that parameter top level node
|
|
111
|
+
configjson = {}
|
|
112
|
+
configjson["parameters"] = configjson_flat
|
|
113
|
+
|
|
114
|
+
# don't need backslashes in the default config path
|
|
115
|
+
# messing with anything else downstream now that it is flattened
|
|
116
|
+
if "Default_Config_Path" in configjson["parameters"]:
|
|
117
|
+
configjson["parameters"].pop("Default_Config_Path")
|
|
118
|
+
|
|
119
|
+
# let's write out a flat version in case someone wants
|
|
120
|
+
# to use regression examples as configs for debug mode
|
|
121
|
+
outfile = new_config_name
|
|
122
|
+
if not use_full_out_path:
|
|
123
|
+
outfile = configjson_path.replace(os.path.basename(configjson_path), new_config_name)
|
|
124
|
+
|
|
125
|
+
with open(outfile, 'w') as fid01:
|
|
126
|
+
json.dump(configjson, fid01, sort_keys=True, indent=4)
|
|
127
|
+
|
|
128
|
+
return configjson
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == '__main__':
|
|
132
|
+
if len(sys.argv) > 1:
|
|
133
|
+
flattenConfig(sys.argv[1])
|
|
134
|
+
else:
|
|
135
|
+
print('usage:', sys.argv[0], 'configFile')
|
|
File without changes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import emod_api.demographics.demographic_exceptions as demog_ex
|
|
2
|
+
|
|
3
|
+
from emod_api.demographics.updateable import Updateable
|
|
4
|
+
from emod_api.utils import check_dimensionality
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgeDistribution(Updateable):
|
|
8
|
+
def __init__(self,
|
|
9
|
+
ages_years: list[float],
|
|
10
|
+
cumulative_population_fraction: list[float]):
|
|
11
|
+
"""
|
|
12
|
+
A cumulative population age distribution in fraction units 0 to 1. This is used as part of initializing the
|
|
13
|
+
population in an EMOD simulation.
|
|
14
|
+
|
|
15
|
+
The AgeDistribution provides a probability each agent at the beginning of a simulation will be
|
|
16
|
+
initialized with a given age. A uniform random number is drawn for each agent and checked against the
|
|
17
|
+
provided cumulative population fractions directly, with the corresponding age entry selected for the agent. If
|
|
18
|
+
the drawn number lies between two values, the selected agent age is linearly interpolated from the two closest
|
|
19
|
+
corresponding ages. If the drawn number lies beyond the provided cumulative population fraction range, the
|
|
20
|
+
closest corresponding age will be selected.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
ages_years: (list[float]) A list of ages (in years) that population fraction data will be provided for.
|
|
24
|
+
Must be a list of monotonically increasing floats within range 0 <= age <= 200 .
|
|
25
|
+
cumulative_population_fraction: (list[float]) A list of cumulative population fractions corresponding to
|
|
26
|
+
the provided ages_years list. Must be a list of monotonically increasing floats within range
|
|
27
|
+
0 <= fraction <= 1 .
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
ages_years: [5, 10, 20, 50, 100]
|
|
31
|
+
cumulative_population_fraction: [0.1, 0.2, 0.5, 0.8, 1.0]
|
|
32
|
+
|
|
33
|
+
Uniform random number draw: 0.8
|
|
34
|
+
Selected age: 50 years
|
|
35
|
+
Uniform random number draw: 0.35
|
|
36
|
+
Selected age: 10 + (0.35 - 0.2) * ((20-10) / (0.5-0.2)) = 15 years
|
|
37
|
+
Uniform random number draw: 0.05 (beyond provided fraction range)
|
|
38
|
+
Selected age: 1 year (nearest corresponding age)
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.ages_years = ages_years
|
|
42
|
+
self.cumulative_population_fraction = cumulative_population_fraction
|
|
43
|
+
# This will convert the object to an age distribution dictionary and then validate it reporting object-relevant
|
|
44
|
+
# messages
|
|
45
|
+
self._validate(distribution_dict=self.to_dict(validate=False), source_is_dict=False)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _rate_scale_factor(cls):
|
|
49
|
+
return 365.0 # convert ages in years to days
|
|
50
|
+
|
|
51
|
+
def to_dict(self, validate: bool = True) -> dict:
|
|
52
|
+
distribution_dict = {
|
|
53
|
+
'ResultValues': self.ages_years,
|
|
54
|
+
'DistributionValues': self.cumulative_population_fraction,
|
|
55
|
+
'ResultScaleFactor': self._rate_scale_factor()
|
|
56
|
+
}
|
|
57
|
+
if validate:
|
|
58
|
+
self._validate(distribution_dict=distribution_dict, source_is_dict=False)
|
|
59
|
+
return distribution_dict
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, distribution_dict: dict):
|
|
63
|
+
cls._validate(distribution_dict=distribution_dict, source_is_dict=True)
|
|
64
|
+
return cls(ages_years=distribution_dict['ResultValues'],
|
|
65
|
+
cumulative_population_fraction=distribution_dict['DistributionValues'])
|
|
66
|
+
|
|
67
|
+
_validation_messages = {
|
|
68
|
+
'fixed_value_check': {
|
|
69
|
+
True: "key: {0} value: {1} does not match expected value: {2}",
|
|
70
|
+
False: None # These are all properties of the obj and cannot be made invalid
|
|
71
|
+
},
|
|
72
|
+
'data_dimensionality_check_ages': {
|
|
73
|
+
True: 'ResultValues must be a 1-d array of floats',
|
|
74
|
+
False: 'ages_years must be a 1-d array of floats'
|
|
75
|
+
},
|
|
76
|
+
'data_dimensionality_check_distributions': {
|
|
77
|
+
True: 'DistributionValues must be a 1-d array of floats',
|
|
78
|
+
False: 'cumulative_population_fraction must be a 1-d array of floats'
|
|
79
|
+
},
|
|
80
|
+
'age_and_distribution_length_check': {
|
|
81
|
+
True: 'ResultValues and DistributionValues must be the same length but are not',
|
|
82
|
+
False: 'ages_years and cumulative_population_fraction must be the same length but are not'
|
|
83
|
+
},
|
|
84
|
+
'age_range_check': {
|
|
85
|
+
True: "ResultValues age values must be: 0 <= age <= 200 in years. Out-of-range index:values : {0}",
|
|
86
|
+
False: "All ages_years values must be: 0 <= age <= 200 in years. Out-of-range index:values : {0}"
|
|
87
|
+
},
|
|
88
|
+
'distribution_range_check': {
|
|
89
|
+
True: "DistributionValues cumulative fractions must be: 0 <= fraction <= 1. "
|
|
90
|
+
"Out-of-range index:values : {0}",
|
|
91
|
+
False: "All cumulative_population_fraction values must be: 0 <= fraction <= 1. "
|
|
92
|
+
"Out-of-range index:values : {0}"
|
|
93
|
+
},
|
|
94
|
+
'age_monotonicity_check': {
|
|
95
|
+
True: "ResultValues ages in years must monotonically increase but do not, index: {0} value: {1}",
|
|
96
|
+
False: "ages_years values must monotonically increase but do not, index: {0} value: {1}"
|
|
97
|
+
},
|
|
98
|
+
'distribution_monotonicity_check': {
|
|
99
|
+
True: "DistributionValues cumulative fractions must monotonically increase but do not, index: {0} value: {1}",
|
|
100
|
+
False: "cumulative_population_fraction values must monotonically increase but do not, index: {0} value: {1}"
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def _validate(cls, distribution_dict: dict, source_is_dict: bool):
|
|
106
|
+
"""
|
|
107
|
+
Validate an AgeDistribution in dict form
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
distribution_dict: (dict) the age distribution dict to validate
|
|
111
|
+
source_is_dict: (bool) If true, report dict-relevant error messages. If false, report obj-relevant messages.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Nothing
|
|
115
|
+
"""
|
|
116
|
+
if source_is_dict is True:
|
|
117
|
+
expected_values = {
|
|
118
|
+
'ResultScaleFactor': cls._rate_scale_factor()
|
|
119
|
+
}
|
|
120
|
+
for key, expected_value in expected_values.items():
|
|
121
|
+
value = distribution_dict[key]
|
|
122
|
+
if value != expected_value:
|
|
123
|
+
message = cls._validation_messages['fixed_value_check'][source_is_dict].format(key, value, expected_value)
|
|
124
|
+
raise demog_ex.InvalidFixedValueException(message)
|
|
125
|
+
|
|
126
|
+
# ensure the ages and distribution values are both 1-d iterables of the same length
|
|
127
|
+
ages = distribution_dict['ResultValues']
|
|
128
|
+
distribution_values = distribution_dict['DistributionValues']
|
|
129
|
+
|
|
130
|
+
is_1d = check_dimensionality(data=ages, dimensionality=1)
|
|
131
|
+
if not is_1d:
|
|
132
|
+
message = cls._validation_messages['data_dimensionality_check_ages'][source_is_dict]
|
|
133
|
+
raise demog_ex.InvalidDataDimensionality(message)
|
|
134
|
+
is_1d = check_dimensionality(data=distribution_values, dimensionality=1)
|
|
135
|
+
if not is_1d:
|
|
136
|
+
message = cls._validation_messages['data_dimensionality_check_distributions'][source_is_dict]
|
|
137
|
+
raise demog_ex.InvalidDataDimensionality(message)
|
|
138
|
+
|
|
139
|
+
if len(ages) != len(distribution_values):
|
|
140
|
+
message = cls._validation_messages['age_and_distribution_length_check'][source_is_dict]
|
|
141
|
+
raise demog_ex.InvalidDataDimensionLength(message)
|
|
142
|
+
|
|
143
|
+
# ensure the age and distribution value lists are ascending and in reasonable ranges
|
|
144
|
+
out_of_range = [f"{index}:{age}" for index, age in enumerate(ages) if (age < 0) or (age > 200)]
|
|
145
|
+
if len(out_of_range) > 0:
|
|
146
|
+
oor_str = ', '.join(out_of_range)
|
|
147
|
+
message = cls._validation_messages['age_range_check'][source_is_dict].format(oor_str)
|
|
148
|
+
raise demog_ex.AgeOutOfRangeException(message)
|
|
149
|
+
out_of_range = [f"{index}:{value}" for index, value in enumerate(distribution_values)
|
|
150
|
+
if (value < 0) or (value > 1)]
|
|
151
|
+
if len(out_of_range) > 0:
|
|
152
|
+
oor_str = ', '.join(out_of_range)
|
|
153
|
+
message = cls._validation_messages['distribution_range_check'][source_is_dict].format(oor_str)
|
|
154
|
+
raise demog_ex.DistributionOutOfRangeException(message)
|
|
155
|
+
|
|
156
|
+
for i in range(1, len(ages)):
|
|
157
|
+
if ages[i] - ages[i - 1] <= 0:
|
|
158
|
+
message = cls._validation_messages['age_monotonicity_check'][source_is_dict].format(i, ages[i])
|
|
159
|
+
raise demog_ex.NonMonotonicAgeException(message)
|
|
160
|
+
for i in range(1, len(distribution_values)):
|
|
161
|
+
if distribution_values[i] - distribution_values[i - 1] <= 0:
|
|
162
|
+
message = cls._validation_messages['distribution_monotonicity_check'][source_is_dict].format(i, distribution_values[i])
|
|
163
|
+
raise demog_ex.NonMonotonicDistributionException(message)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
import getpass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseInputFile:
|
|
8
|
+
__metaclass__ = ABCMeta
|
|
9
|
+
|
|
10
|
+
DEFAULT_ID_REFERENCE = "default_id_reference"
|
|
11
|
+
|
|
12
|
+
def __init__(self, idref: str = None):
|
|
13
|
+
self.idref = self.DEFAULT_ID_REFERENCE if idref is None else idref
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def generate_file(self, name):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def generate_headers(self, extra=None):
|
|
20
|
+
meta = {
|
|
21
|
+
"DateCreated": datetime.today().strftime("%m/%d/%Y"),
|
|
22
|
+
"Tool": "emod-api",
|
|
23
|
+
"Author": getpass.getuser(), # LocalOS.username,
|
|
24
|
+
"IdReference": self.idref,
|
|
25
|
+
"NodeCount": 0,
|
|
26
|
+
}
|
|
27
|
+
meta.update(extra or {})
|
|
28
|
+
return meta
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from scipy import sparse as sp
|
|
7
|
+
from scipy.sparse import linalg as la
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from emod_api.demographics.age_distribution import AgeDistribution
|
|
11
|
+
from emod_api.demographics.mortality_distribution import MortalityDistribution
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_equilibrium_age_distribution(birth_rate: float = 40.0, mortality_rate: float = 20.0) -> AgeDistribution:
|
|
15
|
+
"""
|
|
16
|
+
Create an AgeDistribution object representing an equilibrium for birth and mortality rates.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
birth_rate: (float) The birth rate in units of births/year/1000-women
|
|
20
|
+
mortality_rate: (float) The mortality rate in units of deaths/year/1000 people
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
an AgeDistribution object
|
|
24
|
+
"""
|
|
25
|
+
from emod_api.demographics.age_distribution import AgeDistribution
|
|
26
|
+
|
|
27
|
+
# convert to daily rate per person, EMOD units
|
|
28
|
+
birth_rate = (birth_rate / 1000) / 365 # what is actually used below
|
|
29
|
+
mortality_rate = (mortality_rate / 1000) / 365 # what is actually used below
|
|
30
|
+
|
|
31
|
+
birth_rate = math.log(1 + birth_rate)
|
|
32
|
+
mortality_rate = -1 * math.log(1 - mortality_rate)
|
|
33
|
+
|
|
34
|
+
# It is important for the age distribution computation that the age-spacing be very fine; I've used 30 days here.
|
|
35
|
+
# With coarse spacing, the computation in practice doesn't work as well.
|
|
36
|
+
age_dist_tuple = _computeAgeDist(birth_rate, [i * 30 for i in range(1200)], 1200 * [mortality_rate], 12 * [1.0])
|
|
37
|
+
|
|
38
|
+
# The final demographics file, though, can use coarser binning interpolated from the finely-spaced computed distribution.
|
|
39
|
+
age_bins = list(range(16)) + [20 + 5 * i for i in range(14)]
|
|
40
|
+
cum_pop_fraction = np.interp(age_bins, [i / 365 for i in age_dist_tuple[2]], age_dist_tuple[1]).tolist()
|
|
41
|
+
age_bins.extend([90])
|
|
42
|
+
cum_pop_fraction.extend([1.0])
|
|
43
|
+
distribution = AgeDistribution(ages_years=age_bins, cumulative_population_fraction=cum_pop_fraction)
|
|
44
|
+
return distribution
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _computeAgeDist(bval, mvecX, mvecY, fVec, max_yr=90):
|
|
48
|
+
"""
|
|
49
|
+
Compute equilibrium age distribution given age-specific mortality and crude birth rates
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
bval: crude birth rate in births per day per person
|
|
53
|
+
mvecX: list of age bins in days
|
|
54
|
+
mvecY: List of per day mortality rate for the age bins
|
|
55
|
+
fVec: Seasonal forcing per month
|
|
56
|
+
max_yr : maximum agent age in years
|
|
57
|
+
|
|
58
|
+
returns EquilibPopulationGrowthRate, MonthlyAgeDist, MonthlyAgeBins
|
|
59
|
+
author: Kurt Frey
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
bin_size = 30
|
|
63
|
+
day_to_year = 365
|
|
64
|
+
|
|
65
|
+
# Age brackets
|
|
66
|
+
avecY = np.arange(0, max_yr * day_to_year, bin_size) - 1
|
|
67
|
+
|
|
68
|
+
# Mortality sampling
|
|
69
|
+
mvecX = [-1] + mvecX + [max_yr * day_to_year + 1]
|
|
70
|
+
mvecY = [mvecY[0]] + mvecY + [mvecY[-1]]
|
|
71
|
+
mX = np.arange(0, max_yr * day_to_year, bin_size)
|
|
72
|
+
mX[0] = 1
|
|
73
|
+
mval = 1.0 - np.interp(mX, xp=mvecX, fp=mvecY)
|
|
74
|
+
r_n = mval.size
|
|
75
|
+
|
|
76
|
+
# Matrix construction
|
|
77
|
+
BmatRC = (np.zeros(r_n), np.arange(r_n))
|
|
78
|
+
Bmat = sp.csr_matrix(([bval * bin_size] * r_n, BmatRC), shape=(r_n, r_n))
|
|
79
|
+
Mmat = sp.spdiags(mval[:-1] ** bin_size, -1, r_n, r_n)
|
|
80
|
+
Dmat = Bmat + Mmat
|
|
81
|
+
|
|
82
|
+
# Math
|
|
83
|
+
(gR, popVec) = la.eigs(Dmat, k=1, sigma=1.0)
|
|
84
|
+
gR = np.abs(gR ** (float(day_to_year) / float(bin_size)))
|
|
85
|
+
popVec = np.abs(popVec) / np.sum(np.abs(popVec))
|
|
86
|
+
|
|
87
|
+
# Apply seasonal forcing
|
|
88
|
+
mVecR = [-2.0, 30.5, 30.6, 60.5, 60.6, 91.5, 91.6, 121.5,
|
|
89
|
+
121.6, 152.5, 152.6, 183.5, 183.6, 213.5, 213.6, 244.5,
|
|
90
|
+
245.6, 274.5, 274.6, 305.5, 305.6, 333.5, 335.6, 364.5]
|
|
91
|
+
fVec = np.flipud([val for val in fVec for _ in (0, 1)])
|
|
92
|
+
wfVec = np.array([np.mean(np.interp(np.mod(range(val + 1, val + 31), 365),
|
|
93
|
+
xp=mVecR, fp=fVec)) for val in avecY]).reshape(-1, 1)
|
|
94
|
+
popVec = popVec * wfVec / np.sum(popVec * wfVec)
|
|
95
|
+
|
|
96
|
+
# Age sampling
|
|
97
|
+
avecY[0] = 0
|
|
98
|
+
avecX = np.clip(np.around(np.cumsum(popVec), decimals=7), 0.0, 1.0)
|
|
99
|
+
avecX = np.insert(avecX, 0, np.zeros(1))
|
|
100
|
+
|
|
101
|
+
return gR.tolist()[0], avecX[:-1].tolist(), avecY.tolist()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def generate_mortality_over_time_from_data(data_csv: Union[str, os.PathLike],
|
|
105
|
+
base_year: int) -> MortalityDistribution:
|
|
106
|
+
"""
|
|
107
|
+
Generate a MortalityDistribution object from a data csv file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
data_csv: Path to csv file with the mortality rates by calendar year and age bucket.
|
|
111
|
+
base_year: The calendar year the sim is treating as the base.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
a MortalityDistribution object.
|
|
115
|
+
"""
|
|
116
|
+
if base_year < 0:
|
|
117
|
+
raise ValueError(f"User passed negative value of base_year: {base_year}.")
|
|
118
|
+
if base_year > 2050:
|
|
119
|
+
raise ValueError(f"User passed too large value of base_year: {base_year}.")
|
|
120
|
+
|
|
121
|
+
# Load csv. Convert rate arrays into DTK-compatiable JSON structures.
|
|
122
|
+
rates = [] # array of arrays, but leave that for a minute
|
|
123
|
+
df = pd.read_csv(data_csv)
|
|
124
|
+
header = df.columns
|
|
125
|
+
year_start = int(header[1]) # someone's going to come along with 1990.5, etc. Sigh.
|
|
126
|
+
year_end = int(header[-1])
|
|
127
|
+
if year_end <= year_start:
|
|
128
|
+
raise ValueError(f"Failed check that {year_end} is greater than {year_start} in csv dataset.")
|
|
129
|
+
num_years = year_end - year_start + 1
|
|
130
|
+
rel_years = list()
|
|
131
|
+
for year in range(year_start, year_start + num_years):
|
|
132
|
+
mort_data = list(df[str(year)])
|
|
133
|
+
rel_years.append(year - base_year)
|
|
134
|
+
|
|
135
|
+
age_key = None
|
|
136
|
+
for trykey in df.keys():
|
|
137
|
+
if trykey.lower().startswith("age"):
|
|
138
|
+
age_key = trykey
|
|
139
|
+
raw_age_bins = list(df[age_key])
|
|
140
|
+
|
|
141
|
+
if age_key is None:
|
|
142
|
+
raise ValueError("Failed to find 'Age_Bin' (or similar) column in the csv dataset. Cannot process.")
|
|
143
|
+
|
|
144
|
+
age_bins = list()
|
|
145
|
+
try:
|
|
146
|
+
for age_bin in raw_age_bins:
|
|
147
|
+
left_age = float(age_bin.split("-")[0])
|
|
148
|
+
age_bins.append(left_age)
|
|
149
|
+
|
|
150
|
+
except Exception as ex:
|
|
151
|
+
raise ValueError(f"Ran into error processing the values in the Age-Bin column. {ex}")
|
|
152
|
+
|
|
153
|
+
for idx in range(len(age_bins)): # 18 of these
|
|
154
|
+
# mort_data is the array of mortality rates (by year bin) for age_bin
|
|
155
|
+
mort_data = list(df.transpose()[idx][1:])
|
|
156
|
+
rates.append(mort_data) # 28 of these, 1 for each year, eg
|
|
157
|
+
|
|
158
|
+
distribution = MortalityDistribution(ages_years=age_bins, mortality_rate_matrix=rates, calendar_years=rel_years)
|
|
159
|
+
return distribution
|