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
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)
|