emod-api 1.35.0__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 (62) hide show
  1. emod_api/__init__.py +1 -0
  2. emod_api/campaign.py +171 -0
  3. emod_api/channelreports/__init__.py +0 -0
  4. emod_api/channelreports/__main__.py +5 -0
  5. emod_api/channelreports/channels.py +436 -0
  6. emod_api/channelreports/icj_to_csv.py +64 -0
  7. emod_api/channelreports/plot_icj_means.py +149 -0
  8. emod_api/channelreports/plot_prop_report.py +206 -0
  9. emod_api/channelreports/utils.py +322 -0
  10. emod_api/config/__init__.py +0 -0
  11. emod_api/config/default_from_schema.py +15 -0
  12. emod_api/config/default_from_schema_no_validation.py +204 -0
  13. emod_api/config/from_overrides.py +134 -0
  14. emod_api/demographics/BaseInputFile.py +27 -0
  15. emod_api/demographics/Demographics.py +1628 -0
  16. emod_api/demographics/DemographicsGenerator.py +519 -0
  17. emod_api/demographics/DemographicsInputDataParsers.py +123 -0
  18. emod_api/demographics/DemographicsTemplates.py +815 -0
  19. emod_api/demographics/Node.py +295 -0
  20. emod_api/demographics/PreDefinedDistributions.py +380 -0
  21. emod_api/demographics/PropertiesAndAttributes.py +778 -0
  22. emod_api/demographics/Updateable.py +39 -0
  23. emod_api/demographics/__init__.py +0 -0
  24. emod_api/demographics/__main__.py +5 -0
  25. emod_api/demographics/demographics_utils.py +265 -0
  26. emod_api/demographics/service/__init__.py +0 -0
  27. emod_api/demographics/service/grid_construction.py +148 -0
  28. emod_api/demographics/service/service.py +55 -0
  29. emod_api/interventions/__init__.py +0 -0
  30. emod_api/interventions/__main__.py +5 -0
  31. emod_api/interventions/ccdl.py +26 -0
  32. emod_api/interventions/ccdl_viz.py +186 -0
  33. emod_api/interventions/common.py +1254 -0
  34. emod_api/interventions/import_pressure.py +45 -0
  35. emod_api/interventions/migration.py +279 -0
  36. emod_api/interventions/outbreak.py +120 -0
  37. emod_api/interventions/utils.py +214 -0
  38. emod_api/legacy/__init__.py +0 -0
  39. emod_api/legacy/plotAllCharts.py +230 -0
  40. emod_api/migration/__init__.py +1 -0
  41. emod_api/migration/__main__.py +22 -0
  42. emod_api/migration/migration.py +852 -0
  43. emod_api/multidim_plotter.py +86 -0
  44. emod_api/schema_to_class.py +421 -0
  45. emod_api/serialization/CensusAndModPop.py +40 -0
  46. emod_api/serialization/SerializedPopulation.py +206 -0
  47. emod_api/serialization/__init__.py +0 -0
  48. emod_api/serialization/dtkFileSupport.py +61 -0
  49. emod_api/serialization/dtkFileTools.py +1379 -0
  50. emod_api/serialization/dtkFileUtility.py +141 -0
  51. emod_api/spatialreports/__init__.py +0 -0
  52. emod_api/spatialreports/__main__.py +67 -0
  53. emod_api/spatialreports/plot_spat_means.py +99 -0
  54. emod_api/spatialreports/spatial.py +211 -0
  55. emod_api/utils.py +174 -0
  56. emod_api/weather/__init__.py +0 -0
  57. emod_api/weather/weather.py +429 -0
  58. emod_api-1.35.0.dist-info/METADATA +131 -0
  59. emod_api-1.35.0.dist-info/RECORD +62 -0
  60. emod_api-1.35.0.dist-info/WHEEL +5 -0
  61. emod_api-1.35.0.dist-info/licenses/LICENSE +21 -0
  62. emod_api-1.35.0.dist-info/top_level.txt +1 -0
emod_api/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.35.0"
emod_api/campaign.py ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ You use this simple campaign builder by importing it, adding valid events via "add", and writing it out with "save".
4
+ """
5
+
6
+ import json
7
+
8
+ schema_path = None
9
+ campaign_dict = {"Events": [], "Use_Defaults": 1}
10
+ pubsub_signals_subbing = []
11
+ pubsub_signals_pubbing = []
12
+ adhocs = []
13
+ custom_coordinator_events = []
14
+ custom_node_events = []
15
+ event_map = {}
16
+ use_old_adhoc_handling = False
17
+ unsafe = False
18
+ implicits = list()
19
+ trigger_list = None
20
+
21
+
22
+ def reset():
23
+ del (campaign_dict["Events"][:])
24
+ global pubsub_signals_subbing
25
+ global pubsub_signals_pubbing
26
+ global adhocs
27
+ global event_map
28
+ del (pubsub_signals_subbing[:])
29
+ del (pubsub_signals_pubbing[:])
30
+ del (adhocs[:])
31
+ del (custom_coordinator_events[:])
32
+ del (custom_node_events[:])
33
+ event_map = {}
34
+ from emod_api import schema_to_class as s2c
35
+ s2c.schema_cache = None
36
+ del (implicits[:])
37
+
38
+
39
+ def set_schema(schema_path_in):
40
+ """
41
+ Set the (path to) the schema file. And reset all campaign variables. This is essentially a
42
+ "start_building_campaign" function.
43
+
44
+ Args:
45
+
46
+ schema_path_in. The path to a schema.json.
47
+ Returns:
48
+ Nothing
49
+ """
50
+ reset()
51
+ global schema_path
52
+ schema_path = schema_path_in
53
+
54
+
55
+ def get_schema():
56
+ schema = None
57
+ if schema_path:
58
+ with open(schema_path) as schema_file:
59
+ schema = json.load(schema_file)
60
+ return schema
61
+
62
+
63
+ def add(event, name=None, first=False):
64
+ """
65
+ Add a complete campaign event to the campaign builder. The new event is assumed to be a Python dict, and a
66
+ valid event. The new event is not validated here.
67
+ Set the first flag to True if this is the first event in a campaign because it functions as an
68
+ accumulator and in some situations like sweeps it might have been used recently.
69
+ """
70
+ event.finalize()
71
+ if first:
72
+ print("Use of 'first' flag is deprecated. Use set_schema to start build a new, empty campaign.")
73
+ global campaign_dict
74
+ campaign_dict["Events"] = []
75
+ if "Event_Name" not in event and name is not None:
76
+ event["Event_Name"] = name
77
+ if "Listening" in event:
78
+ pubsub_signals_subbing.extend(event["Listening"])
79
+ event.pop("Listening")
80
+ if "Broadcasting" in event:
81
+ pubsub_signals_pubbing.extend(event["Broadcasting"])
82
+ event.pop("Broadcasting")
83
+ campaign_dict["Events"].append(event)
84
+
85
+
86
+ def get_trigger_list():
87
+ global trigger_list
88
+ if get_schema():
89
+ # This needs to be fixed in the schema post-processor: maybe create a new idmTime:EventEnum and replace
90
+ # all the occurrences with a reference to that.
91
+ try:
92
+ trigger_list = get_schema()["idmTypes"]["idmAbstractType:EventCoordinator"]["BroadcastCoordinatorEvent"][
93
+ "Broadcast_Event"]["enum"]
94
+ except Exception as ex:
95
+ trigger_list = get_schema()["idmTypes"]["idmType:IncidenceCounter"]["Trigger_Condition_List"]["Built-in"]
96
+ return trigger_list
97
+
98
+
99
+ def save(filename="campaign.json"):
100
+ """
101
+ Save 'campaign_dict' as file named 'filename'.
102
+ """
103
+ with open(filename, "w") as camp_file:
104
+ json.dump(campaign_dict, camp_file, sort_keys=True, indent=4)
105
+ import copy
106
+ ignored_events = copy.deepcopy(set(pubsub_signals_pubbing))
107
+ non_camp_events = set()
108
+ if len(pubsub_signals_subbing) > 0:
109
+ for event in set(pubsub_signals_subbing):
110
+ if event in ignored_events:
111
+ ignored_events.remove(event)
112
+ if len(non_camp_events) > 0:
113
+ for event in set(non_camp_events):
114
+ if event in get_adhocs() and not unsafe:
115
+ raise RuntimeError(f"ERROR: Report is configured to LISTEN to the following non-existent event: \n"
116
+ f"{event} \nPlease fix the error.\n")
117
+ return filename
118
+
119
+
120
+ def get_adhocs():
121
+ return event_map
122
+
123
+
124
+ def get_custom_coordinator_events():
125
+ return list(set(custom_coordinator_events))
126
+
127
+
128
+ def get_custom_node_events():
129
+ return list(set(custom_node_events))
130
+
131
+
132
+ def get_recv_trigger(trigger, old=use_old_adhoc_handling):
133
+ """
134
+ Get the correct representation of a trigger (also called signal or even event) that is being listened to.
135
+ """
136
+ pubsub_signals_subbing.append(trigger)
137
+ return get_event(trigger, old)
138
+
139
+
140
+ def get_send_trigger(trigger, old=use_old_adhoc_handling):
141
+ """
142
+ Get the correct representation of a trigger (also called signal or even event) that is being broadcast.
143
+ """
144
+ pubsub_signals_pubbing.append(trigger)
145
+ return get_event(trigger, old)
146
+
147
+
148
+ def get_event(event, old=False):
149
+ """
150
+ Basic placeholder functionality for now. This will map new ad-hoc events to GP_EVENTs and manage that 'cache'
151
+ If event in built-ins, return event, else if in adhoc map, return mapped event, else add to adhoc_map and return
152
+ mapped event.
153
+ """
154
+ if event is None or event == "":
155
+ raise ValueError("campaign.get_event() called with an empty event. Please specify a string.")
156
+
157
+ return_event = None
158
+ global trigger_list
159
+ if trigger_list is None:
160
+ trigger_list = get_trigger_list()
161
+
162
+ if event in trigger_list:
163
+ return_event = event
164
+ elif event in event_map:
165
+ return_event = event_map[event]
166
+ else:
167
+ # get next entry in GP_EVENT_xxx
168
+ new_event_name = event if old else 'GP_EVENT_{:03d}'.format(len(event_map))
169
+ event_map[event] = new_event_name
170
+ return_event = event_map[event]
171
+ return return_event
File without changes
@@ -0,0 +1,5 @@
1
+ #/usr/bin/env python
2
+
3
+ import emod_api.channelreports as conf
4
+ help(conf)
5
+
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Module for reading InsetChart.json channels."""
4
+
5
+ from csv import writer as CsvWriter
6
+ from datetime import datetime
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, List, Union
10
+ import warnings
11
+
12
+ import pandas as pd
13
+
14
+ _CHANNELS = "Channels"
15
+ _DTK_VERSION = "DTK_Version"
16
+ _DATETIME = "DateTime"
17
+ _REPORT_TYPE = "Report_Type"
18
+ _REPORT_VERSION = "Report_Version"
19
+ _SIMULATION_TIMESTEP = "Simulation_Timestep"
20
+ _START_TIME = "Start_Time"
21
+ _TIMESTEPS = "Timesteps"
22
+
23
+ _KNOWN_KEYS = {
24
+ _CHANNELS,
25
+ _DTK_VERSION,
26
+ _DATETIME,
27
+ _REPORT_TYPE,
28
+ _REPORT_VERSION,
29
+ _SIMULATION_TIMESTEP,
30
+ _START_TIME,
31
+ _TIMESTEPS,
32
+ }
33
+
34
+ _TYPE_INSETCHART = "InsetChart"
35
+
36
+ _UNITS = "Units"
37
+ _DATA = "Data"
38
+
39
+ _HEADER = "Header"
40
+
41
+
42
+ class Header(object):
43
+
44
+ # Allow callers to send an arbitrary dictionary, potentially, with extra key:value pairs.
45
+ def __init__(self, **kwargs) -> None:
46
+
47
+ self._channelCount = kwargs[_CHANNELS] if kwargs and _CHANNELS in kwargs else 0
48
+ self._dtkVersion = (
49
+ kwargs[_DTK_VERSION] if kwargs and _DTK_VERSION in kwargs else "unknown-branch (unknown)"
50
+ )
51
+ self._timeStamp = (
52
+ kwargs[_DATETIME]
53
+ if kwargs and _DATETIME in kwargs
54
+ else f"{datetime.now():%a %B %d %Y %H:%M:%S}"
55
+ )
56
+ self._reportType = (
57
+ kwargs[_REPORT_TYPE]
58
+ if kwargs and _REPORT_TYPE in kwargs
59
+ else _TYPE_INSETCHART
60
+ )
61
+ self._reportVersion = (
62
+ kwargs[_REPORT_VERSION] if kwargs and _REPORT_VERSION in kwargs else "0.0"
63
+ )
64
+ self._stepSize = (
65
+ kwargs[_SIMULATION_TIMESTEP]
66
+ if kwargs and _SIMULATION_TIMESTEP in kwargs
67
+ else 1
68
+ )
69
+ self._startTime = kwargs[_START_TIME] if kwargs and _START_TIME in kwargs else 0
70
+ self._numTimeSteps = (
71
+ kwargs[_TIMESTEPS] if kwargs and _TIMESTEPS in kwargs else 0
72
+ )
73
+ self._tags = {key: kwargs[key] for key in kwargs if key not in _KNOWN_KEYS}
74
+
75
+ return
76
+
77
+ @property
78
+ def num_channels(self) -> int:
79
+ return self._channelCount
80
+
81
+ @num_channels.setter
82
+ def num_channels(self, count: int) -> None:
83
+ """> 0"""
84
+ assert count > 0, "numChannels must be > 0"
85
+ self._channelCount = count
86
+ return
87
+
88
+ @property
89
+ def dtk_version(self) -> str:
90
+ return self._dtkVersion
91
+
92
+ @dtk_version.setter
93
+ def dtk_version(self, version: str) -> None:
94
+ """major.minor"""
95
+ self._dtkVersion = f"{version}"
96
+ return
97
+
98
+ @property
99
+ def time_stamp(self) -> str:
100
+ return self._timeStamp
101
+
102
+ @time_stamp.setter
103
+ def time_stamp(self, timestamp: Union[datetime, str]) -> None:
104
+ """datetime or string"""
105
+ self._timeStamp = (
106
+ f"{timestamp:%a %B %d %Y %H:%M:%S}"
107
+ if isinstance(timestamp, datetime)
108
+ else f"{timestamp}"
109
+ )
110
+ return
111
+
112
+ @property
113
+ def report_type(self) -> str:
114
+ return self._reportType
115
+
116
+ @report_type.setter
117
+ def report_type(self, report_type: str) -> None:
118
+ self._reportType = f"{report_type}"
119
+ return
120
+
121
+ @property
122
+ def report_version(self) -> str:
123
+ return self._reportVersion
124
+
125
+ @report_version.setter
126
+ def report_version(self, version: str) -> None:
127
+ self._reportVersion = f"{version}"
128
+ return
129
+
130
+ @property
131
+ def step_size(self) -> int:
132
+ """>= 1"""
133
+ return self._stepSize
134
+
135
+ @step_size.setter
136
+ def step_size(self, size: int) -> None:
137
+ """>= 1"""
138
+ self._stepSize = int(size)
139
+ assert self._stepSize >= 1, "stepSize must be >= 1"
140
+ return
141
+
142
+ @property
143
+ def start_time(self) -> int:
144
+ """>= 0"""
145
+ return self._startTime
146
+
147
+ @start_time.setter
148
+ def start_time(self, time: int) -> None:
149
+ """>= 0"""
150
+ self._startTime = int(time)
151
+ assert self._startTime >= 0, "startTime must be >= 0"
152
+ return
153
+
154
+ @property
155
+ def num_time_steps(self) -> int:
156
+ """>= 1"""
157
+ return self._numTimeSteps
158
+
159
+ @num_time_steps.setter
160
+ def num_time_steps(self, count: int) -> None:
161
+ """>= 1"""
162
+ self._numTimeSteps = int(count)
163
+ assert self._numTimeSteps > 0, "numTimeSteps must be > 0"
164
+ return
165
+
166
+ def as_dictionary(self) -> Dict:
167
+ # https://stackoverflow.com/questions/38987/how-do-i-merge-two-dictionaries-in-a-single-expression
168
+ return {
169
+ **{
170
+ _CHANNELS: self.num_channels,
171
+ _DTK_VERSION: self.dtk_version,
172
+ _DATETIME: self.time_stamp,
173
+ _REPORT_TYPE: self.report_type,
174
+ _REPORT_VERSION: self.report_version,
175
+ _SIMULATION_TIMESTEP: self.step_size,
176
+ _START_TIME: self.start_time,
177
+ _TIMESTEPS: self.num_time_steps,
178
+ },
179
+ **self._tags,
180
+ }
181
+
182
+
183
+ class Channel(object):
184
+
185
+ def __init__(self, title: str, units: str, data: List) -> None:
186
+ self._title = title
187
+ self._units = units
188
+ self._data = data
189
+ return
190
+
191
+ @property
192
+ def title(self) -> str:
193
+ return self._title
194
+
195
+ @title.setter
196
+ def title(self, title: str) -> None:
197
+ self._title = f"{title}"
198
+ return
199
+
200
+ @property
201
+ def units(self) -> str:
202
+ return self._units
203
+
204
+ @units.setter
205
+ def units(self, units: str) -> None:
206
+ self._units = f"{units}"
207
+ return
208
+
209
+ @property
210
+ def data(self):
211
+ return self._data
212
+
213
+ def __getitem__(self, item):
214
+ """Index into channel data by time step"""
215
+ return self._data[item]
216
+
217
+ def __setitem__(self, key, value) -> None:
218
+ """Update channel data by time step"""
219
+ self._data[key] = value
220
+ return
221
+
222
+ def as_dictionary(self) -> Dict:
223
+ return {self.title: {_UNITS: self.units, _DATA: list(self.data)}}
224
+
225
+
226
+ class ChannelReport(object):
227
+
228
+ def __init__(self, filename: str = None, **kwargs):
229
+
230
+ if filename is not None:
231
+ assert isinstance(filename, str), "filename must be a string"
232
+ self._from_file(filename)
233
+ else:
234
+ self._header = Header(**kwargs)
235
+ self._channels = {}
236
+
237
+ return
238
+
239
+ @property
240
+ def header(self) -> Header:
241
+ return self._header
242
+
243
+ # pass-through to header
244
+
245
+ @property
246
+ def dtk_version(self) -> str:
247
+ return self._header.dtk_version
248
+
249
+ @dtk_version.setter
250
+ def dtk_version(self, version: str) -> None:
251
+ self._header.dtk_version = version
252
+ return
253
+
254
+ @property
255
+ def time_stamp(self) -> str:
256
+ return self._header.time_stamp
257
+
258
+ @time_stamp.setter
259
+ def time_stamp(self, time_stamp: Union[datetime, str]) -> None:
260
+ self._header.time_stamp = time_stamp
261
+ return
262
+
263
+ @property
264
+ def report_type(self) -> str:
265
+ return self._header.report_type
266
+
267
+ @report_type.setter
268
+ def report_type(self, report_type: str) -> None:
269
+ self._header.report_type = report_type
270
+ return
271
+
272
+ @property
273
+ def report_version(self) -> str:
274
+ """major.minor"""
275
+ return self._header.report_version
276
+
277
+ @report_version.setter
278
+ def report_version(self, version: str) -> None:
279
+ self._header.report_version = version
280
+ return
281
+
282
+ @property
283
+ def step_size(self) -> int:
284
+ """>= 1"""
285
+ return self._header.step_size
286
+
287
+ @step_size.setter
288
+ def step_size(self, size: int) -> None:
289
+ """>= 1"""
290
+ self._header.step_size = size
291
+ return
292
+
293
+ @property
294
+ def start_time(self) -> int:
295
+ """>= 0"""
296
+ return self._header.start_time
297
+
298
+ @start_time.setter
299
+ def start_time(self, time: int) -> None:
300
+ """>= 0"""
301
+ self._header.start_time = time
302
+ return
303
+
304
+ @property
305
+ def num_time_steps(self) -> int:
306
+ """> 0"""
307
+ return self._header.num_time_steps
308
+
309
+ @num_time_steps.setter
310
+ def num_time_steps(self, count: int):
311
+ """> 0"""
312
+ self._header.num_time_steps = count
313
+ return
314
+
315
+ # end pass-through
316
+
317
+ @property
318
+ def num_channels(self) -> int:
319
+ return len(self._channels)
320
+
321
+ @property
322
+ def channel_names(self) -> List:
323
+ return sorted(self._channels)
324
+
325
+ @property
326
+ def channels(self) -> Dict:
327
+ """Channel objects keyed on channel name/title"""
328
+ return self._channels
329
+
330
+ def __getitem__(self, item: str) -> Channel:
331
+ """Return Channel object by channel name/title"""
332
+ return self._channels[item]
333
+
334
+ def as_dataframe(self) -> pd.DataFrame:
335
+ """Return underlying data as a Pandas DataFrame"""
336
+ dataframe = pd.DataFrame(
337
+ {key: self.channels[key].data for key in self.channel_names}
338
+ )
339
+ return dataframe
340
+
341
+ def write_file(self, filename: str, indent: int = 0, separators=(",", ":")) -> None:
342
+ """Write inset chart to specified text file."""
343
+
344
+ # in case this was generated locally, lets do some consistency checks
345
+ assert len(self._channels) > 0, "Report has no channels."
346
+ counts = set([len(channel.data) for title, channel in self.channels.items()])
347
+ assert (
348
+ len(counts) == 1
349
+ ), f"Channels do not all have the same number of values ({counts})"
350
+
351
+ self._header.num_channels = len(self._channels)
352
+ self.num_time_steps = len(self._channels[self.channel_names[0]].data)
353
+
354
+ with open(filename, "w", encoding="utf-8") as file:
355
+ channels = {}
356
+ for _, channel in self.channels.items():
357
+ # https://stackoverflow.com/questions/38987/how-do-i-merge-two-dictionaries-in-a-single-expression
358
+ channels = {**channels, **channel.as_dictionary()}
359
+ chart = {_HEADER: self.header.as_dictionary(), _CHANNELS: channels}
360
+ json.dump(chart, file, indent=indent, separators=separators)
361
+
362
+ return
363
+
364
+ def _from_file(self, filename: str) -> None:
365
+
366
+ def validate_file(_jason) -> None:
367
+
368
+ assert _HEADER in _jason, f"'{filename}' missing '{_HEADER}' object."
369
+ assert (
370
+ _CHANNELS in _jason[_HEADER]
371
+ ), f"'{filename}' missing '{_HEADER}/{_CHANNELS}' key."
372
+ assert (
373
+ _TIMESTEPS in _jason[_HEADER]
374
+ ), f"'{filename}' missing '{_HEADER}/{_TIMESTEPS}' key."
375
+ assert _CHANNELS in _jason, f"'{filename}' missing '{_CHANNELS}' object."
376
+ num_channels = _jason[_HEADER][_CHANNELS]
377
+ channels_len = len(_jason[_CHANNELS])
378
+ assert num_channels == channels_len, (
379
+ f"'{filename}': "
380
+ + f"'{_HEADER}/{_CHANNELS}' ({num_channels}) does not match number of {_CHANNELS} ({channels_len})."
381
+ )
382
+
383
+ return
384
+
385
+ def validate_channel(_channel, _title, _header) -> None:
386
+
387
+ assert _UNITS in _channel, f"Channel '{_title}' missing '{_UNITS}' entry."
388
+ assert _DATA in _channel, f"Channel '{_title}' missing '{_DATA}' entry."
389
+ count = len(_channel[_DATA])
390
+ assert (
391
+ count == _header.num_time_steps
392
+ ), f"Channel '{title}' data values ({count}) does not match header Time_Steps ({_header.num_time_steps})."
393
+
394
+ return
395
+
396
+ with open(filename, "rb") as file:
397
+ jason = json.load(file)
398
+ validate_file(jason)
399
+
400
+ header_dict = jason[_HEADER]
401
+ self._header = Header(**header_dict)
402
+ self._channels = {}
403
+
404
+ channels = jason[_CHANNELS]
405
+ for title, channel in channels.items():
406
+ validate_channel(channel, title, self._header)
407
+ units = channel[_UNITS]
408
+ data = channel[_DATA]
409
+ self._channels[title] = Channel(title, units, data)
410
+
411
+ return
412
+
413
+ def to_csv(self, filename: Union[str, Path], channel_names: List[str]=None, transpose: bool=False) -> None:
414
+
415
+ """
416
+ Write each channel from the report to a row, CSV style, in the given file.
417
+
418
+ Channel name goes in the first column, channel data goes into subsequent columns.
419
+
420
+ Args:
421
+ filename: string or path specifying destination file
422
+ channel_names: optional list of channels (by name) to write to the file
423
+ transpose: write channels as columns rather than rows
424
+ """
425
+
426
+ if channel_names is None:
427
+ channel_names = self.channel_names
428
+
429
+ if not transpose: # default
430
+ data_frame = pd.DataFrame([[channel_name] + list(self[channel_name]) for channel_name in channel_names])
431
+ # data_frame = pd.DataFrame(([channel_name] + list(self[channel_name]) for channel_name in channel_names))
432
+ data_frame.to_csv(filename, header=False, index=False)
433
+ else: # transposed
434
+ self.as_dataframe().to_csv(filename, header=True, index=True, index_label="timestep")
435
+
436
+ return
@@ -0,0 +1,64 @@
1
+ import os
2
+ import json
3
+ import pandas as pd
4
+
5
+ def _get_sim_years( output_path ):
6
+
7
+ if not os.path.exists( "config.json" ):
8
+ return None
9
+ with open( "config.json" ) as config_fp:
10
+ config = json.load( config_fp )
11
+ if "Base_Year" not in config["parameters"]:
12
+ return None
13
+
14
+ base_year = config["parameters"]["Base_Year"]
15
+ step = config["parameters"]["Simulation_Timestep"]/365
16
+ steps = round(config["parameters"]["Simulation_Duration"]/step)
17
+
18
+ sim_year = [ base_year + step * x for x in range(steps) ]
19
+ return sim_year
20
+
21
+ def inset_chart_json_to_csv_dataframe_pd( output_path ):
22
+ """
23
+ Convert InsetChart.json file in 'output_path' to InsetChart.csv.
24
+ Adding Simulation_Year column if Base_Year exists in config.json.
25
+
26
+ Args:
27
+
28
+ output_path (str): Subdirectory in which to find InsetChart.json
29
+
30
+ Returns:
31
+ N/A
32
+
33
+ Raises:
34
+ - ValueError if InsetChart.json can't be found.
35
+ - ValueError if InsetChart.csv can't be written.
36
+ """
37
+
38
+ icj_path = os.path.join( output_path, "InsetChart.json" )
39
+ if not os.path.exists( icj_path ):
40
+ raise ValueError( f"InsetChart.json not found at {output_path}." )
41
+
42
+ # Load JSON data from file
43
+ with open( icj_path ) as fp:
44
+ icj = json.load( fp )
45
+
46
+ optional_years_channel = _get_sim_years( output_path )
47
+ if optional_years_channel:
48
+ icj["Channels"]["Simulation_Year"]["Data"] = optional_years_channel
49
+
50
+ # Create an empty DataFrame
51
+ df = pd.DataFrame()
52
+
53
+ # Iterate over the Channels keys and extract time series data
54
+ for channel, values in icj["Channels"].items():
55
+ # Create a column in the DataFrame for each channel
56
+ df[channel] = values["Data"]
57
+
58
+ try:
59
+ # Convert DataFrame to CSV
60
+ csv_path = os.path.join( output_path, "InsetChart.csv" )
61
+ df.to_csv(csv_path, index=False)
62
+ except Exception as ex:
63
+ print( f"ERROR: Exception {ex} while writing csv dataframe of InsetChart.json to disk." )
64
+ raise ValueError( ex )