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,782 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from functools import partial
|
|
4
|
+
import json
|
|
5
|
+
from numbers import Integral
|
|
6
|
+
from os import environ, SEEK_SET
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from platform import system
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import csv
|
|
13
|
+
|
|
14
|
+
from emod_api.demographics.demographics import Demographics
|
|
15
|
+
|
|
16
|
+
# for from_demog_and_param_gravity()
|
|
17
|
+
from geographiclib.geodesic import Geodesic
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Layer(dict):
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
The Layer object represents a mapping from source node (IDs) to destination node (IDs) for a particular
|
|
24
|
+
age, gender, age+gender combination, or all users if no age or gender dependence. Users will not generally
|
|
25
|
+
interact directly with Layer objects.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def DatavalueCount(self) -> int:
|
|
36
|
+
"""Get (maximum) number of data values for any node in this layer
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Maximum number of data values for any node in this layer
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
count = max([len(entry) for entry in self.values()]) if len(self) else 0
|
|
43
|
+
return count
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def NodeCount(self) -> int:
|
|
47
|
+
"""Get the number of (source) nodes with rates in this layer
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Number of (source) nodes with rates in this layer
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
return len(self)
|
|
54
|
+
|
|
55
|
+
# @property
|
|
56
|
+
# def Nodes(self) -> dict:
|
|
57
|
+
# return self._nodes
|
|
58
|
+
|
|
59
|
+
def __getitem__(self,
|
|
60
|
+
key: int) -> dict:
|
|
61
|
+
"""Allows indexing directly into this object with source node id
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
key: source node id
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dictionary of outbound rates for the given node id
|
|
68
|
+
"""
|
|
69
|
+
if key not in self:
|
|
70
|
+
if isinstance(key, Integral):
|
|
71
|
+
super().__setitem__(key, defaultdict(float))
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError(f"Migration node IDs must be integer values (key = {key}).")
|
|
74
|
+
return super().__getitem__(key)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_METADATA = "Metadata"
|
|
78
|
+
_AUTHOR = "Author"
|
|
79
|
+
_DATECREATED = "DateCreated"
|
|
80
|
+
_TOOLNAME = "Tool"
|
|
81
|
+
_IDREFERENCE = "IdReference"
|
|
82
|
+
_MIGRATIONTYPE = "MigrationType"
|
|
83
|
+
_NODECOUNT = "NodeCount"
|
|
84
|
+
_DATAVALUECOUNT = "DatavalueCount"
|
|
85
|
+
_GENDERDATATYPE = "GenderDataType"
|
|
86
|
+
_AGESYEARS = "AgesYears"
|
|
87
|
+
_INTERPOLATIONTYPE = "InterpolationType"
|
|
88
|
+
_NODEOFFSETS = "NodeOffsets"
|
|
89
|
+
_EMODAPI = "emod-api"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Migration(object):
|
|
93
|
+
|
|
94
|
+
"""Represents migration data in a mapping from source node (IDs) to destination node (IDs) with rates for each pairing.
|
|
95
|
+
|
|
96
|
+
Migration data may be age dependent, gender dependent, both, or the same for all ages and genders.
|
|
97
|
+
A migration file (along with JSON metadata) can be loaded from the static method Migration.from_file() and
|
|
98
|
+
inspected and/or modified.
|
|
99
|
+
Migration objects can be started from scratch with Migration(), and populated with appropriate source-dest rate data
|
|
100
|
+
and saved to a file with the to_file() method.
|
|
101
|
+
Given migration = Migration(), syntax is as follows:
|
|
102
|
+
|
|
103
|
+
age and gender agnostic: `migration[source_id][dest_id]`
|
|
104
|
+
age dependent: `migration[source_id:age]` # age should be >= 0, ages > last bucket value use last bucket value
|
|
105
|
+
gender dependent: `migration[source_id:gender]` # gender one of Migration.MALE or Migration.FEMALE
|
|
106
|
+
age and gender dependent: `migration[source_id:gender:age]` # gender one of Migration.MALE or Migration.FEMALE
|
|
107
|
+
|
|
108
|
+
EMOD/DTK format migration files (and associated metadata files) can be written with migration.to_file(<filename>).
|
|
109
|
+
EMOD/DTK format migration files (with associated metadata files) can be read with migration.from_file(<filename>).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
SAME_FOR_BOTH_GENDERS = 0
|
|
113
|
+
ONE_FOR_EACH_GENDER = 1
|
|
114
|
+
|
|
115
|
+
LINEAR_INTERPOLATION = 0
|
|
116
|
+
PIECEWISE_CONSTANT = 1
|
|
117
|
+
|
|
118
|
+
LOCAL = 1
|
|
119
|
+
AIR = 2
|
|
120
|
+
REGIONAL = 3
|
|
121
|
+
SEA = 4
|
|
122
|
+
FAMILY = 5
|
|
123
|
+
INTERVENTION = 6
|
|
124
|
+
|
|
125
|
+
IDREF_LEGACY = "Legacy"
|
|
126
|
+
IDREF_GRUMP30ARCSEC = "Gridded world grump30arcsec"
|
|
127
|
+
IDREF_GRUMP2PT5ARCMIN = "Gridded world grump2.5arcmin"
|
|
128
|
+
IDREF_GRUMP1DEGREE = "Gridded world grump1degree"
|
|
129
|
+
|
|
130
|
+
MALE = 0
|
|
131
|
+
FEMALE = 1
|
|
132
|
+
|
|
133
|
+
MAX_AGE = 125
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
|
|
137
|
+
self._agesyears = []
|
|
138
|
+
try:
|
|
139
|
+
self._author = _author()
|
|
140
|
+
except Exception:
|
|
141
|
+
self._author = "Mystery Guest"
|
|
142
|
+
self._datecreated = datetime.now()
|
|
143
|
+
self._genderdatatype = self.SAME_FOR_BOTH_GENDERS
|
|
144
|
+
self._idreference = self.IDREF_LEGACY
|
|
145
|
+
self._interpolationtype = self.PIECEWISE_CONSTANT
|
|
146
|
+
self._migrationtype = self.LOCAL
|
|
147
|
+
self._tool = _EMODAPI
|
|
148
|
+
|
|
149
|
+
self._create_layers()
|
|
150
|
+
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
def _create_layers(self):
|
|
154
|
+
|
|
155
|
+
self._layers = []
|
|
156
|
+
for gender in range(0, self._genderdatatype + 1):
|
|
157
|
+
for age in range(0, len(self.AgesYears) if self.AgesYears else 1):
|
|
158
|
+
self._layers.append(Layer())
|
|
159
|
+
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def AgesYears(self) -> list:
|
|
164
|
+
"""
|
|
165
|
+
List of ages - ages < first value use first bucket, ages > last value use last bucket.
|
|
166
|
+
"""
|
|
167
|
+
return self._agesyears
|
|
168
|
+
|
|
169
|
+
@AgesYears.setter
|
|
170
|
+
def AgesYears(self, ages: list) -> None:
|
|
171
|
+
"""
|
|
172
|
+
List of ages - ages < first value use first bucket, ages > last value use last bucket.
|
|
173
|
+
"""
|
|
174
|
+
if sorted(ages) != self.AgesYears:
|
|
175
|
+
if self.NodeCount > 0:
|
|
176
|
+
warn("Changing age buckets clears existing migration information.", category=UserWarning)
|
|
177
|
+
self._agesyears = sorted(ages)
|
|
178
|
+
self._create_layers()
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def Author(self) -> str:
|
|
183
|
+
"""str: Author value for metadata for this migration datafile"""
|
|
184
|
+
return self._author
|
|
185
|
+
|
|
186
|
+
@Author.setter
|
|
187
|
+
def Author(self, author: str) -> None:
|
|
188
|
+
self._author = author
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def DatavalueCount(self) -> int:
|
|
193
|
+
"""int: Maximum data value count for any layer in this migration datafile"""
|
|
194
|
+
count = max([layer.DatavalueCount for layer in self._layers])
|
|
195
|
+
return count
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def DateCreated(self) -> datetime:
|
|
199
|
+
"""datetime: date/time stamp of this datafile"""
|
|
200
|
+
return self._datecreated
|
|
201
|
+
|
|
202
|
+
@DateCreated.setter
|
|
203
|
+
def DateCreated(self, value) -> None:
|
|
204
|
+
if not isinstance(value, datetime):
|
|
205
|
+
raise RuntimeError(f"DateCreated must be a datetime value (got {type(value)}).")
|
|
206
|
+
self._datecreated = value
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def GenderDataType(self) -> int:
|
|
211
|
+
"""int: gender data type for this datafile - SAME_FOR_BOTH_GENDERS or ONE_FOR_EACH_GENDER"""
|
|
212
|
+
return self._genderdatatype
|
|
213
|
+
|
|
214
|
+
@GenderDataType.setter
|
|
215
|
+
def GenderDataType(self, value: int) -> None:
|
|
216
|
+
|
|
217
|
+
# integer value
|
|
218
|
+
if value in Migration._GENDER_DATATYPE_ENUMS.keys():
|
|
219
|
+
value = int(value)
|
|
220
|
+
# string value
|
|
221
|
+
elif value in Migration._GENDER_DATATYPE_LOOKUP.keys():
|
|
222
|
+
value = Migration._GENDER_DATATYPE_LOOKUP[value]
|
|
223
|
+
else:
|
|
224
|
+
expected = [f"{key}/{value}" for key, value in Migration._GENDER_DATATYPE_LOOKUP.items()]
|
|
225
|
+
raise RuntimeError(f"Unknown gender data type, {value}, expected one of {expected}.")
|
|
226
|
+
|
|
227
|
+
if (self.NodeCount > 0) and (value != self._genderdatatype):
|
|
228
|
+
warn("Changing gender data type clears existing migration information.", category=UserWarning)
|
|
229
|
+
|
|
230
|
+
if value != self._genderdatatype:
|
|
231
|
+
self._genderdatatype = int(value)
|
|
232
|
+
self._create_layers()
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def IdReference(self) -> str:
|
|
237
|
+
"""str: ID reference metadata value"""
|
|
238
|
+
return self._idreference
|
|
239
|
+
|
|
240
|
+
@IdReference.setter
|
|
241
|
+
def IdReference(self, value: str) -> None:
|
|
242
|
+
self._idreference = str(value)
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def InterpolationType(self) -> int:
|
|
247
|
+
"""int: interpolation type for this migration data file - LINEAR_INTERPOLATION or PIECEWISE_CONSTANT"""
|
|
248
|
+
return self._interpolationtype
|
|
249
|
+
|
|
250
|
+
@InterpolationType.setter
|
|
251
|
+
def InterpolationType(self, value: int) -> None:
|
|
252
|
+
|
|
253
|
+
# integer value
|
|
254
|
+
if value in Migration._INTERPOLATION_TYPE_ENUMS.keys():
|
|
255
|
+
self._interpolationtype = int(value)
|
|
256
|
+
# string value
|
|
257
|
+
elif value in Migration._INTERPOLATION_TYPE_LOOKUP.keys():
|
|
258
|
+
self._interpolationtype = Migration._INTERPOLATION_TYPE_LOOKUP[value]
|
|
259
|
+
else:
|
|
260
|
+
expected = [f"{key}/{value}" for key, value in Migration._INTERPOLATION_TYPE_LOOKUP.items()]
|
|
261
|
+
raise RuntimeError(f"Unknown interpolation type, {value}, expected one of {expected}.")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def MigrationType(self) -> int:
|
|
266
|
+
"""int: migration type for this migration data file - LOCAL | AIR | REGIONAL | SEA | FAMILY | INTERVENTION"""
|
|
267
|
+
return self._migrationtype
|
|
268
|
+
|
|
269
|
+
@MigrationType.setter
|
|
270
|
+
def MigrationType(self, value: int) -> None:
|
|
271
|
+
|
|
272
|
+
# integer value
|
|
273
|
+
if value in Migration._MIGRATION_TYPE_ENUMS.keys():
|
|
274
|
+
self._migrationtype = int(value)
|
|
275
|
+
elif value in Migration._MIGRATION_TYPE_LOOKUP.keys():
|
|
276
|
+
self._migrationtype = Migration._MIGRATION_TYPE_LOOKUP[value]
|
|
277
|
+
else:
|
|
278
|
+
expected = [f"{key}/{value}" for key, value in Migration._MIGRATION_TYPE_LOOKUP.items()]
|
|
279
|
+
raise RuntimeError(f"Unknown migration type, {value}, expected one of {expected}.")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def Nodes(self) -> list:
|
|
284
|
+
node_ids = set()
|
|
285
|
+
for layer in self._layers:
|
|
286
|
+
node_ids |= set(layer.keys())
|
|
287
|
+
node_ids = sorted(node_ids)
|
|
288
|
+
return node_ids
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def NodeCount(self) -> int:
|
|
292
|
+
"""int: maximum number of source nodes in any layer of this migration data file"""
|
|
293
|
+
count = max([layer.NodeCount for layer in self._layers])
|
|
294
|
+
return count
|
|
295
|
+
|
|
296
|
+
def get_node_offsets(self, limit: int = 100) -> dict:
|
|
297
|
+
nodes = set()
|
|
298
|
+
for layer in self._layers:
|
|
299
|
+
nodes |= set(key for key in layer.keys())
|
|
300
|
+
count = min(self.DatavalueCount, limit)
|
|
301
|
+
# offsets = {}
|
|
302
|
+
# for index, node in enumerate(sorted(nodes)):
|
|
303
|
+
# offsets[node] = index * 12 * count
|
|
304
|
+
offsets = {node: 12 * index * count for index, node in enumerate(sorted(nodes))}
|
|
305
|
+
return offsets
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def NodeOffsets(self) -> dict:
|
|
309
|
+
"""dict: mapping from source node id to offset to destination and rate data in binary data"""
|
|
310
|
+
return self.get_node_offsets()
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def Tool(self) -> str:
|
|
314
|
+
"""str: tool metadata value"""
|
|
315
|
+
return self._tool
|
|
316
|
+
|
|
317
|
+
@Tool.setter
|
|
318
|
+
def Tool(self, value: str) -> None:
|
|
319
|
+
self._tool = str(value)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
def __getitem__(self, key):
|
|
323
|
+
"""allows indexing on this object to read/write rate data
|
|
324
|
+
Args:
|
|
325
|
+
key (slice): source node id:gender:age (gender and age depend on GenderDataType and AgesYears properties)
|
|
326
|
+
Returns:
|
|
327
|
+
dict for specified node/gender/age
|
|
328
|
+
"""
|
|
329
|
+
if self.GenderDataType == Migration.SAME_FOR_BOTH_GENDERS:
|
|
330
|
+
if not self.AgesYears:
|
|
331
|
+
# Case 1 - no gender or age differentiation - key (integer) == node id
|
|
332
|
+
return self._layers[0][key]
|
|
333
|
+
else:
|
|
334
|
+
# Case 3 - age buckets, no gender differentiation - key (tuple or slice) == node id:age
|
|
335
|
+
if isinstance(key, tuple):
|
|
336
|
+
node_id, age = key
|
|
337
|
+
elif isinstance(key, slice):
|
|
338
|
+
node_id, age = key.start, key.stop
|
|
339
|
+
else:
|
|
340
|
+
raise RuntimeError(f"Invalid indexing for migration - {key}")
|
|
341
|
+
layer_index = self._index_for_gender_and_age(None, age)
|
|
342
|
+
return self._layers[layer_index][node_id]
|
|
343
|
+
else:
|
|
344
|
+
if not self.AgesYears:
|
|
345
|
+
# Case 2 - by gender, no age differentiation - key (tuple or slice) == node id:gender
|
|
346
|
+
if isinstance(key, tuple):
|
|
347
|
+
node_id, gender = key
|
|
348
|
+
elif isinstance(key, slice):
|
|
349
|
+
node_id, gender = key.start, key.stop
|
|
350
|
+
else:
|
|
351
|
+
raise RuntimeError(f"Invalid indexing for migration - {key}")
|
|
352
|
+
if gender not in [Migration.SAME_FOR_BOTH_GENDERS, Migration.ONE_FOR_EACH_GENDER]:
|
|
353
|
+
raise RuntimeError(f"Invalid gender ({gender}) for migration.")
|
|
354
|
+
layer_index = self._index_for_gender_and_age(gender, None)
|
|
355
|
+
return self._layers[layer_index][node_id]
|
|
356
|
+
else:
|
|
357
|
+
# Case 4 - by gender and age - key (slice) == node id:gender:age
|
|
358
|
+
if isinstance(key, tuple):
|
|
359
|
+
node_id, gender, age = key
|
|
360
|
+
elif isinstance(key, slice):
|
|
361
|
+
node_id, gender, age = key.start, key.stop, key.step
|
|
362
|
+
else:
|
|
363
|
+
raise RuntimeError(f"Invalid indexing for migration - {key}")
|
|
364
|
+
if gender not in [Migration.SAME_FOR_BOTH_GENDERS, Migration.ONE_FOR_EACH_GENDER]:
|
|
365
|
+
raise RuntimeError(f"Invalid gender ({gender}) for migration.")
|
|
366
|
+
layer_index = self._index_for_gender_and_age(gender, age)
|
|
367
|
+
return self._layers[layer_index][node_id]
|
|
368
|
+
|
|
369
|
+
# raise RuntimeError("Invalid state.")
|
|
370
|
+
|
|
371
|
+
def _index_for_gender_and_age(self, gender: int, age: float) -> int:
|
|
372
|
+
"""
|
|
373
|
+
Use age to determine age bucket, 0 if no age differentiation.
|
|
374
|
+
Use gender data type to offset by # age buckets if gender data type is one for each gender and gender is female
|
|
375
|
+
Ages < first value use first bucket, ages > last value use last bucket.
|
|
376
|
+
"""
|
|
377
|
+
age_offset = 0
|
|
378
|
+
for age_offset, edge in enumerate(self.AgesYears):
|
|
379
|
+
if edge >= age:
|
|
380
|
+
break
|
|
381
|
+
gender_span = len(self.AgesYears) if self.AgesYears else 1
|
|
382
|
+
gender_offset = gender * gender_span if self.GenderDataType == Migration.ONE_FOR_EACH_GENDER else 0
|
|
383
|
+
index = gender_offset + age_offset
|
|
384
|
+
return index
|
|
385
|
+
|
|
386
|
+
def __iter__(self):
|
|
387
|
+
return iter(self._layers)
|
|
388
|
+
|
|
389
|
+
_MIGRATION_TYPE_ENUMS = {
|
|
390
|
+
LOCAL: "LOCAL_MIGRATION",
|
|
391
|
+
AIR: "AIR_MIGRATION",
|
|
392
|
+
REGIONAL: "REGIONAL_MIGRATION",
|
|
393
|
+
SEA: "SEA_MIGRATION",
|
|
394
|
+
FAMILY: "FAMILY_MIGRATION",
|
|
395
|
+
INTERVENTION: "INTERVENTION_MIGRATION"
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
_GENDER_DATATYPE_ENUMS = {
|
|
399
|
+
SAME_FOR_BOTH_GENDERS: "SAME_FOR_BOTH_GENDERS",
|
|
400
|
+
ONE_FOR_EACH_GENDER: "ONE_FOR_EACH_GENDER"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
_INTERPOLATION_TYPE_ENUMS = {
|
|
404
|
+
LINEAR_INTERPOLATION: "LINEAR_INTERPOLATION",
|
|
405
|
+
PIECEWISE_CONSTANT: "PIECEWISE_CONSTANT"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
def to_file(self, binaryfile: Path, metafile: Path = None, value_limit: int = 100):
|
|
409
|
+
"""Write current data to given file (and .json metadata file)
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
binaryfile (Path): path to output file (metadata will be written to same path with ".json" appended)
|
|
413
|
+
metafile (Path): override standard metadata file naming
|
|
414
|
+
value_limit (int): limit on number of destination values to write for each source node (default = 100)
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
(Path): path to binary file
|
|
418
|
+
"""
|
|
419
|
+
binaryfile = Path(binaryfile).absolute()
|
|
420
|
+
metafile = metafile if metafile else binaryfile.parent / (binaryfile.name + ".json")
|
|
421
|
+
|
|
422
|
+
actual_datavalue_count = min(self.DatavalueCount, value_limit) # limited to 100 destinations
|
|
423
|
+
|
|
424
|
+
node_ids = set()
|
|
425
|
+
for layer in self._layers:
|
|
426
|
+
node_ids |= set(layer.keys())
|
|
427
|
+
node_ids = sorted(node_ids)
|
|
428
|
+
|
|
429
|
+
offsets = self.get_node_offsets(actual_datavalue_count)
|
|
430
|
+
node_offsets_string = ''.join([f"{node:08x}{offsets[node]:08x}" for node in sorted(offsets.keys())])
|
|
431
|
+
|
|
432
|
+
metadata = {
|
|
433
|
+
_METADATA: {
|
|
434
|
+
_AUTHOR: self.Author,
|
|
435
|
+
_DATECREATED: f"{self.DateCreated:%a %b %d %Y %H:%M:%S}",
|
|
436
|
+
_TOOLNAME: self.Tool,
|
|
437
|
+
_IDREFERENCE: self.IdReference,
|
|
438
|
+
_MIGRATIONTYPE: self._MIGRATION_TYPE_ENUMS[self.MigrationType],
|
|
439
|
+
_NODECOUNT: self.NodeCount,
|
|
440
|
+
_DATAVALUECOUNT: actual_datavalue_count,
|
|
441
|
+
# could omit this if SAME_FOR_BOTH_GENDERS since it is the default
|
|
442
|
+
_GENDERDATATYPE: self._GENDER_DATATYPE_ENUMS[self.GenderDataType],
|
|
443
|
+
# _AGESYEARS: self.AgesYears, # see below
|
|
444
|
+
_INTERPOLATIONTYPE: self._INTERPOLATION_TYPE_ENUMS[self.InterpolationType]
|
|
445
|
+
},
|
|
446
|
+
_NODEOFFSETS: node_offsets_string
|
|
447
|
+
}
|
|
448
|
+
if self.AgesYears:
|
|
449
|
+
# older versions of Eradication do not handle empty AgesYears lists robustly
|
|
450
|
+
metadata[_METADATA][_AGESYEARS] = self.AgesYears
|
|
451
|
+
|
|
452
|
+
print(f"Writing metadata to '{metafile}'")
|
|
453
|
+
with metafile.open("w") as handle:
|
|
454
|
+
json.dump(metadata, handle, indent=4, separators=(",", ": "))
|
|
455
|
+
|
|
456
|
+
def key_func(k, d=None):
|
|
457
|
+
return d[k]
|
|
458
|
+
|
|
459
|
+
# layers are in age bucket order by gender, e.g. male 0-5, 5-10, 10+, female 0-5, 5-10, 10+
|
|
460
|
+
# see _index_for_gender_and_age()
|
|
461
|
+
print(f"Writing binary data to '{binaryfile}'")
|
|
462
|
+
with binaryfile.open("wb") as file:
|
|
463
|
+
for layer in self:
|
|
464
|
+
for node in node_ids:
|
|
465
|
+
destinations = np.zeros(actual_datavalue_count, dtype=np.uint32)
|
|
466
|
+
rates = np.zeros(actual_datavalue_count, dtype=np.float64)
|
|
467
|
+
if node in layer:
|
|
468
|
+
|
|
469
|
+
# Sort keys descending on rate and ascending on node ID.
|
|
470
|
+
# That way if we are truncating the list, we include the "most important" nodes.
|
|
471
|
+
keys = sorted(layer[node].keys()) # sorted ascending on node ID
|
|
472
|
+
keys = sorted(keys, key=partial(key_func, d=layer[node]), reverse=True) # descending on rate
|
|
473
|
+
|
|
474
|
+
if len(keys) > actual_datavalue_count:
|
|
475
|
+
keys = keys[0:actual_datavalue_count]
|
|
476
|
+
# save rates in ascending order so small rates are not lost when looking at the cumulative sum
|
|
477
|
+
keys = list(reversed(keys))
|
|
478
|
+
destinations[0:len(keys)] = keys
|
|
479
|
+
rates[0:len(keys)] = [layer[node][key] for key in keys]
|
|
480
|
+
else:
|
|
481
|
+
warn(f"No destination nodes found for node {node}", category=UserWarning)
|
|
482
|
+
destinations.tofile(file)
|
|
483
|
+
rates.tofile(file)
|
|
484
|
+
|
|
485
|
+
return binaryfile
|
|
486
|
+
|
|
487
|
+
_MIGRATION_TYPE_LOOKUP = {
|
|
488
|
+
"LOCAL_MIGRATION": LOCAL,
|
|
489
|
+
"AIR_MIGRATION": AIR,
|
|
490
|
+
"REGIONAL_MIGRATION": REGIONAL,
|
|
491
|
+
"SEA_MIGRATION": SEA,
|
|
492
|
+
"FAMILY_MIGRATION": FAMILY,
|
|
493
|
+
"INTERVENTION_MIGRATION": INTERVENTION
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_GENDER_DATATYPE_LOOKUP = {
|
|
497
|
+
"SAME_FOR_BOTH_GENDERS": SAME_FOR_BOTH_GENDERS,
|
|
498
|
+
"ONE_FOR_EACH_GENDER": ONE_FOR_EACH_GENDER
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_INTERPOLATION_TYPE_LOOKUP = {
|
|
502
|
+
"LINEAR_INTERPOLATION": LINEAR_INTERPOLATION,
|
|
503
|
+
"PIECEWISE_CONSTANT": PIECEWISE_CONSTANT
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def from_file(binaryfile: Path,
|
|
508
|
+
metafile: Path = None) -> Migration:
|
|
509
|
+
"""Reads migration data file from given binary (and associated JSON metadata file)
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
binaryfile (Path): path to binary file (metadata file is assumed to be at same location with ".json" suffix)
|
|
513
|
+
metafile (Path): use given metafile rather than inferring metafile name from the binary file name
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Migration object representing binary data in the given file.
|
|
517
|
+
"""
|
|
518
|
+
binaryfile = Path(binaryfile).absolute()
|
|
519
|
+
metafile = metafile if metafile else binaryfile.parent / (binaryfile.name + ".json")
|
|
520
|
+
|
|
521
|
+
if not binaryfile.exists():
|
|
522
|
+
raise RuntimeError(f"Cannot find migration binary file '{binaryfile}'")
|
|
523
|
+
if not metafile.exists():
|
|
524
|
+
raise RuntimeError(f"Cannot find migration metadata file '{metafile}'.")
|
|
525
|
+
with metafile.open("r") as file:
|
|
526
|
+
jason = json.load(file)
|
|
527
|
+
|
|
528
|
+
# these are the minimum required entries to load a migration file
|
|
529
|
+
assert _METADATA in jason, f"Metadata file '{metafile}' does not have a 'Metadata' entry."
|
|
530
|
+
metadata = jason[_METADATA]
|
|
531
|
+
assert _NODECOUNT in metadata, f"Metadata file '{metafile}' does not have a 'NodeCount' entry."
|
|
532
|
+
assert _DATAVALUECOUNT in metadata, f"Metadata file '{metafile}' does not have a 'DatavalueCount' entry."
|
|
533
|
+
assert _NODEOFFSETS in jason, f"Metadata file '{metafile}' does not have a 'NodeOffsets' entry."
|
|
534
|
+
|
|
535
|
+
migration = Migration()
|
|
536
|
+
migration.Author = _value_with_default(metadata, _AUTHOR, _author())
|
|
537
|
+
migration.DateCreated = _try_parse_date(metadata[_DATECREATED]) if _DATECREATED in metadata else datetime.now()
|
|
538
|
+
migration.Tool = _value_with_default(metadata, _TOOLNAME, _EMODAPI)
|
|
539
|
+
migration.IdReference = _value_with_default(metadata, _IDREFERENCE, Migration.IDREF_LEGACY)
|
|
540
|
+
migration.MigrationType = Migration._MIGRATION_TYPE_LOOKUP[_value_with_default(metadata,
|
|
541
|
+
_MIGRATIONTYPE,
|
|
542
|
+
"LOCAL_MIGRATION")]
|
|
543
|
+
migration.GenderDataType = Migration._GENDER_DATATYPE_LOOKUP[_value_with_default(metadata,
|
|
544
|
+
_GENDERDATATYPE,
|
|
545
|
+
"SAME_FOR_BOTH_GENDERS")]
|
|
546
|
+
migration.AgesYears = _value_with_default(metadata, _AGESYEARS, [])
|
|
547
|
+
migration.InterpolationType = Migration._INTERPOLATION_TYPE_LOOKUP[_value_with_default(metadata,
|
|
548
|
+
_INTERPOLATIONTYPE,
|
|
549
|
+
"PIECEWISE_CONSTANT")]
|
|
550
|
+
|
|
551
|
+
node_count = metadata[_NODECOUNT]
|
|
552
|
+
node_offsets = jason[_NODEOFFSETS]
|
|
553
|
+
if len(node_offsets) != 16 * node_count:
|
|
554
|
+
raise RuntimeError(f"Length of node offsets string {len(node_offsets)} != 16 * node count {node_count}.")
|
|
555
|
+
offsets = _parse_node_offsets(node_offsets, node_count)
|
|
556
|
+
datavalue_count = metadata[_DATAVALUECOUNT]
|
|
557
|
+
with binaryfile.open("rb") as file:
|
|
558
|
+
for gender in range(1 if migration.GenderDataType == Migration.SAME_FOR_BOTH_GENDERS else 2):
|
|
559
|
+
for age in migration.AgesYears if migration.AgesYears else [0]:
|
|
560
|
+
layer = migration._layers[migration._index_for_gender_and_age(gender, age)]
|
|
561
|
+
for node, offset in offsets.items():
|
|
562
|
+
file.seek(offset, SEEK_SET)
|
|
563
|
+
destinations = np.fromfile(file, dtype=np.uint32, count=datavalue_count)
|
|
564
|
+
rates = np.fromfile(file, dtype=np.float64, count=datavalue_count)
|
|
565
|
+
for destination, rate in zip(destinations, rates):
|
|
566
|
+
if rate > 0:
|
|
567
|
+
layer[node][destination] = rate
|
|
568
|
+
|
|
569
|
+
return migration
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def examine_file(filename):
|
|
573
|
+
|
|
574
|
+
def name_for_gender_datatype(e: int) -> str:
|
|
575
|
+
return Migration._GENDER_DATATYPE_ENUMS[e] if e in Migration._GENDER_DATATYPE_ENUMS else "unknown"
|
|
576
|
+
|
|
577
|
+
def name_for_interpolation(e: int) -> str:
|
|
578
|
+
return Migration._INTERPOLATION_TYPE_ENUMS[e] if e in Migration._INTERPOLATION_TYPE_ENUMS else "unknown"
|
|
579
|
+
|
|
580
|
+
def name_for_migration_type(e: int) -> str:
|
|
581
|
+
return Migration._MIGRATION_TYPE_ENUMS[e] if e in Migration._MIGRATION_TYPE_ENUMS else "unknown"
|
|
582
|
+
|
|
583
|
+
migration = from_file(filename)
|
|
584
|
+
print(f"Author: {migration.Author}")
|
|
585
|
+
print(f"DatavalueCount: {migration.DatavalueCount}")
|
|
586
|
+
print(f"DateCreated: {migration.DateCreated:%a %B %d %Y %H:%M}")
|
|
587
|
+
print(f"GenderDataType: {migration.GenderDataType} ({name_for_gender_datatype(migration.GenderDataType)})")
|
|
588
|
+
print(f"IdReference: {migration.IdReference}")
|
|
589
|
+
print(f"InterpolationType: {migration.InterpolationType} ({name_for_interpolation(migration.InterpolationType)})")
|
|
590
|
+
print(f"MigrationType: {migration.MigrationType} ({name_for_migration_type(migration.MigrationType)})")
|
|
591
|
+
print(f"NodeCount: {migration.NodeCount}")
|
|
592
|
+
print(f"NodeOffsets: {migration.NodeOffsets}")
|
|
593
|
+
print(f"Tool: {migration.Tool}")
|
|
594
|
+
print(f"Nodes: {migration.Nodes}")
|
|
595
|
+
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _author() -> str:
|
|
600
|
+
username = "Unknown"
|
|
601
|
+
if system() == "Windows":
|
|
602
|
+
username = environ["USERNAME"]
|
|
603
|
+
elif "USER" in environ:
|
|
604
|
+
username = environ["USER"]
|
|
605
|
+
return username
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _parse_node_offsets(string: str, count: int) -> dict:
|
|
609
|
+
|
|
610
|
+
assert len(string) == 16 * count, f"Length of node offsets string {len(string)} != 16 * node count {count}."
|
|
611
|
+
|
|
612
|
+
offsets = {}
|
|
613
|
+
for index in range(count):
|
|
614
|
+
base = 16 * index
|
|
615
|
+
offset = base + 8
|
|
616
|
+
offsets[int(string[base:base + 8], 16)] = int(string[offset:offset + 8], 16)
|
|
617
|
+
|
|
618
|
+
return offsets
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _try_parse_date(string: str) -> datetime:
|
|
622
|
+
|
|
623
|
+
patterns = [
|
|
624
|
+
"%a %b %d %Y %H:%M:%S",
|
|
625
|
+
"%a %b %d %H:%M:%S %Y",
|
|
626
|
+
"%m/%d/%Y",
|
|
627
|
+
"%Y-%m-%d %H:%M:%S.%f"
|
|
628
|
+
]
|
|
629
|
+
|
|
630
|
+
for pattern in patterns:
|
|
631
|
+
try:
|
|
632
|
+
timestamp = datetime.strptime(string, pattern)
|
|
633
|
+
return timestamp
|
|
634
|
+
except ValueError:
|
|
635
|
+
pass
|
|
636
|
+
|
|
637
|
+
timestamp = datetime.now()
|
|
638
|
+
warn(f"Could not parse date stamp '{string}', using datetime.now() ({timestamp})")
|
|
639
|
+
|
|
640
|
+
return timestamp
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _value_with_default(dictionary: dict, key: str, default: object) -> object:
|
|
644
|
+
return dictionary[key] if key in dictionary else default
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
"""
|
|
648
|
+
utility functions emodpy-utils?
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def from_demog_and_param_gravity(demographics_file_path, gravity_params, id_ref, migration_type=Migration.LOCAL):
|
|
653
|
+
demog = Demographics.from_file(demographics_file_path)
|
|
654
|
+
return _from_demog_and_param_gravity(demog, gravity_params, id_ref, migration_type)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _from_demog_and_param_gravity(demographics, gravity_params, id_ref, migration_type=Migration.LOCAL):
|
|
658
|
+
"""
|
|
659
|
+
Create migration files from a gravity model and an input demographics file.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
def _compute_migr_prob(grav_params, home_pop, dest_pop, dist):
|
|
663
|
+
"""
|
|
664
|
+
Utility function for computing migration probabilities for gravity model.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
# If home/dest node has 0 pop, assume this node is the regional work node-- no local migration allowed
|
|
668
|
+
if home_pop == 0 or dest_pop == 0:
|
|
669
|
+
return 0.
|
|
670
|
+
else:
|
|
671
|
+
num_trips = grav_params[0] * home_pop ** grav_params[1] * dest_pop ** grav_params[2] * dist ** grav_params[3]
|
|
672
|
+
prob_trip = np.min([1., num_trips / home_pop])
|
|
673
|
+
return prob_trip
|
|
674
|
+
|
|
675
|
+
def _compute_migr_dict(node_list, grav_params, **kwargs):
|
|
676
|
+
"""
|
|
677
|
+
Utility function for computing migration value map.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
excluded_nodes = set(kwargs["exclude_nodes"]) if "exclude_nodes" in kwargs else set()
|
|
681
|
+
|
|
682
|
+
mig = Migration()
|
|
683
|
+
geodesic = Geodesic.WGS84
|
|
684
|
+
|
|
685
|
+
for source_node in node_list:
|
|
686
|
+
|
|
687
|
+
source_id = source_node["NodeID"]
|
|
688
|
+
src_lat = source_node["NodeAttributes"]["Latitude"]
|
|
689
|
+
src_long = source_node["NodeAttributes"]["Longitude"]
|
|
690
|
+
src_pop = source_node["NodeAttributes"]["InitialPopulation"]
|
|
691
|
+
|
|
692
|
+
if source_id in excluded_nodes:
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
for destination_node in node_list:
|
|
696
|
+
|
|
697
|
+
if destination_node == source_node:
|
|
698
|
+
continue
|
|
699
|
+
|
|
700
|
+
dest_id = destination_node["NodeID"]
|
|
701
|
+
|
|
702
|
+
if dest_id in excluded_nodes:
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
dst_lat = destination_node["NodeAttributes"]["Latitude"]
|
|
706
|
+
dst_long = destination_node["NodeAttributes"]["Longitude"]
|
|
707
|
+
dst_pop = destination_node["NodeAttributes"]["InitialPopulation"]
|
|
708
|
+
|
|
709
|
+
distance = geodesic.Inverse(src_lat, src_long, dst_lat, dst_long, Geodesic.DISTANCE)['s12'] / 1000 # km
|
|
710
|
+
probability = _compute_migr_prob(grav_params, src_pop, dst_pop, distance)
|
|
711
|
+
|
|
712
|
+
mig[source_id][dest_id] = probability
|
|
713
|
+
|
|
714
|
+
return mig
|
|
715
|
+
|
|
716
|
+
# load
|
|
717
|
+
nodes = [node.to_dict() for node in demographics.nodes]
|
|
718
|
+
migration = _compute_migr_dict(nodes, gravity_params)
|
|
719
|
+
migration.IdReference = id_ref
|
|
720
|
+
migration.MigrationType = migration_type
|
|
721
|
+
|
|
722
|
+
return migration
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# by gender, by age
|
|
726
|
+
_mapping_fns = {
|
|
727
|
+
(False, False): lambda m, i, g, a: m[i],
|
|
728
|
+
(False, True): lambda m, i, g, a: m[i:a],
|
|
729
|
+
(True, False): lambda m, i, g, a: m[i:g],
|
|
730
|
+
(True, True): lambda m, i, g, a: m[i:g:a]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
# by gender, by age
|
|
734
|
+
_display_fns = {
|
|
735
|
+
(False, False): lambda i, g, a, d, r: f"{i},{d},{r}", # id only
|
|
736
|
+
(False, True): lambda i, g, a, d, r: f"{i},{a},{d},{r}", # id:age
|
|
737
|
+
(True, False): lambda i, g, a, d, r: f"{i},{g},{d},{r}", # id:gender
|
|
738
|
+
(True, True): lambda i, g, a, d, r: f"{i},{g},{a},{d},{r}" # id:gender:age
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def to_csv(filename: Path):
|
|
743
|
+
|
|
744
|
+
migration = from_file(filename)
|
|
745
|
+
|
|
746
|
+
mapping = _mapping_fns[(migration.GenderDataType == Migration.ONE_FOR_EACH_GENDER, bool(migration.AgesYears))]
|
|
747
|
+
display = _display_fns[(migration.GenderDataType == Migration.ONE_FOR_EACH_GENDER, bool(migration.AgesYears))]
|
|
748
|
+
|
|
749
|
+
print(display("node", "gender", "age", "destination", "rate"))
|
|
750
|
+
for gender in range(1 if migration.GenderDataType == Migration.SAME_FOR_BOTH_GENDERS else 2):
|
|
751
|
+
for age in migration.AgesYears if migration.AgesYears else [0]:
|
|
752
|
+
for node in migration.Nodes:
|
|
753
|
+
for destination, rate in mapping(migration, node, gender, age).items():
|
|
754
|
+
print(display(node, gender, age, destination, rate))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def from_csv(filename: Path,
|
|
758
|
+
id_ref,
|
|
759
|
+
mig_type=None) -> Migration:
|
|
760
|
+
"""Create migration from csv file. The file should have columns 'source' for the source node, 'destination' for the destination node, and 'rate' for the migration rate.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
filename: csv file
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
Migration object
|
|
767
|
+
"""
|
|
768
|
+
migration = Migration()
|
|
769
|
+
migration.IdReference = id_ref
|
|
770
|
+
if not mig_type:
|
|
771
|
+
mig_type = Migration.LOCAL
|
|
772
|
+
else:
|
|
773
|
+
migration._migrationtype = mig_type
|
|
774
|
+
with filename.open("r") as csvfile:
|
|
775
|
+
reader = csv.DictReader(csvfile)
|
|
776
|
+
csv_data_read = False
|
|
777
|
+
for row in reader:
|
|
778
|
+
csv_data_read = True
|
|
779
|
+
migration[int(row['source'])][int(row['destination'])] = float(row['rate'])
|
|
780
|
+
assert csv_data_read, "Csv file %s does not contain migration data." % filename
|
|
781
|
+
|
|
782
|
+
return migration
|