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.
Files changed (42) hide show
  1. aiphoria/__init__.py +59 -0
  2. aiphoria/core/__init__.py +55 -0
  3. aiphoria/core/builder.py +305 -0
  4. aiphoria/core/datachecker.py +1808 -0
  5. aiphoria/core/dataprovider.py +806 -0
  6. aiphoria/core/datastructures.py +1686 -0
  7. aiphoria/core/datavisualizer.py +431 -0
  8. aiphoria/core/datavisualizer_data/LICENSE +21 -0
  9. aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
  10. aiphoria/core/datavisualizer_data/pako.min.js +2 -0
  11. aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
  12. aiphoria/core/flowmodifiersolver.py +1754 -0
  13. aiphoria/core/flowsolver.py +1472 -0
  14. aiphoria/core/logger.py +113 -0
  15. aiphoria/core/network_graph.py +136 -0
  16. aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
  17. aiphoria/core/network_graph_data/echarts_min.js +45 -0
  18. aiphoria/core/network_graph_data/network_graph.html +76 -0
  19. aiphoria/core/network_graph_data/network_graph.js +1391 -0
  20. aiphoria/core/parameters.py +269 -0
  21. aiphoria/core/types.py +20 -0
  22. aiphoria/core/utils.py +362 -0
  23. aiphoria/core/visualizer_parameters.py +7 -0
  24. aiphoria/data/example_scenario.xlsx +0 -0
  25. aiphoria/example.py +66 -0
  26. aiphoria/lib/docs/dynamic_stock.py +124 -0
  27. aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
  28. aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
  29. aiphoria/lib/odym/modules/__init__.py +1 -0
  30. aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
  31. aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
  32. aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
  33. aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
  34. aiphoria/lib/odym/modules/test/__init__.py +2 -0
  35. aiphoria/runner.py +678 -0
  36. aiphoria-0.8.0.dist-info/METADATA +119 -0
  37. aiphoria-0.8.0.dist-info/RECORD +40 -0
  38. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
  39. aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
  40. aiphoria-0.0.1.dist-info/METADATA +0 -5
  41. aiphoria-0.0.1.dist-info/RECORD +0 -5
  42. {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