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.
Files changed (71) hide show
  1. emod_api/__init__.py +1 -0
  2. emod_api/campaign.py +170 -0
  3. emod_api/channelreports/__init__.py +0 -0
  4. emod_api/channelreports/channels.py +433 -0
  5. emod_api/channelreports/icj_to_csv.py +65 -0
  6. emod_api/channelreports/plot_icj_means.py +149 -0
  7. emod_api/channelreports/plot_prop_report.py +205 -0
  8. emod_api/channelreports/utils.py +326 -0
  9. emod_api/config/__init__.py +0 -0
  10. emod_api/config/default_from_schema.py +16 -0
  11. emod_api/config/default_from_schema_no_validation.py +177 -0
  12. emod_api/config/from_overrides.py +135 -0
  13. emod_api/demographics/__init__.py +0 -0
  14. emod_api/demographics/age_distribution.py +163 -0
  15. emod_api/demographics/base_input_file.py +28 -0
  16. emod_api/demographics/calculators.py +159 -0
  17. emod_api/demographics/demographic_exceptions.py +54 -0
  18. emod_api/demographics/demographics.py +249 -0
  19. emod_api/demographics/demographics_base.py +752 -0
  20. emod_api/demographics/demographics_overlay.py +41 -0
  21. emod_api/demographics/fertility_distribution.py +235 -0
  22. emod_api/demographics/implicit_functions.py +112 -0
  23. emod_api/demographics/mortality_distribution.py +227 -0
  24. emod_api/demographics/node.py +456 -0
  25. emod_api/demographics/overlay_node.py +16 -0
  26. emod_api/demographics/properties_and_attributes.py +737 -0
  27. emod_api/demographics/service/__init__.py +0 -0
  28. emod_api/demographics/service/grid_construction.py +143 -0
  29. emod_api/demographics/service/service.py +55 -0
  30. emod_api/demographics/susceptibility_distribution.py +170 -0
  31. emod_api/demographics/updateable.py +58 -0
  32. emod_api/legacy/__init__.py +0 -0
  33. emod_api/legacy/plotAllCharts.py +230 -0
  34. emod_api/migration/__init__.py +0 -0
  35. emod_api/migration/__main__.py +22 -0
  36. emod_api/migration/migration.py +782 -0
  37. emod_api/multidim_plotter.py +80 -0
  38. emod_api/schema_to_class.py +440 -0
  39. emod_api/serialization/__init__.py +0 -0
  40. emod_api/serialization/census_and_mod_pop.py +48 -0
  41. emod_api/serialization/dtk_file_support.py +61 -0
  42. emod_api/serialization/dtk_file_tools.py +1378 -0
  43. emod_api/serialization/dtk_file_utility.py +141 -0
  44. emod_api/serialization/serialized_population.py +205 -0
  45. emod_api/spatialreports/__init__.py +0 -0
  46. emod_api/spatialreports/__main__.py +67 -0
  47. emod_api/spatialreports/plot_spat_means.py +99 -0
  48. emod_api/spatialreports/spatial.py +210 -0
  49. emod_api/utils/__init__.py +26 -0
  50. emod_api/utils/distributions/__init__.py +0 -0
  51. emod_api/utils/distributions/base_distribution.py +38 -0
  52. emod_api/utils/distributions/bimodal_distribution.py +64 -0
  53. emod_api/utils/distributions/constant_distribution.py +58 -0
  54. emod_api/utils/distributions/demographic_distribution_flag.py +16 -0
  55. emod_api/utils/distributions/distribution_type.py +15 -0
  56. emod_api/utils/distributions/dual_constant_distribution.py +68 -0
  57. emod_api/utils/distributions/dual_exponential_distribution.py +75 -0
  58. emod_api/utils/distributions/exponential_distribution.py +63 -0
  59. emod_api/utils/distributions/gaussian_distribution.py +69 -0
  60. emod_api/utils/distributions/log_normal_distribution.py +61 -0
  61. emod_api/utils/distributions/poisson_distribution.py +59 -0
  62. emod_api/utils/distributions/uniform_distribution.py +70 -0
  63. emod_api/utils/distributions/weibull_distribution.py +69 -0
  64. emod_api/utils/str_enum.py +6 -0
  65. emod_api/weather/__init__.py +0 -0
  66. emod_api/weather/weather.py +428 -0
  67. emod_api-3.0.2.dist-info/METADATA +131 -0
  68. emod_api-3.0.2.dist-info/RECORD +71 -0
  69. emod_api-3.0.2.dist-info/WHEEL +5 -0
  70. emod_api-3.0.2.dist-info/licenses/LICENSE +21 -0
  71. 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