aiphoria 0.0.1__py3-none-any.whl → 0.8.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.
- aiphoria/__init__.py +59 -0
- aiphoria/core/__init__.py +55 -0
- aiphoria/core/builder.py +305 -0
- aiphoria/core/datachecker.py +1808 -0
- aiphoria/core/dataprovider.py +806 -0
- aiphoria/core/datastructures.py +1686 -0
- aiphoria/core/datavisualizer.py +431 -0
- aiphoria/core/datavisualizer_data/LICENSE +21 -0
- aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
- aiphoria/core/datavisualizer_data/pako.min.js +2 -0
- aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
- aiphoria/core/flowmodifiersolver.py +1754 -0
- aiphoria/core/flowsolver.py +1472 -0
- aiphoria/core/logger.py +113 -0
- aiphoria/core/network_graph.py +136 -0
- aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
- aiphoria/core/network_graph_data/echarts_min.js +45 -0
- aiphoria/core/network_graph_data/network_graph.html +76 -0
- aiphoria/core/network_graph_data/network_graph.js +1391 -0
- aiphoria/core/parameters.py +269 -0
- aiphoria/core/types.py +20 -0
- aiphoria/core/utils.py +362 -0
- aiphoria/core/visualizer_parameters.py +7 -0
- aiphoria/data/example_scenario.xlsx +0 -0
- aiphoria/example.py +66 -0
- aiphoria/lib/docs/dynamic_stock.py +124 -0
- aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
- aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
- aiphoria/lib/odym/modules/__init__.py +1 -0
- aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
- aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
- aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
- aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
- aiphoria/lib/odym/modules/test/__init__.py +2 -0
- aiphoria/runner.py +678 -0
- aiphoria-0.8.0.dist-info/METADATA +119 -0
- aiphoria-0.8.0.dist-info/RECORD +40 -0
- {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
- aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
- aiphoria-0.0.1.dist-info/METADATA +0 -5
- aiphoria-0.0.1.dist-info/RECORD +0 -5
- {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import List, Union, Any, Dict
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from .datastructures import Process, Flow, Stock, FlowModifier, ScenarioDefinition, Color
|
|
6
|
+
from .parameters import ParameterName, ParameterFillMethod, StockDistributionType, ParameterScenarioType
|
|
7
|
+
|
|
8
|
+
# Suppress openpyxl warnings about Data Validation being suppressed
|
|
9
|
+
warnings.filterwarnings('ignore', category=UserWarning, module="openpyxl")
|
|
10
|
+
warnings.filterwarnings('ignore', category=FutureWarning, module="openpyxl")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataProvider(object):
|
|
14
|
+
def __init__(self, filename: str = "",
|
|
15
|
+
sheet_settings_name: str = "Settings",
|
|
16
|
+
sheet_settings_col_range: Union[str, int] = "B:C",
|
|
17
|
+
sheet_settings_skip_num_rows: int = 5,
|
|
18
|
+
):
|
|
19
|
+
|
|
20
|
+
self._workbook = None
|
|
21
|
+
self._param_name_to_value = {}
|
|
22
|
+
self._processes: List[Process] = []
|
|
23
|
+
self._flows: List[Flow] = []
|
|
24
|
+
self._stocks: List[Stock] = []
|
|
25
|
+
self._scenario_definitions: List[ScenarioDefinition] = []
|
|
26
|
+
self._colors: List[Color] = []
|
|
27
|
+
self._sheet_name_processes: Union[None, str] = None
|
|
28
|
+
self._sheet_name_flows: Union[None, str] = None
|
|
29
|
+
self._sheet_name_scenarios: Union[None, str] = None
|
|
30
|
+
self._sheet_name_colors: Union[None, str] = None
|
|
31
|
+
self._sheet_name_process_positions: Union[None, str] = None
|
|
32
|
+
self._year_to_process_id_to_position = {}
|
|
33
|
+
|
|
34
|
+
# Check that all required keys exists
|
|
35
|
+
required_params = [
|
|
36
|
+
[ParameterName.SheetNameProcesses,
|
|
37
|
+
str,
|
|
38
|
+
"Sheet name that contains data for Processes, (e.g. Processes)",
|
|
39
|
+
],
|
|
40
|
+
[ParameterName.SkipNumRowsProcesses,
|
|
41
|
+
int,
|
|
42
|
+
"Number of rows to skip when reading data for Processes (e.g. 2). NOTE: Header row must be the first row to read!",
|
|
43
|
+
],
|
|
44
|
+
[ParameterName.IgnoreColumnsProcesses,
|
|
45
|
+
list,
|
|
46
|
+
"Columns to ignore when reading Process sheet"
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
# Flow related
|
|
50
|
+
[ParameterName.SheetNameFlows,
|
|
51
|
+
str,
|
|
52
|
+
"Sheet name that contains data for Flows (e.g. Flows)",
|
|
53
|
+
],
|
|
54
|
+
[ParameterName.SkipNumRowsFlows,
|
|
55
|
+
int,
|
|
56
|
+
"Number of rows to skip when reading data for Processes (e.g. 2). NOTE: Header row must be the first row to read!"
|
|
57
|
+
],
|
|
58
|
+
[ParameterName.IgnoreColumnsFlows,
|
|
59
|
+
list,
|
|
60
|
+
"Columns to ignore when reading Flows sheet"
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
# Model related
|
|
64
|
+
[ParameterName.StartYear,
|
|
65
|
+
int,
|
|
66
|
+
"Starting year of the model"
|
|
67
|
+
],
|
|
68
|
+
[ParameterName.EndYear,
|
|
69
|
+
int,
|
|
70
|
+
"Ending year of the model, included in time range"
|
|
71
|
+
],
|
|
72
|
+
[ParameterName.DetectYearRange,
|
|
73
|
+
bool,
|
|
74
|
+
"Detect the year range automatically from file"
|
|
75
|
+
],
|
|
76
|
+
[ParameterName.UseVirtualFlows,
|
|
77
|
+
bool,
|
|
78
|
+
"Use virtual flows (create missing flows for Processes that have imbalance of input and output flows, i.e. unreported flows)"
|
|
79
|
+
],
|
|
80
|
+
[ParameterName.VirtualFlowsEpsilon,
|
|
81
|
+
float,
|
|
82
|
+
"Maximum allowed absolute difference of process input and outputs before creating virtual flow"
|
|
83
|
+
],
|
|
84
|
+
[ParameterName.BaselineValueName,
|
|
85
|
+
str,
|
|
86
|
+
"Baseline value name. Name of the value type that is used as baseline e.g. 'Solid wood equivalent'"
|
|
87
|
+
],
|
|
88
|
+
[ParameterName.BaselineUnitName,
|
|
89
|
+
str,
|
|
90
|
+
"Baseline unit name. This is used with relative flows when exporting flow data to CSVs."
|
|
91
|
+
],
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Optional parameters entry structure:
|
|
95
|
+
# Name of the parameter, expected value type, comment, default value
|
|
96
|
+
optional_params = [
|
|
97
|
+
[ParameterName.ConversionFactorCToCO2,
|
|
98
|
+
float,
|
|
99
|
+
"Conversion factor from C to CO2",
|
|
100
|
+
None,
|
|
101
|
+
],
|
|
102
|
+
|
|
103
|
+
[ParameterName.FillMissingAbsoluteFlows,
|
|
104
|
+
bool,
|
|
105
|
+
"Fill missing absolute flows with previous valid flow data?",
|
|
106
|
+
True,
|
|
107
|
+
],
|
|
108
|
+
|
|
109
|
+
[ParameterName.FillMissingRelativeFlows,
|
|
110
|
+
bool,
|
|
111
|
+
"Fill missing relative flows with previous valid flow data?",
|
|
112
|
+
True,
|
|
113
|
+
],
|
|
114
|
+
|
|
115
|
+
[ParameterName.FillMethod,
|
|
116
|
+
str,
|
|
117
|
+
"Fill method if either fill_missing_absolute_flows or fill_missing_relative_flows is enabled",
|
|
118
|
+
ParameterFillMethod.Zeros,
|
|
119
|
+
],
|
|
120
|
+
[ParameterName.UseScenarios,
|
|
121
|
+
bool,
|
|
122
|
+
"Run scenarios",
|
|
123
|
+
True,
|
|
124
|
+
],
|
|
125
|
+
[ParameterName.SheetNameScenarios,
|
|
126
|
+
str,
|
|
127
|
+
"Sheet name that contains data for scenarios (flow modifiers and constraints)",
|
|
128
|
+
"Scenarios",
|
|
129
|
+
],
|
|
130
|
+
[ParameterName.IgnoreColumnsScenarios,
|
|
131
|
+
list,
|
|
132
|
+
"Columns to ignore when reading Scenarios sheet",
|
|
133
|
+
[],
|
|
134
|
+
],
|
|
135
|
+
[ParameterName.ScenarioType,
|
|
136
|
+
str,
|
|
137
|
+
"Scenario type (Constrained / Unconstrained)",
|
|
138
|
+
ParameterScenarioType.Constrained,
|
|
139
|
+
# ParameterScenarioType.Unconstrained,
|
|
140
|
+
],
|
|
141
|
+
[ParameterName.SheetNameColors,
|
|
142
|
+
str,
|
|
143
|
+
"Sheet name that contains data for transformation stage colors (e.g. Colors)",
|
|
144
|
+
None,
|
|
145
|
+
],
|
|
146
|
+
[ParameterName.IgnoreColumnsColors,
|
|
147
|
+
list,
|
|
148
|
+
"Columns to ignore when reading Colors sheet",
|
|
149
|
+
[],
|
|
150
|
+
],
|
|
151
|
+
[ParameterName.CreateNetworkGraphs,
|
|
152
|
+
bool,
|
|
153
|
+
"Create network graphs to visualize process connections for each scenario",
|
|
154
|
+
False,
|
|
155
|
+
],
|
|
156
|
+
[ParameterName.CreateSankeyCharts,
|
|
157
|
+
bool,
|
|
158
|
+
"Create Sankey charts for each scenario",
|
|
159
|
+
True,
|
|
160
|
+
],
|
|
161
|
+
[ParameterName.OutputPath,
|
|
162
|
+
str,
|
|
163
|
+
"Path to directory where all output is created (relative to running script)",
|
|
164
|
+
"output",
|
|
165
|
+
],
|
|
166
|
+
[ParameterName.ShowPlots,
|
|
167
|
+
bool,
|
|
168
|
+
"Show Matplotlib plots",
|
|
169
|
+
True,
|
|
170
|
+
],
|
|
171
|
+
[ParameterName.VisualizeInflowsToProcesses,
|
|
172
|
+
list,
|
|
173
|
+
"Create inflow visualization and export data for process IDs defined in here. " +
|
|
174
|
+
"Each process ID must be separated by comma (',')",
|
|
175
|
+
[],
|
|
176
|
+
],
|
|
177
|
+
[ParameterName.PrioritizeLocations,
|
|
178
|
+
list,
|
|
179
|
+
"Prioritize process locations. If stock is outflowing to prioritized location, then "
|
|
180
|
+
"ignore that amount as inflows to stock. This is to simulate trade flows happening during the timestep "
|
|
181
|
+
"which should not go in to the stock",
|
|
182
|
+
[],
|
|
183
|
+
],
|
|
184
|
+
[ParameterName.PrioritizeTransformationStages,
|
|
185
|
+
list,
|
|
186
|
+
"Prioritize process transformation stages. If stock is outflowing to prioritized transformation stage, "
|
|
187
|
+
"then ignore that amount as inflows to stock. This is to simulate trade flows happening during "
|
|
188
|
+
"the timestep which should not go in to the stock",
|
|
189
|
+
[],
|
|
190
|
+
],
|
|
191
|
+
[ParameterName.SheetNameProcessPositions,
|
|
192
|
+
str,
|
|
193
|
+
"Sheet name that contains data for process positions in normalized format (position data in range [0,1])",
|
|
194
|
+
None,
|
|
195
|
+
],
|
|
196
|
+
[ParameterName.CheckErrors,
|
|
197
|
+
bool,
|
|
198
|
+
"Check errors when building data (development)",
|
|
199
|
+
True,
|
|
200
|
+
]
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
param_type_to_str = {int: "integer", float: "float", str: "string", bool: "boolean", list: "list"}
|
|
204
|
+
|
|
205
|
+
# Read settings sheet from the file
|
|
206
|
+
param_name_to_value = {}
|
|
207
|
+
try:
|
|
208
|
+
with pd.ExcelFile(filename) as xls:
|
|
209
|
+
try:
|
|
210
|
+
sheet_settings = pd.read_excel(io=xls,
|
|
211
|
+
sheet_name=sheet_settings_name,
|
|
212
|
+
usecols=sheet_settings_col_range,
|
|
213
|
+
skiprows=sheet_settings_skip_num_rows,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
for row_index, row in sheet_settings.iterrows():
|
|
217
|
+
param_name, param_value = row
|
|
218
|
+
param_name_to_value[param_name] = param_value
|
|
219
|
+
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
raise Exception("DataProvider: Settings sheet '{}' not found in file {}!".format(
|
|
222
|
+
sheet_settings_name, filename))
|
|
223
|
+
|
|
224
|
+
except FileNotFoundError as ex:
|
|
225
|
+
raise Exception("File not found: {}".format(filename))
|
|
226
|
+
|
|
227
|
+
# Check that all required params are defined in settings sheet
|
|
228
|
+
missing_params = []
|
|
229
|
+
for entry in required_params:
|
|
230
|
+
param_name, param_type, param_desc = entry
|
|
231
|
+
if param_name not in param_name_to_value:
|
|
232
|
+
missing_params.append(entry)
|
|
233
|
+
|
|
234
|
+
# Print missing parameters and information
|
|
235
|
+
if missing_params:
|
|
236
|
+
print("DataProvider: Settings sheet (Sheet) is missing required following parameters:")
|
|
237
|
+
max_param_name_len = 0
|
|
238
|
+
for entry in missing_params:
|
|
239
|
+
param_name = entry[0]
|
|
240
|
+
max_param_name_len = len(param_name) if len(param_name) > max_param_name_len else max_param_name_len
|
|
241
|
+
|
|
242
|
+
for entry in missing_params:
|
|
243
|
+
param_name, param_type, param_desc = entry
|
|
244
|
+
fixed_param_name = "{:" + str(max_param_name_len) + "}"
|
|
245
|
+
fixed_param_name = fixed_param_name.format(param_name)
|
|
246
|
+
print("\t{} (type: {}). {}".format(fixed_param_name, param_type_to_str[param_type], param_desc))
|
|
247
|
+
|
|
248
|
+
raise Exception(-1)
|
|
249
|
+
|
|
250
|
+
# Check that required and optionals parameters are correct types
|
|
251
|
+
for entry in required_params:
|
|
252
|
+
param_name, param_type, param_desc = entry
|
|
253
|
+
if param_name in param_name_to_value:
|
|
254
|
+
found_param_value = param_name_to_value[param_name]
|
|
255
|
+
found_param_type = type(found_param_value)
|
|
256
|
+
try:
|
|
257
|
+
if param_type is bool:
|
|
258
|
+
found_param_value = self._to_bool(found_param_value)
|
|
259
|
+
|
|
260
|
+
if param_type is list:
|
|
261
|
+
found_param_value = self._to_list(found_param_value)
|
|
262
|
+
|
|
263
|
+
found_param_value = param_type(found_param_value)
|
|
264
|
+
self._param_name_to_value[param_name] = found_param_value
|
|
265
|
+
except ValueError as e:
|
|
266
|
+
print("Invalid type for required parameter '{}': expected {}, got {}".format(
|
|
267
|
+
param_name, param_type_to_str[param_type], param_type_to_str[found_param_type]))
|
|
268
|
+
|
|
269
|
+
# Check that optional parameters are correct types
|
|
270
|
+
for entry in optional_params:
|
|
271
|
+
param_name, param_type, param_desc, param_default_value = entry
|
|
272
|
+
if param_name in param_name_to_value:
|
|
273
|
+
found_param_value = param_name_to_value[param_name]
|
|
274
|
+
found_param_type = type(found_param_value)
|
|
275
|
+
try:
|
|
276
|
+
if param_type is bool:
|
|
277
|
+
found_param_value = self._to_bool(found_param_value)
|
|
278
|
+
|
|
279
|
+
if param_type is list:
|
|
280
|
+
found_param_value = self._to_list(found_param_value)
|
|
281
|
+
|
|
282
|
+
self._param_name_to_value[param_name] = param_type(found_param_value)
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
print("Invalid type for optional parameter '{}': expected {}, got {}".format(
|
|
285
|
+
param_name, param_type_to_str[param_type], param_type_to_str[found_param_type]))
|
|
286
|
+
|
|
287
|
+
# Check that FillMethod has valid value
|
|
288
|
+
if param_name is ParameterName.FillMethod:
|
|
289
|
+
# Convert found param name and valid fill method names to lowercase
|
|
290
|
+
# and check if found param name is one of the valid method names
|
|
291
|
+
valid_fill_method_names = [fill_method_name for fill_method_name in ParameterFillMethod]
|
|
292
|
+
found_param_value_lower = found_param_value.lower().strip()
|
|
293
|
+
valid_method_names_lower = [name.lower().strip() for name in valid_fill_method_names]
|
|
294
|
+
if found_param_value_lower in valid_method_names_lower:
|
|
295
|
+
# Get the actual parameter name from ParameterFillMethod-enum
|
|
296
|
+
fill_method_index = valid_method_names_lower.index(found_param_value_lower)
|
|
297
|
+
found_param_value = valid_fill_method_names[fill_method_index]
|
|
298
|
+
self._param_name_to_value[param_name] = found_param_value
|
|
299
|
+
else:
|
|
300
|
+
print("{} not valid value for {}! ".format(found_param_value, param_name), end="")
|
|
301
|
+
print("Valid values are: ", end="")
|
|
302
|
+
for index, method_name in enumerate(valid_fill_method_names):
|
|
303
|
+
print(method_name, end="")
|
|
304
|
+
if index < len(valid_fill_method_names) - 1:
|
|
305
|
+
print(", ", end="")
|
|
306
|
+
else:
|
|
307
|
+
print("")
|
|
308
|
+
|
|
309
|
+
self._param_name_to_value[param_name] = param_default_value
|
|
310
|
+
print("")
|
|
311
|
+
raise Exception(-1)
|
|
312
|
+
|
|
313
|
+
elif param_name is ParameterName.ScenarioType:
|
|
314
|
+
valid_scenario_type_names = [scenario_type_name for scenario_type_name in ParameterScenarioType]
|
|
315
|
+
found_param_value_lower = found_param_value.lower().strip()
|
|
316
|
+
valid_method_names_lower = [name.lower().strip() for name in valid_scenario_type_names]
|
|
317
|
+
if found_param_value_lower in valid_method_names_lower:
|
|
318
|
+
# Get the actual parameter name from ParameterFillMethod-enum
|
|
319
|
+
scenario_type_index = valid_method_names_lower.index(found_param_value_lower)
|
|
320
|
+
found_param_value = valid_scenario_type_names[scenario_type_index]
|
|
321
|
+
self._param_name_to_value[param_name] = found_param_value
|
|
322
|
+
else:
|
|
323
|
+
print("{} not valid value for {}! ".format(found_param_value, param_name), end="")
|
|
324
|
+
print("Valid values are: ", end="")
|
|
325
|
+
for index, method_name in enumerate(valid_scenario_type_names):
|
|
326
|
+
print(method_name, end="")
|
|
327
|
+
if index < len(valid_scenario_type_names) - 1:
|
|
328
|
+
print(", ", end="")
|
|
329
|
+
else:
|
|
330
|
+
print("")
|
|
331
|
+
|
|
332
|
+
self._param_name_to_value[param_name] = param_default_value
|
|
333
|
+
print("")
|
|
334
|
+
raise Exception(-1)
|
|
335
|
+
|
|
336
|
+
else:
|
|
337
|
+
# Use default optional parameter value
|
|
338
|
+
self._param_name_to_value[param_name] = param_default_value
|
|
339
|
+
|
|
340
|
+
# ********************************************
|
|
341
|
+
# * Read processes and flows from Excel file *
|
|
342
|
+
# ********************************************
|
|
343
|
+
|
|
344
|
+
# Create Processes and Flows
|
|
345
|
+
sheet_name_processes = self._param_name_to_value.get(ParameterName.SheetNameProcesses, None)
|
|
346
|
+
ignore_columns_processes = self._param_name_to_value.get(ParameterName.IgnoreColumnsProcesses, [])
|
|
347
|
+
skip_num_rows_processes = self._param_name_to_value.get(ParameterName.SkipNumRowsProcesses, None)
|
|
348
|
+
|
|
349
|
+
sheet_name_flows = self._param_name_to_value.get(ParameterName.SheetNameFlows, None)
|
|
350
|
+
ignore_columns_flows = self._param_name_to_value.get(ParameterName.IgnoreColumnsFlows, [])
|
|
351
|
+
skip_num_rows_flows = self._param_name_to_value.get(ParameterName.SkipNumRowsFlows, None)
|
|
352
|
+
|
|
353
|
+
sheet_name_scenarios = self._param_name_to_value.get(ParameterName.SheetNameScenarios, None)
|
|
354
|
+
ignore_columns_scenarios = self._param_name_to_value.get(ParameterName.IgnoreColumnsScenarios, [])
|
|
355
|
+
skip_num_rows_scenarios = self._param_name_to_value.get(ParameterName.SkipNumRowsScenarios, None)
|
|
356
|
+
|
|
357
|
+
# Optional
|
|
358
|
+
# Colors
|
|
359
|
+
sheet_name_colors = self._param_name_to_value.get(ParameterName.SheetNameColors, None)
|
|
360
|
+
ignore_columns_colors = self._param_name_to_value.get(ParameterName.IgnoreColumnsColors, [])
|
|
361
|
+
skip_num_rows_colors = self._param_name_to_value.get(ParameterName.SkipNumRowsColors, None)
|
|
362
|
+
|
|
363
|
+
# Process positions
|
|
364
|
+
sheet_name_process_positions = self._param_name_to_value.get(ParameterName.SheetNameProcessPositions, None)
|
|
365
|
+
|
|
366
|
+
use_scenarios = self._param_name_to_value[ParameterName.UseScenarios]
|
|
367
|
+
if not use_scenarios:
|
|
368
|
+
sheet_name_scenarios = ""
|
|
369
|
+
|
|
370
|
+
# Sheet name to DataFrame
|
|
371
|
+
sheets = {}
|
|
372
|
+
try:
|
|
373
|
+
with pd.ExcelFile(filename) as xls:
|
|
374
|
+
try:
|
|
375
|
+
sheet_processes = pd.read_excel(xls,
|
|
376
|
+
sheet_name=sheet_name_processes,
|
|
377
|
+
skiprows=skip_num_rows_processes)
|
|
378
|
+
sheets[sheet_name_processes] = self._drop_ignored_columns_from_sheet(sheet_processes,
|
|
379
|
+
ignore_columns_processes)
|
|
380
|
+
except ValueError:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
sheet_flows = pd.read_excel(xls,
|
|
385
|
+
sheet_name=sheet_name_flows,
|
|
386
|
+
skiprows=skip_num_rows_flows)
|
|
387
|
+
sheets[sheet_name_flows] = self._drop_ignored_columns_from_sheet(sheet_flows,
|
|
388
|
+
ignore_columns_flows)
|
|
389
|
+
except ValueError:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
# Optionals
|
|
393
|
+
if use_scenarios:
|
|
394
|
+
try:
|
|
395
|
+
sheet_scenarios = pd.read_excel(xls,
|
|
396
|
+
sheet_name=sheet_name_scenarios,
|
|
397
|
+
skiprows=skip_num_rows_scenarios)
|
|
398
|
+
sheets[sheet_name_scenarios] = self._drop_ignored_columns_from_sheet(sheet_scenarios,
|
|
399
|
+
ignore_columns_scenarios)
|
|
400
|
+
|
|
401
|
+
except ValueError:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
sheet_colors = pd.read_excel(xls,
|
|
406
|
+
sheet_name=sheet_name_colors,
|
|
407
|
+
skiprows=skip_num_rows_colors)
|
|
408
|
+
sheets[sheet_name_colors] = self._drop_ignored_columns_from_sheet(sheet_colors,
|
|
409
|
+
ignore_columns_colors)
|
|
410
|
+
except ValueError:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
sheet_process_positions = pd.read_excel(xls, sheet_name=sheet_name_process_positions)
|
|
415
|
+
sheets[sheet_name_process_positions] = sheet_process_positions
|
|
416
|
+
except ValueError:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
except FileNotFoundError:
|
|
420
|
+
raise Exception("Settings file '{}' not found".format(filename))
|
|
421
|
+
|
|
422
|
+
# Check that all the required sheets exists
|
|
423
|
+
required_sheet_names = [sheet_name_processes, sheet_name_flows]
|
|
424
|
+
missing_sheet_names = self._check_missing_sheet_names(required_sheet_names, sheets)
|
|
425
|
+
if missing_sheet_names:
|
|
426
|
+
print("Settings file '{}' is missing following required sheets:".format(filename))
|
|
427
|
+
for key in missing_sheet_names:
|
|
428
|
+
print("\t- {}".format(key))
|
|
429
|
+
raise Exception(-1)
|
|
430
|
+
|
|
431
|
+
self._sheet_name_processes = sheet_name_processes
|
|
432
|
+
self._sheet_name_flows = sheet_name_flows
|
|
433
|
+
|
|
434
|
+
# Check that all optional sheets exists (only if optional sheets have been defined)
|
|
435
|
+
optional_sheet_names = [sheet_name_colors, sheet_name_process_positions]
|
|
436
|
+
|
|
437
|
+
if sheet_name_scenarios:
|
|
438
|
+
optional_sheet_names.append(sheet_name_scenarios)
|
|
439
|
+
|
|
440
|
+
missing_sheet_names = self._check_missing_sheet_names(optional_sheet_names, sheets)
|
|
441
|
+
if missing_sheet_names:
|
|
442
|
+
errors = []
|
|
443
|
+
s = "Settings file '{}' is missing following sheets:".format(filename)
|
|
444
|
+
errors.append(s)
|
|
445
|
+
for key in missing_sheet_names:
|
|
446
|
+
s = "\t- {}".format(key)
|
|
447
|
+
errors.append(s)
|
|
448
|
+
raise Exception(errors)
|
|
449
|
+
|
|
450
|
+
self._sheet_name_scenarios = sheet_name_scenarios
|
|
451
|
+
self._sheet_name_colors = sheet_name_colors
|
|
452
|
+
self._sheet_name_process_positions = sheet_name_process_positions
|
|
453
|
+
|
|
454
|
+
# Create Processes
|
|
455
|
+
rows_processes = []
|
|
456
|
+
df_processes = sheets[self._sheet_name_processes]
|
|
457
|
+
for (row_index, row) in df_processes.iterrows():
|
|
458
|
+
row = self._convert_row_nan_to_none(row)
|
|
459
|
+
rows_processes.append(row)
|
|
460
|
+
|
|
461
|
+
self._processes = self._create_objects_from_rows(Process,
|
|
462
|
+
rows_processes,
|
|
463
|
+
row_start=skip_num_rows_processes)
|
|
464
|
+
|
|
465
|
+
# Create Flows
|
|
466
|
+
rows_flows = []
|
|
467
|
+
df_flows = sheets[self._sheet_name_flows]
|
|
468
|
+
for (row_index, row) in df_flows.iterrows():
|
|
469
|
+
row = self._convert_row_nan_to_none(row)
|
|
470
|
+
rows_flows.append(row)
|
|
471
|
+
|
|
472
|
+
self._flows = self._create_objects_from_rows(Flow,
|
|
473
|
+
rows_flows,
|
|
474
|
+
row_start=skip_num_rows_flows)
|
|
475
|
+
|
|
476
|
+
# Create Stocks from Processes
|
|
477
|
+
self._stocks = self._create_stocks_from_processes(self._processes)
|
|
478
|
+
|
|
479
|
+
# Create alternative scenarios (optional)
|
|
480
|
+
if sheet_name_scenarios:
|
|
481
|
+
rows_scenarios = []
|
|
482
|
+
df_scenarios = sheets[self._sheet_name_scenarios]
|
|
483
|
+
for (row_index, row) in df_scenarios.iterrows():
|
|
484
|
+
row = self._convert_row_nan_to_none(row)
|
|
485
|
+
rows_scenarios.append(row)
|
|
486
|
+
|
|
487
|
+
self._scenario_definitions = self._create_scenario_definitions(rows_scenarios)
|
|
488
|
+
|
|
489
|
+
# Create colors (optional)
|
|
490
|
+
if sheet_name_colors:
|
|
491
|
+
rows_colors = []
|
|
492
|
+
df_colors = sheets[self._sheet_name_colors]
|
|
493
|
+
for (row_index, row) in df_colors.iterrows():
|
|
494
|
+
row = self._convert_row_nan_to_none(row)
|
|
495
|
+
rows_colors.append(row)
|
|
496
|
+
|
|
497
|
+
self._colors = self._create_colors(rows_colors)
|
|
498
|
+
|
|
499
|
+
# Read and update detailed Process positions (optional)
|
|
500
|
+
if sheet_name_process_positions:
|
|
501
|
+
|
|
502
|
+
# Map year -> Process ID -> position
|
|
503
|
+
year_to_process_id_to_position = {}
|
|
504
|
+
df_process_positions = sheets[sheet_name_process_positions]
|
|
505
|
+
for (row_index, row) in df_process_positions.iterrows():
|
|
506
|
+
year = row.iloc[0]
|
|
507
|
+
process_id = row.iloc[1]
|
|
508
|
+
x = self._to_normalized_float(row.iloc[2], 3)
|
|
509
|
+
y = self._to_normalized_float(row.iloc[3], 3)
|
|
510
|
+
normalized_x = np.clip(x, 0.001, 0.999)
|
|
511
|
+
normalized_y = np.clip(y, 0.001, 0.999)
|
|
512
|
+
|
|
513
|
+
if year not in year_to_process_id_to_position:
|
|
514
|
+
year_to_process_id_to_position[year] = {}
|
|
515
|
+
|
|
516
|
+
process_id_to_position = year_to_process_id_to_position[year]
|
|
517
|
+
if process_id not in process_id_to_position:
|
|
518
|
+
process_id_to_position[process_id] = [normalized_x, normalized_y]
|
|
519
|
+
|
|
520
|
+
self._year_to_process_id_to_position = year_to_process_id_to_position
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def sheet_name_processes(self):
|
|
524
|
+
return self._sheet_name_processes
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def sheet_name_flows(self):
|
|
528
|
+
return self._sheet_name_flows
|
|
529
|
+
|
|
530
|
+
def _check_missing_sheet_names(self, required_sheet_names: List[str], sheets: Dict[str, pd.DataFrame]):
|
|
531
|
+
missing_sheet_names = []
|
|
532
|
+
for key in required_sheet_names:
|
|
533
|
+
if key not in sheets:
|
|
534
|
+
missing_sheet_names.append(key)
|
|
535
|
+
|
|
536
|
+
return missing_sheet_names
|
|
537
|
+
|
|
538
|
+
def _create_objects_from_rows(self, object_type=None, rows=None, row_start=-1) -> List:
|
|
539
|
+
if rows is None:
|
|
540
|
+
rows = []
|
|
541
|
+
|
|
542
|
+
result = []
|
|
543
|
+
if not object_type:
|
|
544
|
+
return result
|
|
545
|
+
|
|
546
|
+
row_number = row_start + 2
|
|
547
|
+
for row in rows:
|
|
548
|
+
if not self._is_row_valid(row):
|
|
549
|
+
row_number += 1
|
|
550
|
+
continue
|
|
551
|
+
|
|
552
|
+
new_instance = object_type(row, row_number)
|
|
553
|
+
if new_instance.is_valid():
|
|
554
|
+
result.append(new_instance)
|
|
555
|
+
|
|
556
|
+
row_number += 1
|
|
557
|
+
|
|
558
|
+
return result
|
|
559
|
+
|
|
560
|
+
def _create_stocks_from_processes(self, processes=None) -> List[Stock]:
|
|
561
|
+
# Create stocks only for Processes that have lifetime > 1
|
|
562
|
+
# NOTE: If stock type is LandfillDecay* then set lifetime to 1 by default
|
|
563
|
+
# if stock lifetime is not defined
|
|
564
|
+
if processes is None:
|
|
565
|
+
processes = []
|
|
566
|
+
|
|
567
|
+
# Ignore stock lifetime if stock distribution type is any of these
|
|
568
|
+
ignore_stock_lifetime_for_types = {StockDistributionType.LandfillDecayWood,
|
|
569
|
+
StockDistributionType.LandfillDecayPaper}
|
|
570
|
+
result = []
|
|
571
|
+
for process in processes:
|
|
572
|
+
ignore_stock_lifetime = process.stock_distribution_type in ignore_stock_lifetime_for_types
|
|
573
|
+
|
|
574
|
+
# Add stock lifetime if stock type is in ignore_stock_lifetime
|
|
575
|
+
# This is used for LandfillDecay* stock types and stock lifetime is needed
|
|
576
|
+
# so that ODYM is able to calculate decays properly
|
|
577
|
+
if ignore_stock_lifetime and process.stock_lifetime == 0:
|
|
578
|
+
process.stock_lifetime = 1
|
|
579
|
+
|
|
580
|
+
if process.stock_lifetime == 0:
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
new_stock = Stock(process, row_number=process.row_number)
|
|
584
|
+
if new_stock.is_valid():
|
|
585
|
+
result.append(new_stock)
|
|
586
|
+
|
|
587
|
+
return result
|
|
588
|
+
|
|
589
|
+
def _create_scenario_definitions(self, rows: List[Any] = None) -> List[ScenarioDefinition]:
|
|
590
|
+
if not rows:
|
|
591
|
+
rows = []
|
|
592
|
+
|
|
593
|
+
flow_modifiers = []
|
|
594
|
+
for row_index, row in enumerate(rows):
|
|
595
|
+
new_flow_modifier = FlowModifier(row)
|
|
596
|
+
new_flow_modifier.row_number = row_index + 2 # Header = row 1
|
|
597
|
+
if new_flow_modifier.is_valid():
|
|
598
|
+
flow_modifiers.append(new_flow_modifier)
|
|
599
|
+
|
|
600
|
+
# Build scenario mappings
|
|
601
|
+
result = []
|
|
602
|
+
if not flow_modifiers:
|
|
603
|
+
# No alternative scenarios found, create later only the baseline scenario
|
|
604
|
+
pass
|
|
605
|
+
else:
|
|
606
|
+
# Map scenario names to flow modifiers
|
|
607
|
+
scenario_name_to_flow_modifiers = {}
|
|
608
|
+
for flow_modifier in flow_modifiers:
|
|
609
|
+
scenario_name = flow_modifier.scenario_name
|
|
610
|
+
if scenario_name not in scenario_name_to_flow_modifiers:
|
|
611
|
+
scenario_name_to_flow_modifiers[scenario_name] = []
|
|
612
|
+
scenario_name_to_flow_modifiers[scenario_name].append(flow_modifier)
|
|
613
|
+
|
|
614
|
+
# Create scenario definitions from mappings
|
|
615
|
+
for scenario_name, scenario_flow_modifiers in scenario_name_to_flow_modifiers.items():
|
|
616
|
+
new_scenario_definition = ScenarioDefinition(scenario_name, scenario_flow_modifiers)
|
|
617
|
+
result.append(new_scenario_definition)
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
def _create_colors(self, rows: List[Any] = None) -> List[Color]:
|
|
621
|
+
if not rows:
|
|
622
|
+
rows = []
|
|
623
|
+
|
|
624
|
+
result = []
|
|
625
|
+
for row_index, row in enumerate(rows):
|
|
626
|
+
# Header = row 1
|
|
627
|
+
result.append(Color(row, row_number=row_index + 2))
|
|
628
|
+
return result
|
|
629
|
+
|
|
630
|
+
def _is_row_valid(self, row):
|
|
631
|
+
"""
|
|
632
|
+
Check if row is valid.
|
|
633
|
+
Defining all first 4 columns makes the row valid.
|
|
634
|
+
|
|
635
|
+
:param row: Target row
|
|
636
|
+
:return: True if row is valid, false otherwise
|
|
637
|
+
"""
|
|
638
|
+
# Each row must have all first columns defined
|
|
639
|
+
cols = row.iloc[0:4]
|
|
640
|
+
if any(pd.isna(cols)):
|
|
641
|
+
return False
|
|
642
|
+
|
|
643
|
+
return True
|
|
644
|
+
|
|
645
|
+
def _convert_row_nan_to_none(self, row: pd.Series) -> pd.Series:
|
|
646
|
+
"""
|
|
647
|
+
Check the row and convert NaN to None.
|
|
648
|
+
Modifies the original row.
|
|
649
|
+
|
|
650
|
+
:param row: Series
|
|
651
|
+
:return: Returns the original modified row
|
|
652
|
+
"""
|
|
653
|
+
for col_name, value in row.items():
|
|
654
|
+
if np.isreal(value) and np.isnan(value):
|
|
655
|
+
row[col_name] = None
|
|
656
|
+
return row
|
|
657
|
+
|
|
658
|
+
def _to_bool(self, value: Any) -> bool:
|
|
659
|
+
"""
|
|
660
|
+
Check and convert value to bool.
|
|
661
|
+
If value is string then converts to lowercase and checks if value is either "true" or "false"
|
|
662
|
+
and returns corresponding value as bool.\n
|
|
663
|
+
NOTE: Only converts value to string and checks bool validity, no other checking is done.
|
|
664
|
+
|
|
665
|
+
:param value: Value to convert to bool
|
|
666
|
+
:return: Value as bool
|
|
667
|
+
"""
|
|
668
|
+
if isinstance(value, str):
|
|
669
|
+
if value.lower() == "true":
|
|
670
|
+
return True
|
|
671
|
+
|
|
672
|
+
if isinstance(value, bool):
|
|
673
|
+
return value
|
|
674
|
+
|
|
675
|
+
else:
|
|
676
|
+
return False
|
|
677
|
+
|
|
678
|
+
def _to_normalized_float(self, val: Any, decimals: int = 3) -> float:
|
|
679
|
+
"""
|
|
680
|
+
Check and convert val to normalized float [0, 1] with optional precision.
|
|
681
|
+
Returns -1 if conversion fails.
|
|
682
|
+
|
|
683
|
+
:param val: Target value
|
|
684
|
+
:param decimals: Number of decimals to keep (default = 3)
|
|
685
|
+
:return: Float
|
|
686
|
+
"""
|
|
687
|
+
result = -1.0
|
|
688
|
+
try:
|
|
689
|
+
result = round(float(val), decimals)
|
|
690
|
+
except ValueError as ex:
|
|
691
|
+
pass
|
|
692
|
+
|
|
693
|
+
return result
|
|
694
|
+
|
|
695
|
+
def _to_list(self, value: Any, sep=',', allowed_chars = [":"]) -> List[str]:
|
|
696
|
+
"""
|
|
697
|
+
Check and convert value to list of strings.
|
|
698
|
+
Default separator is comma (',')
|
|
699
|
+
Returns empty list of conversion is not possible.
|
|
700
|
+
|
|
701
|
+
:param value: Value to be converted to list
|
|
702
|
+
:return: List of strings
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
result = []
|
|
706
|
+
if type(value) is not str:
|
|
707
|
+
# float and int are not valid types, just return empty list
|
|
708
|
+
result = []
|
|
709
|
+
else:
|
|
710
|
+
# Split the string by sep and ignore all elements that are not strings
|
|
711
|
+
splits = value.split(sep)
|
|
712
|
+
for s in splits:
|
|
713
|
+
stripped = str(s).strip()
|
|
714
|
+
has_allowed_char = np.any([stripped.find(c) < 0 for c in allowed_chars])
|
|
715
|
+
if not stripped.isalpha() and has_allowed_char:
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
result.append(stripped)
|
|
719
|
+
|
|
720
|
+
return result
|
|
721
|
+
|
|
722
|
+
def _excel_column_name_to_index(self, col_name: str) -> int:
|
|
723
|
+
n = 0
|
|
724
|
+
for c in col_name:
|
|
725
|
+
n = \
|
|
726
|
+
(n * 26) + 1 + ord(c) - ord('A')
|
|
727
|
+
return n
|
|
728
|
+
|
|
729
|
+
def _excel_column_index_to_name(self, col_index: int) -> str:
|
|
730
|
+
name = ''
|
|
731
|
+
n = col_index
|
|
732
|
+
while n > 0:
|
|
733
|
+
n, r = divmod(n - 1, 26)
|
|
734
|
+
name = chr(r + ord('A')) + name
|
|
735
|
+
return name
|
|
736
|
+
|
|
737
|
+
def _drop_ignored_columns_from_sheet(self, sheet: pd.DataFrame, ignored_col_names: List[str]) -> pd.DataFrame:
|
|
738
|
+
"""
|
|
739
|
+
Drop list of Excel column names from DataFrame.
|
|
740
|
+
Does not modify original pd.DataFrame.
|
|
741
|
+
|
|
742
|
+
:param sheet: Target pd.DataFrame
|
|
743
|
+
:param ignored_col_names: List of ignored Excel column names, e.g. ['A', 'B', 'E']
|
|
744
|
+
:return:
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
# Drop ignored columns
|
|
748
|
+
col_indices_to_drop = []
|
|
749
|
+
for ignored_col_name in ignored_col_names:
|
|
750
|
+
# Convert index of Excel column name (1-based) to 0-based index for DataFrame
|
|
751
|
+
col_index = self._excel_column_name_to_index(ignored_col_name) - 1
|
|
752
|
+
col_indices_to_drop.append(col_index)
|
|
753
|
+
|
|
754
|
+
new_sheet = sheet.drop(sheet.columns[col_indices_to_drop], axis=1)
|
|
755
|
+
return new_sheet
|
|
756
|
+
|
|
757
|
+
def get_model_params(self) -> dict[ParameterName, Any]:
|
|
758
|
+
"""
|
|
759
|
+
Get model parameters read from the data file.
|
|
760
|
+
|
|
761
|
+
:return: Dictionary of parameter name to parameter value
|
|
762
|
+
"""
|
|
763
|
+
return self._param_name_to_value
|
|
764
|
+
|
|
765
|
+
def get_processes(self) -> List[Process]:
|
|
766
|
+
"""
|
|
767
|
+
Get all Processes.
|
|
768
|
+
|
|
769
|
+
:return: List of Processes
|
|
770
|
+
"""
|
|
771
|
+
return self._processes
|
|
772
|
+
|
|
773
|
+
def get_flows(self) -> List[Flow]:
|
|
774
|
+
"""
|
|
775
|
+
Get all Flows.
|
|
776
|
+
|
|
777
|
+
:return: List of Flows
|
|
778
|
+
"""
|
|
779
|
+
return self._flows
|
|
780
|
+
|
|
781
|
+
def get_stocks(self) -> List[Stock]:
|
|
782
|
+
"""
|
|
783
|
+
Get all Stocks.
|
|
784
|
+
|
|
785
|
+
:return: List of Stocks
|
|
786
|
+
"""
|
|
787
|
+
return self._stocks
|
|
788
|
+
|
|
789
|
+
def get_scenario_definitions(self) -> List[ScenarioDefinition]:
|
|
790
|
+
"""
|
|
791
|
+
Get all ScenarioDefinitions.
|
|
792
|
+
|
|
793
|
+
:return: List of ScenarioDefinitions
|
|
794
|
+
"""
|
|
795
|
+
return self._scenario_definitions
|
|
796
|
+
|
|
797
|
+
def get_color_definitions(self) -> List[Color]:
|
|
798
|
+
return self._colors
|
|
799
|
+
|
|
800
|
+
def get_process_positions(self) -> Dict[int, Dict[str, List[float]]]:
|
|
801
|
+
"""
|
|
802
|
+
Get year -> Process ID -> position mappings.
|
|
803
|
+
|
|
804
|
+
:return: Dictionary (year -> Dictionary(Process ID, List[x, y]))
|
|
805
|
+
"""
|
|
806
|
+
return self._year_to_process_id_to_position
|