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