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,1808 @@
1
+ import copy
2
+ from typing import List, Dict, Tuple, Any
3
+ import numpy as np
4
+ import pandas as pd
5
+ from .dataprovider import DataProvider
6
+ from .datastructures import Process, Flow, Stock, ScenarioDefinition, Scenario, ScenarioData, Color, ProcessEntry
7
+ from .parameters import ParameterName, ParameterFillMethod, StockDistributionType,\
8
+ RequiredStockDistributionParameters, AllowedStockDistributionParameterValues
9
+ from .types import FunctionType, ChangeType
10
+
11
+
12
+ class DataChecker(object):
13
+ def __init__(self, dataprovider: DataProvider = None):
14
+ self._dataprovider = dataprovider
15
+ self._processes = self._dataprovider.get_processes()
16
+ self._flows = self._dataprovider.get_flows()
17
+ self._stocks = self._dataprovider.get_stocks()
18
+ self._scenario_definitions = self._dataprovider.get_scenario_definitions()
19
+ self._color_definitions = self._dataprovider.get_color_definitions()
20
+ self._year_to_flow_id_to_flow = {}
21
+ self._year_start = 0
22
+ self._year_end = 0
23
+ self._years = []
24
+
25
+ def build_scenarios(self) -> List[Scenario]:
26
+ """
27
+ Build scenarios to be solved using the FlowSolver.
28
+ First element in the list is always baseline scenario and existence of this is always guaranteed.
29
+
30
+ :return: Dictionary with data for FlowSolver
31
+ """
32
+
33
+ # NOTE: All flows must have data for the starting year
34
+ processes = self._dataprovider.get_processes()
35
+ flows = self._dataprovider.get_flows()
36
+ stocks = self._dataprovider.get_stocks()
37
+
38
+ model_params = self._dataprovider.get_model_params()
39
+ detect_year_range = model_params[ParameterName.DetectYearRange]
40
+ self._year_start = model_params[ParameterName.StartYear]
41
+ self._year_end = model_params[ParameterName.EndYear]
42
+ use_virtual_flows = model_params[ParameterName.UseVirtualFlows]
43
+ baseline_value_name = model_params[ParameterName.BaselineValueName]
44
+ baseline_unit_name = model_params[ParameterName.BaselineUnitName]
45
+ check_errors = model_params[ParameterName.CheckErrors]
46
+
47
+ # Mapping of year -> Process ID -> Process position (normalized)
48
+ year_to_process_id_to_position = self._dataprovider.get_process_positions()
49
+
50
+ # Default optional values
51
+ # The default values are set inside DataProvider but
52
+ # in this is to ensure that the optional parameters have default
53
+ # values if they are missing from the settings file
54
+ virtual_flows_epsilon = 0.1
55
+ if use_virtual_flows and ParameterName.VirtualFlowsEpsilon in model_params:
56
+ virtual_flows_epsilon = model_params[ParameterName.VirtualFlowsEpsilon]
57
+
58
+ fill_missing_absolute_flows = True
59
+ if ParameterName.FillMissingAbsoluteFlows in model_params:
60
+ fill_missing_absolute_flows = model_params[ParameterName.FillMissingAbsoluteFlows]
61
+
62
+ fill_missing_relative_flows = True
63
+ if ParameterName.FillMissingRelativeFlows in model_params:
64
+ fill_missing_relative_flows = model_params[ParameterName.FillMissingRelativeFlows]
65
+
66
+ fill_method = ParameterFillMethod.Zeros
67
+ if ParameterName.FillMethod in model_params:
68
+ fill_method = model_params[ParameterName.FillMethod]
69
+
70
+ if not processes:
71
+ error = "No valid processes!"
72
+ raise Exception([error])
73
+
74
+ if not flows:
75
+ error = "No valid flows!"
76
+ raise Exception([error])
77
+
78
+ if not processes or not flows:
79
+ error = "No processes or flows!"
80
+ raise Exception([error])
81
+
82
+ if detect_year_range:
83
+ self._year_start, self._year_end = self._detect_year_range(flows)
84
+ else:
85
+ self._year_start = model_params[ParameterName.StartYear]
86
+ self._year_end = model_params[ParameterName.EndYear]
87
+
88
+ # Check if start year is after end year and vice versa
89
+ if self._year_start > self._year_end:
90
+ error = "Start year is greater than end year! (start year: {}, end year: {})".format(
91
+ self._year_start, self._year_end)
92
+ raise Exception([error])
93
+
94
+ if self._year_end < self._year_start:
95
+ error = "End year is less than start year! (start year: {}, end year: {})".format(
96
+ self._year_start, self._year_end)
97
+ raise Exception([error])
98
+
99
+ # Check if data years are outside defined year range
100
+ ok, errors = self._check_if_data_is_outside_year_range(flows)
101
+ if not ok:
102
+ raise Exception(errors)
103
+
104
+ # Build array of available years, last year is also included in year range
105
+ self._years = self._get_year_range()
106
+
107
+ # Get all unique Flow IDs and Process IDs that are used in the selected year range as dictionaries
108
+ # because set does not preserve insertion order
109
+ # Dictionaries preserve insertion order in Python version >= 3.7
110
+ unique_flow_ids = self._get_unique_flow_ids_in_year_range(flows, self._years)
111
+ unique_process_ids = self._get_unique_process_ids_in_year_range(flows, processes, self._years)
112
+ df_year_to_flows = self._create_year_to_flow_data(unique_flow_ids, flows, self._years)
113
+
114
+ # **********************************
115
+ # * Check invalid parameter values *
116
+ # **********************************
117
+
118
+ if check_errors:
119
+ print("Checking processes for inflow visualization...")
120
+ process_ids_for_inflow_viz = model_params[ParameterName.VisualizeInflowsToProcesses]
121
+ ok, errors = self._check_process_ids_for_inflow_visualization(process_ids_for_inflow_viz, unique_process_ids)
122
+ if not ok:
123
+ raise Exception(errors)
124
+
125
+ # Check that source and target processes for flows are defined
126
+ print("Checking flow source and target processes...")
127
+ ok, errors = self._check_flow_sources_and_targets(unique_process_ids, df_year_to_flows)
128
+ if not ok:
129
+ raise Exception(errors)
130
+
131
+ # Check that there is not multiple definitions for the exact same flow per year
132
+ # Exact means that source and target processes are the same
133
+ print("Checking multiple flow definitions in the same year...")
134
+ ok, errors = self._check_flow_multiple_definitions_per_year(unique_flow_ids, flows, self._years)
135
+ if not ok:
136
+ raise Exception(errors)
137
+
138
+ # Check if stock distribution type and parameters are set and valid
139
+ print("Checking process stock parameters...")
140
+ ok, errors = self._check_process_stock_parameters(processes)
141
+ if not ok:
142
+ raise Exception(errors)
143
+
144
+ # Check if stocks are defined in processes that do not have any inflows and outflows at any year
145
+ print("Checking stocks in isolated processes...")
146
+ ok, errors = self._check_stocks_in_isolated_processes(stocks, unique_process_ids)
147
+ if not ok:
148
+ raise Exception(errors)
149
+
150
+ if fill_missing_absolute_flows or fill_missing_absolute_flows:
151
+ print("Checking fill method requirements...")
152
+ ok, errors = self._check_fill_method_requirements(fill_method, df_year_to_flows)
153
+ if not ok:
154
+ raise Exception(errors)
155
+
156
+ # Create and propagate flow data for missing years
157
+ df_year_to_flows = self._create_flow_data_for_missing_years(
158
+ df_year_to_flows,
159
+ fill_missing_absolute_flows=fill_missing_absolute_flows,
160
+ fill_missing_relative_flows=fill_missing_relative_flows,
161
+ fill_method=fill_method
162
+ )
163
+
164
+ # Create process to flow mappings
165
+ df_year_to_process_flows = self._create_process_to_flows_entries(unique_process_ids, df_year_to_flows)
166
+
167
+ # NOTE: This is workaround for situation where flow merging removes 0% relative outflow
168
+ # and that causes root process checking to fail
169
+ # Evaluate 0.0 % relative flows as 0.0 absolute flows
170
+ for year in df_year_to_process_flows.index:
171
+ for process_id in df_year_to_process_flows.columns:
172
+ entry: ProcessEntry = df_year_to_process_flows.at[year, process_id]
173
+ inflows = entry.inflows
174
+ outflows = entry.outflows
175
+
176
+ for flow in inflows:
177
+ is_relative_flow = not flow.is_unit_absolute_value
178
+ is_zero_flow = flow.value < 0.01
179
+ if is_relative_flow and is_zero_flow:
180
+ flow.evaluated_value = 0.0
181
+ flow.evaluated_share = 0.0
182
+ flow.is_evaluated = True
183
+
184
+ for flow in outflows:
185
+ is_relative_flow = not flow.is_unit_absolute_value
186
+ is_zero_flow = flow.value < 0.01
187
+ if is_relative_flow and is_zero_flow:
188
+ flow.evaluated_value = 0.0
189
+ flow.evaluated_share = 0.0
190
+ flow.is_evaluated = True
191
+
192
+ print("Merge relative outflows...")
193
+ # NOTE: This is externalized in the future, now go with the hardcoded value
194
+ # min_threshold is value for checking if flow is 100%: any relative flow share greater than min_threshold
195
+ # are considered relative flows with 100% share.
196
+ min_threshold = 99.99
197
+ df_year_to_process_flows, df_year_to_flows = self._merge_relative_outflows(df_year_to_process_flows,
198
+ df_year_to_flows,
199
+ min_threshold)
200
+
201
+ # Remove isolated processes caused by the flow merging
202
+ df_year_to_process_flows = self._remove_isolated_processes(df_year_to_process_flows)
203
+
204
+ if check_errors:
205
+ # Check that root flows have no inflows and only absolute outflows
206
+ print("Checking root processes...")
207
+ ok, errors = self._check_root_processes(df_year_to_process_flows)
208
+ if not ok:
209
+ raise Exception(errors)
210
+
211
+ # Check if process only absolute inflows AND absolute outflows so that
212
+ # the total inflow matches with the total outflows within certain limit
213
+ if not model_params[ParameterName.UseVirtualFlows]:
214
+ print("Checking process total inflows and total outflows mismatches...")
215
+ ok, errors = self._check_process_inflows_and_outflows_mismatch(df_year_to_process_flows,
216
+ epsilon=virtual_flows_epsilon)
217
+ if not ok:
218
+ raise Exception(errors)
219
+
220
+ print("Checking relative flow errors...")
221
+ ok, errors = self._check_relative_flow_errors(df_year_to_flows)
222
+ if not ok:
223
+ raise Exception(errors)
224
+
225
+ # Check if process has no inflows and only relative outflows:
226
+ print("Checking processes with no inflows and only relative outflows...")
227
+ ok, errors = self._check_process_has_no_inflows_and_only_relative_outflows(df_year_to_process_flows)
228
+ if not ok:
229
+ raise Exception(errors)
230
+
231
+ print("Checking isolated/unconnected processes...")
232
+ ok, errors = self._check_for_isolated_processes(df_year_to_process_flows)
233
+ if not ok:
234
+ raise Exception(errors)
235
+
236
+ print("Checking prioritized locations...")
237
+ ok, errors = self._check_prioritized_locations(self._processes, model_params)
238
+ if not ok:
239
+ raise Exception(errors)
240
+
241
+ print("Checking prioritized transformation stages...")
242
+ ok, errors = self._check_prioritized_transformation_stages(self._processes, model_params)
243
+ if not ok:
244
+ raise Exception(errors)
245
+
246
+ # Check that the sheet ParameterName.SheetNameScenarios exists
247
+ # and that it has properly defined data (source process ID, target process IDs, etc.)
248
+ print("Checking scenario definitions...")
249
+ ok, errors = self._check_scenario_definitions(df_year_to_process_flows)
250
+ if not ok:
251
+ raise Exception(errors)
252
+
253
+ # Check that colors have both name and valid value
254
+ print("Checking color definitions...")
255
+ ok, errors = self._check_color_definitions(self._color_definitions)
256
+ if not ok:
257
+ raise Exception(errors)
258
+
259
+ print("Checking flow indicators...")
260
+ ok, errors = self._check_flow_indicators(df_year_to_flows)
261
+ if not ok:
262
+ raise Exception(errors)
263
+
264
+ # *************************************
265
+ # * Unpack DataFrames to dictionaries *
266
+ # *************************************
267
+
268
+ # Create mapping of year -> Process ID -> Process by deep copying entry from DataFrame
269
+ year_to_process_id_to_process = {}
270
+ for year in df_year_to_process_flows.index:
271
+ year_to_process_id_to_process[year] = {}
272
+ for process_id in df_year_to_process_flows.columns:
273
+ entry = df_year_to_process_flows.at[year, process_id]
274
+ if pd.isna(entry):
275
+ continue
276
+
277
+ new_entry = copy.deepcopy(entry)
278
+ process = new_entry.process
279
+
280
+ # Update process position
281
+ if year in year_to_process_id_to_position:
282
+ process_id_to_position = year_to_process_id_to_position[year]
283
+ if process_id in process_id_to_position:
284
+ position = process_id_to_position[process_id]
285
+ process.position_x = position[0]
286
+ process.position_y = position[1]
287
+
288
+ year_to_process_id_to_process[year][process_id] = process
289
+
290
+ # Create mapping of year -> Process ID -> List of incoming Flow IDs and list of outgoing Flow IDs
291
+ year_to_process_id_to_flow_ids = {}
292
+ for year in df_year_to_process_flows.index:
293
+ year_to_process_id_to_flow_ids[year] = {}
294
+ for process_id in df_year_to_process_flows.columns:
295
+ entry = df_year_to_process_flows.at[year, process_id]
296
+ if pd.isna(entry):
297
+ continue
298
+
299
+ new_entry: ProcessEntry = copy.deepcopy(entry)
300
+ inflow_ids = [flow.id for flow in entry.inflows]
301
+ outflow_ids = [flow.id for flow in entry.outflows]
302
+ year_to_process_id_to_flow_ids[year][process_id] = {"in": inflow_ids, "out": outflow_ids}
303
+
304
+ # Create mapping of year -> Flow ID -> Flow by deep copying entry from DataFrame
305
+ year_to_flow_id_to_flow = {}
306
+ for year in df_year_to_flows.index:
307
+ year_to_flow_id_to_flow[year] = {}
308
+ for flow_id in df_year_to_flows.columns:
309
+ entry = df_year_to_flows.at[year, flow_id]
310
+ if pd.isna(entry):
311
+ continue
312
+
313
+ new_entry = copy.deepcopy(df_year_to_flows.at[year, flow_id])
314
+ year_to_flow_id_to_flow[year][flow_id] = new_entry
315
+
316
+ # Process ID to stock mapping
317
+ process_id_to_stock = {}
318
+ for stock in stocks:
319
+ stock_id = stock.id
320
+ process_id_to_stock[stock_id] = stock
321
+
322
+ # Copy Indicator mappings from first unique Flow (Indicator ID -> Indicator)
323
+ # and set indicator conversion factors to default values.
324
+ # NOTE: Virtual flows creation uses directly these default values
325
+ first_unique_flow = unique_flow_ids[list(unique_flow_ids.keys())[0]]
326
+ indicator_name_to_indicator = copy.deepcopy(first_unique_flow.indicator_name_to_indicator)
327
+ for name, indicator in indicator_name_to_indicator.items():
328
+ indicator.conversion_factor = 1.0
329
+
330
+ # List of all scenarios, first element is always the baseline scenario and always exists even if
331
+ # any alternative scenarios are not defined
332
+ scenarios = []
333
+ print("Building baseline scenario...")
334
+ baseline_scenario_data = ScenarioData(years=self._years,
335
+ year_to_process_id_to_process=year_to_process_id_to_process,
336
+ year_to_process_id_to_flow_ids=year_to_process_id_to_flow_ids,
337
+ year_to_flow_id_to_flow=year_to_flow_id_to_flow,
338
+ unique_process_id_to_process=unique_process_ids,
339
+ unique_flow_id_to_flow=unique_flow_ids,
340
+ process_id_to_stock=process_id_to_stock,
341
+ stocks=stocks,
342
+ use_virtual_flows=use_virtual_flows,
343
+ virtual_flows_epsilon=virtual_flows_epsilon,
344
+ baseline_value_name=baseline_value_name,
345
+ baseline_unit_name=baseline_unit_name,
346
+ indicator_name_to_indicator=indicator_name_to_indicator
347
+ )
348
+
349
+ baseline_scenario_definition = ScenarioDefinition(name="Baseline", flow_modifiers=[])
350
+ baseline_scenario = Scenario(definition=baseline_scenario_definition,
351
+ data=baseline_scenario_data,
352
+ model_params=model_params)
353
+ scenarios.append(baseline_scenario)
354
+
355
+ # Create alternative Scenarios
356
+ num_alternative_scenarios = len(self._scenario_definitions)
357
+ print("Building {} alternative scenarios...".format(num_alternative_scenarios))
358
+ for index, scenario_definition in enumerate(self._scenario_definitions):
359
+ # Alternative scenarios do not have ScenarioData at this point, data is filled from the FlowSolver later
360
+ new_alternative_scenario = Scenario(definition=scenario_definition,
361
+ data=ScenarioData(),
362
+ model_params=model_params)
363
+ scenarios.append(new_alternative_scenario)
364
+
365
+ return scenarios
366
+
367
+ def check_processes_integrity(self):
368
+ # Check that there is only processes with unique ids
369
+ errors = []
370
+ process_id_to_processes = {}
371
+ for process in self._processes:
372
+ process_id = process.id
373
+ if process_id not in process_id_to_processes:
374
+ process_id_to_processes[process_id] = []
375
+
376
+ list_of_processes = process_id_to_processes[process_id]
377
+ list_of_processes.append(process)
378
+
379
+ for process_id, list_of_processes in process_id_to_processes.items():
380
+ if len(list_of_processes) > 1:
381
+ s = "Found multiple processes with the same ID in sheet '{}':\n".format(
382
+ self._dataprovider.sheet_name_processes)
383
+ for process in list_of_processes:
384
+ s += "\t- {} (row {})\n".format(process, process.row_number)
385
+ errors.append(s)
386
+
387
+ return not errors, errors
388
+
389
+ def check_flows_integrity(self):
390
+ # Check all years that all processes that are flows are using exists
391
+ result = True
392
+ messages = []
393
+ flows_missing_source_ids = []
394
+ flows_missing_target_ids = []
395
+ flows_missing_value = []
396
+ flows_missing_unit = []
397
+
398
+ for year, flow_id_to_flow in self._year_to_flow_id_to_flow.items():
399
+ for flow_id, flow in flow_id_to_flow:
400
+ source_process_id = flow.source_process_id
401
+ target_process_id = flow.target_process_id
402
+
403
+ if source_process_id not in self._processes:
404
+ flows_missing_source_ids.append(flow)
405
+
406
+ if target_process_id not in self._processes:
407
+ flows_missing_target_ids.append(flow)
408
+
409
+ # Value or unit could be 0 so check for None
410
+ if flow.value is None:
411
+ flows_missing_value.append(flow)
412
+
413
+ if flow.unit is None:
414
+ flows_missing_unit.append(flow)
415
+
416
+ flows = flows_missing_source_ids
417
+ if flows:
418
+ result = False
419
+ messages.append("Found flows missing source process IDs:")
420
+ for flow in flows:
421
+ messages.append("\t- Flow in row {} is missing Source_ID".format(flow.row_number))
422
+
423
+ flows = flows_missing_target_ids
424
+ if flows:
425
+ result = False
426
+ messages.append("Found flows missing target process IDs:")
427
+ for flow in flows:
428
+ messages.append("\t- Flow in row {} is missing Target_ID".format(flow.row_number))
429
+
430
+ flows = flows_missing_value
431
+ if flows:
432
+ result = False
433
+ messages.append("Found flows missing value:")
434
+ for flow in flows:
435
+ messages.append("\t- Flow in row {} is missing value".format(flow.row_number))
436
+
437
+ flows = flows_missing_unit
438
+ if flows:
439
+ result = False
440
+ messages.append("Found flows missing unit:")
441
+ for flow in flows:
442
+ messages.append("\t- Flow in row {} is missing unit".format(flow.row_number))
443
+
444
+ return result, messages
445
+
446
+ def _check_for_isolated_processes(self, df_year_to_process_to_flows: pd.DataFrame) -> Tuple[bool, List[str]]:
447
+ """
448
+ Check for isolated Processes (= processes that have no inflows and no outflows) in any year.
449
+ This is most likely error in data.
450
+
451
+ :return: Tuple (has errors (bool), list of errors (list[str]))
452
+ """
453
+ errors = []
454
+ for process_id in df_year_to_process_to_flows.columns:
455
+ flow_data = df_year_to_process_to_flows[[process_id]]
456
+ has_inflows = False
457
+ has_outflows = False
458
+ for year in flow_data.index:
459
+ entry: ProcessEntry = flow_data.at[year, process_id]
460
+ if entry is pd.NA:
461
+ continue
462
+
463
+ has_inflows = has_inflows or len(entry.inflows) > 0
464
+ has_inflows = has_inflows or len(entry.outflows) > 0
465
+
466
+ if (not has_inflows) and (not has_outflows):
467
+ errors.append("ERROR: Found isolated Process '{}', no inflows and no outflows at any year".format(
468
+ process_id))
469
+
470
+ return not errors, errors
471
+
472
+ def _check_prioritized_transformation_stages(self, processes: List[Process], model_params: Dict[str, Any])\
473
+ -> Tuple[bool, List[str]]:
474
+ """
475
+ Check that prioritized transform stages are valid transformation stage names.
476
+
477
+ :param processes: List of Processes
478
+ :param model_params: Model parameters (Dictionary)
479
+ :return: Tuple (has errors (bool), list of errors (str))
480
+ """
481
+ errors = []
482
+ prioritized_transform_stages = model_params[ParameterName.PrioritizeTransformationStages]
483
+ found_transformation_stages = set()
484
+ for process in processes:
485
+ found_transformation_stages.add(process.transformation_stage)
486
+
487
+ for transformation_stage in prioritized_transform_stages:
488
+ if transformation_stage not in found_transformation_stages:
489
+ s = "Transformation stage '{}' is not used in any Processes".format(transformation_stage)
490
+ errors.append(s)
491
+
492
+ return not errors, errors
493
+
494
+ def _check_prioritized_locations(self, processes: List[Process], model_params: Dict[str, Any])\
495
+ -> Tuple[bool, List[str]]:
496
+ """
497
+ Check that prioritized locations are valid location names.
498
+
499
+ :param processes: List of Processes
500
+ :param model_params: Model parameters (Dictionary)
501
+ :return: Tuple (has errors (bool), list of errors (str))
502
+ """
503
+ errors = []
504
+ prioritized_locations = model_params[ParameterName.PrioritizeLocations]
505
+ found_locations = set()
506
+ for process in processes:
507
+ found_locations.add(process.location)
508
+
509
+ for location in prioritized_locations:
510
+ if location not in prioritized_locations:
511
+ s = "Location '{}' is not used in any Processes".format(location)
512
+ errors.append(s)
513
+
514
+ return not errors, errors
515
+
516
+ def check_for_errors(self) -> bool:
517
+ """
518
+ Check for additional errors after building the scenarios.
519
+ Returns True if no errors were found.
520
+ Raises Exception if:
521
+ - found errors when checking processes' integrity
522
+ - found errors when checking flows' integrity
523
+
524
+ :raises Exception: Exception containing error message
525
+ """
526
+ ok, messages_processes = self.check_processes_integrity()
527
+ if not ok:
528
+ raise Exception(messages_processes)
529
+
530
+ ok, messages_flows = self.check_flows_integrity()
531
+ if not ok:
532
+ raise Exception(messages_flows)
533
+
534
+ return ok
535
+
536
+ def get_processes(self) -> List[Process]:
537
+ return self._processes
538
+
539
+ def get_flows(self) -> List[Flow]:
540
+ return self._flows
541
+
542
+ def get_stocks(self) -> List[Stock]:
543
+ return self._stocks
544
+
545
+ def get_start_year(self) -> int:
546
+ return self._year_start
547
+
548
+ def get_end_year(self) -> int:
549
+ return self._year_end
550
+
551
+ def get_year_to_flow_id_to_flow_mapping(self):
552
+ return self._year_to_flow_id_to_flow
553
+
554
+ def _detect_year_range(self, flows: List[Flow]) -> (int, int):
555
+ """
556
+ Detect year range for flow data.
557
+ Return tuple (start year, end year)
558
+
559
+ :param flows: List of Flows
560
+ :return: Tuple (start year, end year)
561
+ """
562
+ year_min = 9999
563
+ year_max = 0
564
+
565
+ for flow in flows:
566
+ flow_year = flow.year
567
+ if flow_year is None:
568
+ continue
569
+
570
+ flow_year = int(flow_year)
571
+ if flow_year <= year_min:
572
+ year_min = flow_year
573
+ if flow_year >= year_max:
574
+ year_max = flow_year
575
+
576
+ year_start = min(year_min, year_max)
577
+ year_end = max(year_min, year_max)
578
+ return year_start, year_end
579
+
580
+ def _get_year_range(self) -> list[int]:
581
+ """
582
+ Get year range used in simulation as list of integers. Starting year and end year are included in the range.
583
+ :return: List of years as integers
584
+ """
585
+ return [year for year in range(self._year_start, self._year_end + 1)]
586
+
587
+ def _check_if_data_is_outside_year_range(self, flows: List[Flow]) -> Tuple[bool, List[str]]:
588
+ """
589
+ Check if data is outside year range.
590
+
591
+ :param flows: List of Flows
592
+ :return: Tuple (has errors (bool), list of errors (list[str])
593
+ """
594
+ errors = []
595
+
596
+ # Get year range defined in settings (= simulation years)
597
+ years = self._get_year_range()
598
+ year_min = min(years)
599
+ year_max = max(years)
600
+
601
+ unique_flow_years = set()
602
+ for flow in flows:
603
+ unique_flow_years.add(flow.year)
604
+
605
+ # Min and max year found in data
606
+ flow_year_min = min(unique_flow_years)
607
+ flow_year_max = max(unique_flow_years)
608
+
609
+ # Flows defined later that end_year
610
+ if flow_year_min > year_max:
611
+ error = "All flows are defined after the end year ({}) defined in settings file".format(year_max)
612
+ errors.append(error)
613
+
614
+ # Flows defined before the start_year
615
+ if flow_year_max < year_min:
616
+ error = "All flows are defined before the start year ({}) defined in the settings file".format(year_min)
617
+ errors.append(error)
618
+
619
+ # First flow year is defined afther the start_year
620
+ if flow_year_min > year_min:
621
+ error = "Start year ({}) is set before first flow data year ({}) in settings file".format(
622
+ year_min, flow_year_min)
623
+ errors.append(error)
624
+
625
+ return not errors, errors
626
+
627
+ def _check_process_ids_for_inflow_visualization(self, process_ids: List[str],
628
+ unique_processes: Dict[str, Process]) -> Tuple[bool, List[str]]:
629
+ """
630
+ Check that all Process IDs that are selected for inflow visualization are valid
631
+
632
+ :param process_ids: List of Process IDs
633
+ :param unique_processes: Dictionary (Process ID, Process)
634
+ :return: Tuple (has errors, list of errors)
635
+ """
636
+ errors = []
637
+ for process_id in process_ids:
638
+ if process_id not in unique_processes.keys():
639
+ errors.append("Process inflows to visualize '{}' is not valid process ID!".format(process_id))
640
+
641
+ return not errors, errors
642
+
643
+
644
+ def _check_flow_sources_and_targets(self,
645
+ unique_process_ids: dict[str, Process],
646
+ df_year_to_flows: pd.DataFrame) -> [bool, List[str]]:
647
+ """
648
+ Check that all Flow sources and target Processes exists.
649
+
650
+ :param unique_process_ids:
651
+ :param df_year_to_flows: DataFrame
652
+ :return: Tuple (True, list of errors)
653
+ """
654
+ errors = []
655
+ sheet_name_flows = self._dataprovider.sheet_name_flows
656
+ for year in df_year_to_flows.index:
657
+ for flow_id in df_year_to_flows.columns:
658
+ flow = df_year_to_flows.at[year, flow_id]
659
+ if pd.isnull(flow):
660
+ continue
661
+
662
+ if flow.source_process_id not in unique_process_ids:
663
+ s = "No source process {} for flow {} (row number {}) in year {} (in Excel sheet {}) ".format(
664
+ flow.source_process_id, flow_id, flow.row_number, year, sheet_name_flows)
665
+ errors.append(s)
666
+
667
+ if flow.target_process_id not in unique_process_ids:
668
+ s = "No target process {} for flow {} (row number {}) in year {} (sheet {})".format(
669
+ flow.target_process_id, flow_id, flow.row_number, year, sheet_name_flows)
670
+ errors.append(s)
671
+
672
+ return not errors, errors
673
+
674
+ def _get_unique_flow_ids(self, flows: List[Flow]) -> Dict[str, Flow]:
675
+ unique_flow_id_to_flow = {}
676
+ for flow in flows:
677
+ if flow.id not in unique_flow_id_to_flow:
678
+ unique_flow_id_to_flow[flow.id] = flow
679
+ return unique_flow_id_to_flow
680
+
681
+ def _get_unique_process_ids(self, processes: List[Process]) -> Dict[str, Process]:
682
+ unique_process_id_to_process = {}
683
+ for process in processes:
684
+ if process.id not in unique_process_id_to_process:
685
+ unique_process_id_to_process[process.id] = process
686
+ return unique_process_id_to_process
687
+
688
+ def _get_unique_flow_ids_in_year_range(self, flows: list[Flow], years: list[int]) -> dict[str, Flow]:
689
+ """
690
+ Get unique Flow IDs used in the year range as dictionary [Flow ID -> Flow].
691
+
692
+ :param flows: List of Flows
693
+ :param years: List of years
694
+ :return: Dictionary [Flow ID -> Flow]
695
+ """
696
+ unique_flow_id_to_flow = {}
697
+ for flow in flows:
698
+ flow_year = flow.year
699
+ if flow_year not in years:
700
+ continue
701
+
702
+ flow_id = flow.id
703
+ if flow_id not in unique_flow_id_to_flow:
704
+ unique_flow_id_to_flow[flow_id] = flow
705
+
706
+ return unique_flow_id_to_flow
707
+
708
+ def _get_unique_process_ids_in_year_range(self,
709
+ flows: list[Flow],
710
+ processes: list[Process],
711
+ years: list[int]) -> dict[str, Process]:
712
+ """
713
+ Get unique Process IDs used in the year range as dictionary [Process ID -> Process].
714
+
715
+ :param flows: List of Flows
716
+ :param years: List of years
717
+ :return: Dictionary [Process ID -> Process]
718
+ """
719
+ # Map Process IDs to Processes (this contains all Processes)
720
+ process_id_to_process = {}
721
+ for process in processes:
722
+ process_id = process.id
723
+ process_id_to_process[process_id] = process
724
+
725
+ # Map all unique Process IDs to Processes
726
+ unique_process_id_to_process = {}
727
+ for flow in flows:
728
+ flow_year = flow.year
729
+ if flow_year not in years:
730
+ continue
731
+
732
+ source_process_id = flow.source_process_id
733
+ if source_process_id not in unique_process_id_to_process:
734
+ source_process = process_id_to_process[source_process_id]
735
+ unique_process_id_to_process[source_process_id] = source_process
736
+
737
+ target_process_id = flow.target_process_id
738
+ if target_process_id not in unique_process_id_to_process:
739
+ target_process = process_id_to_process[target_process_id]
740
+ unique_process_id_to_process[target_process_id] = target_process
741
+
742
+ return unique_process_id_to_process
743
+
744
+ def _create_year_to_flow_mapping(self, flows) -> Dict[int, Dict[str, Flow]]:
745
+ year_to_flow_id_to_flow = {}
746
+ for flow in flows:
747
+ if flow.year not in year_to_flow_id_to_flow:
748
+ year_to_flow_id_to_flow[flow.year] = {}
749
+
750
+ if flow.id not in year_to_flow_id_to_flow[flow.year]:
751
+ year_to_flow_id_to_flow[flow.year][flow.id] = flow
752
+ return year_to_flow_id_to_flow
753
+
754
+ def _create_year_to_flow_data(self,
755
+ unique_flow_ids: dict[str, Flow],
756
+ flows: list[Flow],
757
+ years: list[int]) -> pd.DataFrame:
758
+ """
759
+ Create DataFrame that has year as index and Flow IDs as column and cell is Flow-object.
760
+
761
+ :param unique_flow_ids: Dictionary of unique [Flow ID -> Flow]
762
+ :param flows: List of Flows
763
+ :param years: list of years
764
+ :return: DataFrame
765
+ """
766
+
767
+ df = pd.DataFrame(index=years, columns=unique_flow_ids.keys())
768
+ for flow in flows:
769
+ if flow.year not in years:
770
+ continue
771
+
772
+ df.at[flow.year, flow.id] = flow
773
+
774
+ return df
775
+
776
+ def _check_flow_multiple_definitions_per_year(self,
777
+ unique_flow_ids: Dict[str, Flow],
778
+ flows: List[Flow],
779
+ years: list[int]
780
+ ) -> Tuple[bool, List[str]]:
781
+ """
782
+ Check that there is only one Flow definition per year.
783
+
784
+ :param unique_flow_ids: Dictionary of unique [Flow ID -> Flow]
785
+ :param flows: List of Flows
786
+ :param years: List of years
787
+ :return: True if successful, False otherwise
788
+ """
789
+ errors = []
790
+ sheet_name_flows = self._dataprovider.sheet_name_flows
791
+ df = pd.DataFrame(index=years, columns=list(unique_flow_ids.keys()))
792
+ for flow in flows:
793
+ if flow.year not in years:
794
+ continue
795
+
796
+ if flow.year > max(years):
797
+ continue
798
+
799
+ if pd.isnull(df.at[flow.year, flow.id]):
800
+ df.at[flow.year, flow.id] = []
801
+ df.at[flow.year, flow.id].append(flow)
802
+
803
+ for year in df.index:
804
+ for flow_id in df.columns:
805
+ existing_flows = df.at[year, flow_id]
806
+ if type(existing_flows) != list:
807
+ continue
808
+
809
+ if len(existing_flows) > 1:
810
+ target_flow = existing_flows[0]
811
+ s = "Multiple definitions for the same flow '{}' in year {} in sheet named '{}':".format(
812
+ target_flow.id, target_flow.year, sheet_name_flows)
813
+ errors.append(s)
814
+
815
+ for duplicate_flow in existing_flows:
816
+ s = "- in row {}".format(duplicate_flow.row_number)
817
+ errors.append(s)
818
+
819
+ return not errors, errors
820
+
821
+ def _check_root_processes(self, df_year_to_process_flows: pd.DataFrame):
822
+ """
823
+ Check root processes.
824
+ Root processes do not have inflows and have only absolute outflows.
825
+
826
+ :param df_year_to_process_flows: DataFrame
827
+ :return: Tuple (has errors (bool), list of errors (list[str]))
828
+ """
829
+
830
+ errors = []
831
+ for year in df_year_to_process_flows.index:
832
+ for process_id in df_year_to_process_flows.columns:
833
+ entry: ProcessEntry = df_year_to_process_flows.at[year, process_id]
834
+
835
+ # Skip NA entries
836
+ if pd.isna(entry):
837
+ continue
838
+
839
+ process = entry.process
840
+ inflows = entry.inflows
841
+ outflows = entry.outflows
842
+
843
+ if len(inflows) > 0:
844
+ continue
845
+
846
+ abs_outflows = []
847
+ rel_outflows = []
848
+ for flow in outflows:
849
+ if flow.is_unit_absolute_value:
850
+ abs_outflows.append(flow)
851
+ else:
852
+ rel_outflows.append(flow)
853
+
854
+ num_abs_outflows = len(abs_outflows)
855
+ num_rel_outflows = len(rel_outflows)
856
+ no_outflows = (num_abs_outflows == 0) and (num_rel_outflows == 0)
857
+
858
+ # NOTE: This is workaround for situation where flow merging removes 0% relative outflow
859
+ # and that causes root process checking to fail
860
+ is_outflows_evaluated = np.all([flow.is_evaluated for flow in rel_outflows])
861
+
862
+ if no_outflows:
863
+ # Error: Root process does not have any outflows
864
+ msg = "{}: Root process '{}' has no inflows and outflows".format(year, process)
865
+ errors.append(msg)
866
+
867
+ if num_rel_outflows > 0 and not is_outflows_evaluated:
868
+ # Error: root process can have only absolute outflows
869
+ msg = "{}: Root process '{}' has only relative outflows".format(year, process)
870
+ errors.append(msg)
871
+
872
+ return not errors, errors
873
+
874
+ def _check_process_inflows_and_outflows_mismatch(self,
875
+ df_process_to_flows: pd.DataFrame,
876
+ epsilon: float = 0.1) -> Tuple[bool, List[str]]:
877
+
878
+ errors = []
879
+ sheet_name_flows = self._dataprovider.sheet_name_flows
880
+ for year in df_process_to_flows.index:
881
+ for process_id in df_process_to_flows.columns:
882
+ entry: ProcessEntry = df_process_to_flows.at[year, process_id]
883
+ inflows = entry.inflows
884
+ outflows = entry.outflows
885
+
886
+ if not inflows:
887
+ continue
888
+
889
+ if not outflows:
890
+ continue
891
+
892
+ is_all_inflows_absolute = all([flow.is_unit_absolute_value for flow in inflows])
893
+ is_all_outflows_absolute = all([flow.is_unit_absolute_value for flow in outflows])
894
+ if is_all_inflows_absolute and is_all_outflows_absolute:
895
+ inflows_total = np.sum([flow.value for flow in inflows])
896
+ outflows_total = np.sum([flow.value for flow in outflows])
897
+ diff_abs = np.abs(inflows_total) - np.abs(outflows_total)
898
+ if diff_abs > epsilon:
899
+ s = "Total inflows and total outflows for process '{}' does not match.".format(process_id)
900
+ errors.append(s)
901
+
902
+ s = "Absolute difference of total inflows and total outflows was {}".format(diff_abs)
903
+ errors.append(s)
904
+
905
+ s = "Check following inflows in Excel sheet '{}':".format(sheet_name_flows)
906
+ errors.append(s)
907
+ for flow in inflows:
908
+ s = "- flow '{}' in row {}".format(flow.id, flow.row_number)
909
+ errors.append(s)
910
+
911
+ errors.append("Check following outflows:")
912
+ for flow in outflows:
913
+ s = "- flow '{}' in row {}".format(flow.id, flow.row_number)
914
+ errors.append(s)
915
+
916
+ s = ""
917
+ errors.append(s)
918
+
919
+ # Check if process has more outflows than inflows
920
+ inflows_abs = [flow for flow in inflows if flow.is_unit_absolute_value]
921
+ outflows_abs = [flow for flow in outflows if flow.is_unit_absolute_value]
922
+ inflows_total_abs = np.sum([flow.value for flow in inflows_abs if flow.is_unit_absolute_value])
923
+ outflows_total_abs = np.sum([flow.value for flow in outflows_abs if flow.is_unit_absolute_value])
924
+ if outflows_total_abs > inflows_total_abs:
925
+ s = "Total outflows are greater than total inflows for process '{}'".format(process_id)
926
+ errors.append(s)
927
+
928
+ s = "Year {}, total absolute inflows={}, total absolute outflows={}".format(
929
+ year, inflows_total_abs, outflows_total_abs)
930
+ errors.append(s)
931
+
932
+ s = "Check following inflows in Excel sheet '{}':".format(sheet_name_flows)
933
+ errors.append(s)
934
+ for flow in inflows_abs:
935
+ s = "- flow '{}' in row {} (value: {})".format(flow.id, flow.row_number, flow.value)
936
+ errors.append(s)
937
+
938
+ errors.append("Check following outflows:")
939
+ for flow in outflows_abs:
940
+ s = "- flow '{}' in row {} (value: {})".format(flow.id, flow.row_number, flow.value)
941
+ errors.append(s)
942
+
943
+ s = ""
944
+ errors.append(s)
945
+
946
+ return not errors, errors
947
+
948
+ def _check_relative_flow_errors(self, df_year_to_flows: pd.DataFrame, epsilon: float = 0.01) -> Tuple[bool, List[str]]:
949
+ """
950
+ Check that relative flows do not go over 100%.
951
+ Default epsilon is 0.01.
952
+
953
+ :param df_year_to_flows: DataFrame
954
+ :param epsilon: Maximum allowed difference when checking if total outflows > 100.0
955
+ :return: Tuple (bool, list of errors)
956
+ """
957
+ errors = []
958
+ for flow_id in df_year_to_flows.columns:
959
+ flow_data = df_year_to_flows[flow_id]
960
+ for year, flow in flow_data.items():
961
+ # NOTE: NEW, allow making "holes" to DataFrame
962
+ if not isinstance(flow, Flow):
963
+ continue
964
+
965
+ if flow.is_unit_absolute_value:
966
+ continue
967
+
968
+ if flow.value < 0.0:
969
+ s = "Flow {} has value less than 0% for year {} in row {} in sheet '{}'".format(
970
+ flow.id, flow.year, flow.row_number, self._dataprovider.sheet_name_flows
971
+ )
972
+ errors.append(s)
973
+ return not errors, errors
974
+
975
+ if flow.value > 100.0:
976
+ s = "Flow {} has value over 100% for year {} in row {} in sheet '{}'".format(
977
+ flow.id, flow.year, flow.row_number, self._dataprovider.sheet_name_flows
978
+ )
979
+ errors.append(s)
980
+ return not errors, errors
981
+
982
+ # Check if total relative outflows from process are >100%
983
+ for year in df_year_to_flows.index:
984
+ process_id_to_rel_outflows = {}
985
+ for flow_id in df_year_to_flows.columns:
986
+ flow = df_year_to_flows.at[year, flow_id]
987
+ # NOTE: NEW, allow making "holes" to DataFrame
988
+ if not isinstance(flow, Flow):
989
+ continue
990
+
991
+ if flow.is_unit_absolute_value:
992
+ continue
993
+
994
+ # Gather relative outflows to source process ID
995
+ outflows = process_id_to_rel_outflows.get(flow.source_process_id, [])
996
+ outflows.append(flow)
997
+ process_id_to_rel_outflows[flow.source_process_id] = outflows
998
+
999
+ # Check if total outflows of the process are > 100%
1000
+ for process_id, outflows in process_id_to_rel_outflows.items():
1001
+ total_share = np.sum([flow.value for flow in outflows])
1002
+ diff = np.abs(total_share - 100.0)
1003
+ if (total_share > 100.0) and (diff > epsilon):
1004
+ s = "Process {} has total relative outflows over 100%".format(process_id)
1005
+ s += " (total={:.3f}%) for year {} in sheet '{}'".format(
1006
+ total_share, year, self._dataprovider.sheet_name_flows)
1007
+ s.format(process_id)
1008
+ errors.append(s)
1009
+
1010
+ s = "Check following flows:"
1011
+ errors.append(s)
1012
+ for flow in outflows:
1013
+ s = "\t{} (row {})".format(flow, flow.row_number)
1014
+ errors.append(s)
1015
+ errors.append("")
1016
+
1017
+ return not errors, errors
1018
+
1019
+ def _create_flow_id_has_data_mapping(self, df_flows) -> pd.DataFrame:
1020
+ df = df_flows.copy()
1021
+ for flow_id in df.columns:
1022
+ df[flow_id] = np.where(pd.isnull(df[flow_id]), False, True)
1023
+ return df
1024
+
1025
+ def _create_flow_data_for_missing_years(self,
1026
+ df_year_flows: pd.DataFrame,
1027
+ fill_missing_absolute_flows: bool,
1028
+ fill_missing_relative_flows: bool,
1029
+ fill_method: str = ParameterFillMethod.Zeros
1030
+ ) -> pd.DataFrame:
1031
+ """
1032
+ Fill years missing Flow data with the previous valid Flow data.
1033
+ If fill_absolute_flows is set to True then process Flows with absolute values.
1034
+ If fill_relative_values is set to True then process Flows with relative values.
1035
+ If both fill_absolute_flows and fill_relative_flows are set to False then returns copy
1036
+ of the original.
1037
+
1038
+ :param df_year_flows: DataFrame
1039
+ :param fill_missing_absolute_flows: If True then process Flows with absolute values
1040
+ :param fill_missing_relative_flows: If True then process Flows with relative values
1041
+ :return: DataFrame
1042
+ """
1043
+ # No filling, convert nan in DataFrame to None
1044
+ if (not fill_missing_absolute_flows) and (not fill_missing_relative_flows):
1045
+ result = df_year_flows.copy()
1046
+ return result
1047
+
1048
+ # Create flow has data as boolean mapping
1049
+ df_flow_id_has_data = df_year_flows.copy()
1050
+ for flow_id in df_flow_id_has_data.columns:
1051
+ df_flow_id_has_data[flow_id] = np.where(pd.isnull(df_flow_id_has_data[flow_id]), False, True)
1052
+
1053
+ # Find earliest and latest year of Flow data for each Flow ID
1054
+ flow_id_to_min_year = {flow_id: max(df_year_flows.index) for flow_id in df_year_flows}
1055
+ flow_id_to_max_year = {flow_id: min(df_year_flows.index) for flow_id in df_year_flows}
1056
+ for flow_id in df_year_flows.columns:
1057
+ for year, has_flow_data in df_year_flows[flow_id].items():
1058
+ if pd.notna(has_flow_data):
1059
+ if year < flow_id_to_min_year[flow_id]:
1060
+ flow_id_to_min_year[flow_id] = year
1061
+ if year > flow_id_to_max_year[flow_id]:
1062
+ flow_id_to_max_year[flow_id] = year
1063
+
1064
+ # Find gaps flow data columns and set values according to fill_method
1065
+ for flow_id in df_year_flows.columns:
1066
+ flow_data = df_year_flows[flow_id]
1067
+ flow_has_data = df_flow_id_has_data[flow_id]
1068
+
1069
+ # NOTE: Now checks the flow type on from the first occurrence of flow data and assume that the flow type
1070
+ # NOTE: does not change during the years
1071
+ first_valid_flow = flow_data[flow_id_to_min_year[flow_id]]
1072
+ is_abs_flow = first_valid_flow.is_unit_absolute_value
1073
+ is_rel_flow = not is_abs_flow
1074
+
1075
+ # Skip to next if not using filling for flow type
1076
+ if is_abs_flow and not fill_missing_absolute_flows:
1077
+ continue
1078
+
1079
+ if is_rel_flow and not fill_missing_relative_flows:
1080
+ continue
1081
+
1082
+ if fill_method == ParameterFillMethod.Zeros:
1083
+ # Fill all missing flow values with zeros
1084
+ # Fill all missing absolute flows with zeros
1085
+ flow_id_min_year = flow_id_to_min_year[flow_data.name]
1086
+ missing_flow_base = copy.deepcopy(flow_data.loc[flow_id_min_year])
1087
+ for year, has_data in flow_has_data.items():
1088
+ if not has_data:
1089
+ new_flow_data = copy.deepcopy(missing_flow_base)
1090
+ if new_flow_data.is_unit_absolute_value:
1091
+ new_flow_data.value = 0.0
1092
+ new_flow_data.year = year
1093
+ flow_data[year] = new_flow_data
1094
+ flow_has_data[year] = True
1095
+
1096
+ if fill_method == ParameterFillMethod.Previous:
1097
+ # Fill all missing flow values using the last found flow values
1098
+ # NOTE: Do not fill flows if flows are missing at the start of the flow_data
1099
+ flow_data.ffill(inplace=True)
1100
+
1101
+ # DataFrame.ffill copies the object that the new created object references
1102
+ # to the last found flow object so overwrite all objects in flow_data
1103
+ # with the new deepcopied flow object
1104
+ for year, flow in flow_data.items():
1105
+ if pd.isna(flow):
1106
+ continue
1107
+
1108
+ new_flow = copy.deepcopy(flow)
1109
+ new_flow.year = year
1110
+ flow_data.at[year] = new_flow
1111
+ flow_has_data[year] = True
1112
+
1113
+ if fill_method == ParameterFillMethod.Next:
1114
+ # Fill all missing flow values using the next found flow values
1115
+ # NOTE: Do not fill flows if flows are missing at the end of the flow_data
1116
+ flow_data.bfill(inplace=True)
1117
+
1118
+ # DataFrame.bfill copies the object that the new created object references
1119
+ # to the last found flow object so overwrite all objects in flow_data
1120
+ # with the new deepcopied flow object
1121
+ for year, flow in flow_data.items():
1122
+ if pd.isna(flow):
1123
+ continue
1124
+
1125
+ new_flow = copy.deepcopy(flow)
1126
+ new_flow.year = year
1127
+ flow_data.at[year] = new_flow
1128
+ flow_has_data[year] = True
1129
+
1130
+ if fill_method == ParameterFillMethod.Interpolate:
1131
+ # Fill all missing flow values using interpolation
1132
+ # Do not fill flow values if missing at the start of flow_data or missing at the end of flow_data
1133
+ flow_values = flow_data.copy()
1134
+ for year, has_data in flow_has_data.items():
1135
+ if not has_data:
1136
+ continue
1137
+
1138
+ flow_values[year] = flow_data[year].value
1139
+
1140
+ flow_values = flow_values.astype("float64")
1141
+ flow_values.interpolate(limit_direction="forward", inplace=True)
1142
+
1143
+ # Get first valid flow data and use that as missing flow base
1144
+ flow_id_min_year = flow_id_to_min_year[flow_data.name]
1145
+ missing_flow_base = copy.deepcopy(flow_data.loc[flow_id_min_year])
1146
+ for year, interpolated_value in flow_values.items():
1147
+ if pd.isna(interpolated_value):
1148
+ continue
1149
+
1150
+ new_flow = copy.deepcopy(missing_flow_base)
1151
+ new_flow.value = flow_values[year]
1152
+ new_flow.year = year
1153
+ flow_data[year] = new_flow
1154
+ flow_has_data[year] = True
1155
+
1156
+ return df_year_flows
1157
+
1158
+ def _create_process_to_flow_ids(self, unique_process_ids, processes: List[Process], df_flows: pd.DataFrame) -> pd.DataFrame:
1159
+ df = pd.DataFrame(dtype="object", index=df_flows.index, columns=unique_process_ids)
1160
+ for year in df_flows.index:
1161
+ for process in processes:
1162
+ df.at[year, process.id] = {"process": copy.deepcopy(process), "flow_ids": {"in": [], "out": []}}
1163
+
1164
+ # Add process inflows and outflows for every year
1165
+ for year in df_flows.index:
1166
+ for flow_id in df_flows.columns:
1167
+ # No data defined for process ID at this year
1168
+ flow = df_flows.at[year, flow_id]
1169
+ if pd.isnull(flow):
1170
+ continue
1171
+
1172
+ df.at[year, flow.source_process_id]["flow_ids"]["out"].append(flow.id)
1173
+ df.at[year, flow.target_process_id]["flow_ids"]["in"].append(flow.id)
1174
+ return df
1175
+
1176
+ def _create_process_to_flows(self,
1177
+ unique_process_ids: dict[str, Process],
1178
+ df_year_flows: pd.DataFrame) -> pd.DataFrame:
1179
+ """
1180
+
1181
+ :param unique_process_ids: Dictionary of unique [Process ID -> Process]
1182
+ :param df_year_flows: DataFrame, year to Flows
1183
+ :return: DataFrame
1184
+ """
1185
+
1186
+ df = pd.DataFrame(index=df_year_flows.index, columns=list(unique_process_ids.keys()), dtype="object")
1187
+ for year in df_year_flows.index:
1188
+ for process_id, process in unique_process_ids.items():
1189
+ df.at[year, process.id] = {"process": copy.deepcopy(process), "flows": {"in": [], "out": []}}
1190
+
1191
+ # Add process inflows and outflows for every year
1192
+ for year in df_year_flows.index:
1193
+ for flow_id in df_year_flows.columns:
1194
+ flow = df_year_flows.at[year, flow_id]
1195
+
1196
+ # No data defined for process ID at this year
1197
+ if pd.isnull(flow):
1198
+ continue
1199
+
1200
+ df.at[year, flow.source_process_id]["flows"]["out"].append(flow)
1201
+ df.at[year, flow.target_process_id]["flows"]["in"].append(flow)
1202
+
1203
+ return df
1204
+
1205
+ def _create_process_to_flows_entries(self,
1206
+ unique_process_ids: dict[str, Process],
1207
+ df_year_flows: pd.DataFrame) -> pd.DataFrame:
1208
+ """
1209
+ Create Process to Flows (inflows and outflows) entries.
1210
+ Entries are ProcessEntry-objects inside DataFrame.
1211
+
1212
+ :param unique_process_ids: Dictionary of unique [Process ID -> Process]
1213
+ :param df_year_flows: DataFrame, year to flows
1214
+ :return: DataFrame (index as year, column as Process ID, cell as ProcessEntry)
1215
+ """
1216
+
1217
+ df = pd.DataFrame(index=df_year_flows.index, columns=list(unique_process_ids.keys()), dtype="object")
1218
+
1219
+ for year in df_year_flows.index:
1220
+ for process_id, process in unique_process_ids.items():
1221
+ df.at[year, process_id] = ProcessEntry(process)
1222
+
1223
+ # Add Process inflows and outflows for every year
1224
+ for year in df_year_flows.index:
1225
+ for flow_id in df_year_flows.columns:
1226
+ flow = df_year_flows.at[year, flow_id]
1227
+ if pd.isnull(flow):
1228
+ continue
1229
+
1230
+ entry_source_process = df.at[year, flow.source_process_id]
1231
+ entry_source_process.add_outflow(flow)
1232
+
1233
+ entry_target_process = df.at[year, flow.target_process_id]
1234
+ entry_target_process.add_inflow(flow)
1235
+
1236
+ return df
1237
+
1238
+ @staticmethod
1239
+ def _merge_relative_outflows(df_year_to_process_flows: pd.DataFrame,
1240
+ df_year_to_flows: pd.DataFrame,
1241
+ min_threshold: float = 99.9) -> Tuple[pd.DataFrame, pd.DataFrame]:
1242
+ """
1243
+ Check if relative outflows needs merging. Flow merging means that
1244
+ if Process has only one 100% relative flow then from that year onward
1245
+ that is going to be the only outflow unless new Flows are introduced
1246
+ in later years. Other relative outflows are removed from the Process for that
1247
+ year and the rest of the years.
1248
+
1249
+ :param min_threshold: Flow with share greater than this are considered as 100%
1250
+ :param df_year_to_process_flows: pd.DataFrame (index is year, column is Process ID)
1251
+ :param df_year_to_flows: pd.DataFrame (index is year, column is Flow ID)
1252
+ :return: pd.DataFrame
1253
+ """
1254
+ assert min_threshold > 0.0, "min_threshold should be > 0.0"
1255
+ assert min_threshold <= 100.0, "min_threshold should be <= 100.0"
1256
+
1257
+ df_process = df_year_to_process_flows.copy()
1258
+ df_flows = df_year_to_flows.copy()
1259
+ for process_id in df_process.columns:
1260
+ for year in df_process.index:
1261
+ entry: ProcessEntry = df_process.at[year, process_id]
1262
+ inflows = entry.inflows
1263
+ outflows = entry.outflows
1264
+
1265
+ # Skip root processes
1266
+ if not inflows:
1267
+ continue
1268
+
1269
+ # Get only Processes that have only 1 relative outflow
1270
+ rel_outflows = [flow for flow in outflows if not flow.is_unit_absolute_value]
1271
+ full_relative_outflows = [flow for flow in rel_outflows if flow.value > min_threshold]
1272
+ if not full_relative_outflows:
1273
+ continue
1274
+
1275
+ assert len(full_relative_outflows) == 1, "There should be only 1 full relative outflow"
1276
+
1277
+ # Remove all other flows except the Flow that had 100% share from both
1278
+ # source process outflows and in target process inflows.
1279
+ flows_to_remove = list(set(rel_outflows) - set(full_relative_outflows))
1280
+
1281
+ for flow in flows_to_remove:
1282
+ # Remove the source process outflow for this year
1283
+ source_entry: ProcessEntry = df_process.at[year, flow.source_process_id]
1284
+ source_entry.remove_outflow(flow.id)
1285
+
1286
+ # Remove the target process inflow for this year
1287
+ target_entry: ProcessEntry = df_process.at[year, flow.target_process_id]
1288
+ target_entry.remove_inflow(flow.id)
1289
+
1290
+ # Remove the flow also from df_flows for this year
1291
+ df_flows.at[year, flow.id] = pd.NA
1292
+
1293
+ return df_process, df_flows
1294
+
1295
+ def _remove_isolated_processes(self, df_year_to_process_flows: pd.DataFrame) -> pd.DataFrame:
1296
+ df = df_year_to_process_flows.copy()
1297
+
1298
+ # Remove isolated processes from DataFrame
1299
+ for year in df.index:
1300
+ for process_id in df.columns:
1301
+ entry = df.at[year, process_id]
1302
+ if (not entry.inflows) and (not entry.outflows):
1303
+ df.at[year, process_id] = pd.NA
1304
+
1305
+ return df
1306
+
1307
+ def _check_process_stock_parameters(self, processes: List[Process]) -> Tuple[bool, list[str]]:
1308
+ """
1309
+ Check if Process has valid definition for stock distribution type
1310
+ :param processes: List of Processes
1311
+ :return: True if no errors, False otherwise
1312
+ """
1313
+ errors = []
1314
+ print("Checking stock distribution types...")
1315
+ allowed_distribution_types = set([name.value for name in StockDistributionType])
1316
+
1317
+ for process in processes:
1318
+ if process.stock_lifetime == 0:
1319
+ # Default to fixed and continue to next
1320
+ process.stock_distribution_type = StockDistributionType.Fixed
1321
+ continue
1322
+
1323
+ if process.stock_distribution_type not in allowed_distribution_types:
1324
+ msg = "Process {} has invalid stock distribution type '{}' in row {} in sheet '{}'".format(
1325
+ process.id, process.stock_distribution_type,
1326
+ process.row_number, self._dataprovider.sheet_name_processes)
1327
+ errors.append(msg)
1328
+
1329
+ if errors:
1330
+ # Add information about the valid stock distribution types
1331
+ errors.append("")
1332
+ errors.append("\tValid stock distribution types are:")
1333
+ for distribution_type in allowed_distribution_types:
1334
+ errors.append("\t{}".format(distribution_type))
1335
+ errors.append("")
1336
+
1337
+ return not errors, errors
1338
+
1339
+ # Check stock lifetimes
1340
+ errors = []
1341
+ for process in processes:
1342
+ if process.stock_lifetime < 0:
1343
+ msg = "Process {} has negative stock lifetime ({}) in row {} in sheet '{}'".format(
1344
+ process.id, process.stock_lifetime, process.row_number, self._dataprovider.sheet_name_processes)
1345
+ errors.append(msg)
1346
+
1347
+ if process.stock_lifetime > len(self._years):
1348
+ msg = "Process {} has stock with lifetime ({}) greater than length of simulation ({}) in row {} in sheet '{}'".format(
1349
+ process.id, process.stock_lifetime, len(self._years), process.row_number,
1350
+ self._dataprovider.sheet_name_processes)
1351
+ errors.append(msg)
1352
+
1353
+ if errors:
1354
+ return not errors, errors
1355
+
1356
+ # Check if Process has valid parameters for stock distribution parameters
1357
+ # Expected: float or dictionary with valid keys (stddev, shape, scale)
1358
+ errors = []
1359
+ print("Checking stock distribution parameters...")
1360
+ for process in processes:
1361
+ # Check that all required stock distribution parameters are present and have valid type
1362
+ found_params = process.stock_distribution_params
1363
+ required_params = RequiredStockDistributionParameters[process.stock_distribution_type]
1364
+ is_float = type(process.stock_distribution_params) is float
1365
+ num_required_params = len(required_params)
1366
+
1367
+ if not num_required_params:
1368
+ continue
1369
+
1370
+ if process.stock_distribution_params is None:
1371
+ # Check if stock has all required parameters
1372
+ s = "Stock distribution parameter '{}' needs following parameters for process '{}' in row {}:".format(
1373
+ process.stock_distribution_type, process.id, process.row_number)
1374
+ errors.append(s)
1375
+ for p in required_params:
1376
+ errors.append("\t{}".format(p))
1377
+ continue
1378
+
1379
+ else:
1380
+ for required_param_name, required_param_value_type in required_params.items():
1381
+ # Check if only float was provided
1382
+ if num_required_params and is_float:
1383
+ s = "Stock distribution parameters was number, following parameters are required "
1384
+ s += "for distribution type '{}' for process '{}' in row {}".format(
1385
+ process.stock_distribution_type, process.id, process.row_number)
1386
+ errors.append(s)
1387
+ for p in required_params.keys():
1388
+ errors.append("\t{}".format(p))
1389
+
1390
+ continue
1391
+
1392
+ # Check if required parameter name is found in stock distribution parameters
1393
+ if required_param_name not in found_params:
1394
+ s = "Stock distribution type '{}' needs following parameters for process '{}' in row {}:".format(
1395
+ process.stock_distribution_type, process.id, process.row_number)
1396
+ errors.append(s)
1397
+ for p in required_params:
1398
+ errors.append("\t{}".format(p))
1399
+ continue
1400
+
1401
+ # Check if parameter has proper value type
1402
+ for found_param_name, found_param_value in found_params.items():
1403
+ allowed_parameter_values = AllowedStockDistributionParameterValues[found_param_name]
1404
+
1405
+ # If allowed_parameter_values is empty then skip to next, nothing to check against
1406
+ if not allowed_parameter_values:
1407
+ continue
1408
+
1409
+ if found_param_value not in allowed_parameter_values:
1410
+ s = "Stock distribution parameter '{}' needs following parameters for process '{}' in row {}:".format(
1411
+ process.stock_distribution_type, process.id, process.row_number)
1412
+ errors.append(s)
1413
+ for p in allowed_parameter_values:
1414
+ errors.append("\t{}".format(p))
1415
+
1416
+ if errors:
1417
+ return not errors, errors
1418
+
1419
+ pass
1420
+
1421
+ return not errors, errors
1422
+
1423
+ def _check_stocks_in_isolated_processes(self, stocks: List[Stock], unique_process_ids: Dict[str, Process]) -> Tuple[bool, List[str]]:
1424
+ # Check for processes with stocks that are not used unique_process_ids
1425
+ """
1426
+ Check for stock in isolated Processes.
1427
+
1428
+ :param stocks: All stocks (list of Stocks)
1429
+ :param unique_process_ids: Set of unique Process IDs
1430
+ :return: Tuple (has errors (bool), list of errors (List[str])
1431
+ """
1432
+ errors = []
1433
+ for stock in stocks:
1434
+ stock_id = stock.id
1435
+ if stock_id not in unique_process_ids:
1436
+ s = "ERROR: Stock found in isolated Process {} in row {}".format(stock_id, stock.row_number)
1437
+ errors.append(s)
1438
+
1439
+ return not errors, errors
1440
+
1441
+ def _check_fill_method_requirements(self, fill_method: ParameterFillMethod, df_year_to_flows: pd.DataFrame)\
1442
+ ->Tuple[bool, List[str]]:
1443
+ """
1444
+ Check if fill method requirements are met for flows.
1445
+ Fill method Zeros: No checks needed.
1446
+ Fill method Previous: At least Flow for the starting year must be defined in the data.
1447
+ Fill method Next: At least Flow for the last year must be defined in the data.
1448
+ Fill method Interpolate: At least start AND last year have Flows defined in the data.
1449
+
1450
+ :param fill_method: Fill method (ParameterFillMethod)
1451
+ :param df_year_to_flows: DataFrame
1452
+ :return: Tuple (has errors (bool), list of errors (list[str]))
1453
+ """
1454
+ errors = []
1455
+ if fill_method is ParameterFillMethod.Zeros:
1456
+ # Flow must be defined at least for one year
1457
+ # This works always, just fills years without Flow data with Flows
1458
+ return not errors, errors
1459
+
1460
+ if fill_method is ParameterFillMethod.Previous:
1461
+ pass
1462
+
1463
+ if fill_method is ParameterFillMethod.Next:
1464
+ pass
1465
+
1466
+ if fill_method is ParameterFillMethod.Interpolate:
1467
+ pass
1468
+
1469
+ return not errors, errors
1470
+
1471
+ def _check_process_has_no_inflows_and_only_relative_outflows(self, df_year_to_process_flows: pd.DataFrame)\
1472
+ -> Tuple[bool, List[str]]:
1473
+ """
1474
+ Check for Processes that have no inflows and have only relative outflows.
1475
+ This is error in data.
1476
+
1477
+ :param df_year_to_process_flows: DataFrame (index: year, column: Process name, cell: ProcessEntry)
1478
+ :return: True if no errors, False otherwise
1479
+ """
1480
+ errors = []
1481
+ print("Checking for processes that have no inflows and only relative outflows...")
1482
+ year_to_errors = {}
1483
+ for year in df_year_to_process_flows.index:
1484
+ for process_id in df_year_to_process_flows.columns:
1485
+ entry: ProcessEntry = df_year_to_process_flows.at[year, process_id]
1486
+
1487
+ # Skip removed entries
1488
+ if pd.isna(entry):
1489
+ continue
1490
+
1491
+ process = entry.process
1492
+ flows_in = entry.inflows
1493
+ flows_out = entry.outflows
1494
+
1495
+ no_inflows = len(flows_in) == 0
1496
+ all_outflows_relative = len(flows_out) > 0 and all([not flow.is_unit_absolute_value for flow in flows_out])
1497
+
1498
+ # NOTE: This is workaround for situation where flow merging removes 0% relative outflow
1499
+ # and that causes root process checking to fail
1500
+ is_all_evaluated = np.all([flow.is_evaluated for flow in flows_out])
1501
+
1502
+ if no_inflows and all_outflows_relative and not is_all_evaluated:
1503
+ if year not in year_to_errors:
1504
+ year_to_errors[year] = []
1505
+ year_to_errors[year].append("{}".format(process.id))
1506
+
1507
+ has_errors = len(year_to_errors.keys()) > 0
1508
+ if has_errors:
1509
+ msg = "DataChecker: Found following Processes that can not be evaluated" + \
1510
+ " (= processes have no inflows and have ONLY relative outflows)\n"
1511
+
1512
+ msg += "Ensure that any process causing an error has at least one absolute incoming flow in the first year"
1513
+
1514
+ errors.append(msg)
1515
+ for year, year_errors in year_to_errors.items():
1516
+ errors.append("Year {} ({} errors):".format(year, len(year_errors)))
1517
+ for error in year_errors:
1518
+ errors.append("\t{}".format(error))
1519
+ errors.append("")
1520
+
1521
+ return not has_errors, errors
1522
+
1523
+ def _check_scenario_definitions(self, df_year_to_process_flows: pd.DataFrame) -> Tuple[bool, List[str]]:
1524
+ """
1525
+ Check scenario definitions.
1526
+
1527
+ :param df_year_to_process_flows: pd.DataFrame
1528
+ :return: Tuple (has errors: bool, list of errors)
1529
+ """
1530
+ errors = []
1531
+ scenario_definitions = self._scenario_definitions
1532
+ valid_years = list(df_year_to_process_flows.index)
1533
+ first_valid_year = min(valid_years)
1534
+ last_valid_year = max(valid_years)
1535
+ for scenario_definition in scenario_definitions:
1536
+ scenario_name = scenario_definition.name
1537
+ flow_modifiers = scenario_definition.flow_modifiers
1538
+
1539
+ for flow_modifier in flow_modifiers:
1540
+ error_message_prefix = "Error in flow modifier in row {} (scenario '{}'): ".format(
1541
+ flow_modifier.row_number, scenario_name)
1542
+
1543
+ # Check that all required node IDs are valid during the defined time range
1544
+ source_process_id = flow_modifier.source_process_id
1545
+ target_process_id = flow_modifier.target_process_id
1546
+ opposite_target_process_ids = flow_modifier.opposite_target_process_ids
1547
+
1548
+ # Is the flow modifier in valid year range?
1549
+ start_year = flow_modifier.start_year
1550
+ end_year = flow_modifier.end_year
1551
+ years = [year for year in range(flow_modifier.start_year, flow_modifier.end_year + 1)]
1552
+
1553
+ # Check rule for start year
1554
+ if start_year < first_valid_year:
1555
+ s = "" + error_message_prefix
1556
+ s += "Source Process ID '{}' start year ({}) is before first year of simulation ({})".format(
1557
+ source_process_id, start_year, first_valid_year)
1558
+ errors.append(s)
1559
+ continue
1560
+
1561
+ # Check rule for last year
1562
+ if end_year > last_valid_year:
1563
+ s = "" + error_message_prefix
1564
+ s += "Source Process ID '{}' end year ({}) is after last year of simulation ({})".format(
1565
+ source_process_id, end_year, last_valid_year)
1566
+ errors.append(s)
1567
+ continue
1568
+
1569
+ # Check if source Process ID exists for the defined year range
1570
+ for year in years:
1571
+ year_data = df_year_to_process_flows[df_year_to_process_flows.index == year]
1572
+ if source_process_id not in year_data.columns:
1573
+ s = "" + error_message_prefix
1574
+ s += "Source Process ID '{}' not defined for the year {}".format(source_process_id, year)
1575
+ errors.append(s)
1576
+ continue
1577
+
1578
+ # Check if target Process ID exists for the defined year range
1579
+ if target_process_id not in year_data.columns:
1580
+ s = "" + error_message_prefix
1581
+ s += "Target Process ID '{}' not defined for the year {}".format(source_process_id, year)
1582
+ errors.append(s)
1583
+ continue
1584
+
1585
+ entry: ProcessEntry = year_data.at[year, source_process_id]
1586
+ flows_out = entry.outflows
1587
+ target_process_id_to_flow = {flow.target_process_id: flow for flow in flows_out}
1588
+ source_to_target_flow = target_process_id_to_flow.get(target_process_id, None)
1589
+
1590
+ # Check that source Process ID is connected to target Process ID
1591
+ if source_to_target_flow is None:
1592
+ s = "" + error_message_prefix
1593
+ s += "Source Process ID '{}' does not have outflow to target Process ID {}".format(
1594
+ source_process_id, target_process_id)
1595
+ errors.append(s)
1596
+ continue
1597
+
1598
+ # Check that the flows from source Process ID to opposite target Process ID exists
1599
+ for opposite_target_process_id in opposite_target_process_ids:
1600
+ source_to_opposite_target_flow = target_process_id_to_flow.get(opposite_target_process_id, None)
1601
+ if source_to_opposite_target_flow is None:
1602
+ s = "" + error_message_prefix
1603
+ s += "Process ID '{}' does not have outflow to opposite target Process ID {}".format(
1604
+ source_process_id, opposite_target_process_id)
1605
+ errors.append(s)
1606
+ continue
1607
+
1608
+ # Check that the flow from source Process to opposite target Process has the same type (absolute
1609
+ # or relative) as the flow from source Process to target Process
1610
+ is_source_to_target_flow_abs = source_to_target_flow.is_unit_absolute_value
1611
+ is_source_to_opposite_target_flow_abs = source_to_opposite_target_flow.is_unit_absolute_value
1612
+ if is_source_to_target_flow_abs != is_source_to_opposite_target_flow_abs:
1613
+ source_to_target_flow_type = "absolute" if is_source_to_target_flow_abs else "relative"
1614
+ source_to_opposite_flow_type = "absolute" if is_source_to_opposite_target_flow_abs else "relative"
1615
+ s = "" + error_message_prefix
1616
+ s += "Source Process ID {} to target Process ID {} is {} flow".format(
1617
+ source_process_id, target_process_id, source_to_target_flow_type)
1618
+
1619
+ s += " but flow from source Process ID {} to opposite target Process ID {} is {} flow".format(
1620
+ source_process_id, opposite_target_process_id, source_to_opposite_flow_type)
1621
+ errors.append(s)
1622
+
1623
+ change_type_names = [change_type.value for change_type in ChangeType]
1624
+ function_type_names = [function_type.value for function_type in FunctionType]
1625
+ if flow_modifier.change_type not in change_type_names:
1626
+ s = "" + error_message_prefix
1627
+ s += "Invalid change type '{}'".format(flow_modifier.change_type)
1628
+ errors.append(s)
1629
+
1630
+ if flow_modifier.function_type not in function_type_names:
1631
+ s = "" + error_message_prefix
1632
+ s += "Invalid function type: '{}'".format(flow_modifier.function_type)
1633
+ errors.append(s)
1634
+
1635
+ if flow_modifier.function_type is FunctionType.Constant:
1636
+ if not flow_modifier.use_target_value:
1637
+ s = "" + error_message_prefix
1638
+ s += "No target value set"
1639
+ errors.append(s)
1640
+ else:
1641
+ if not flow_modifier.use_change_in_value and not flow_modifier.use_target_value:
1642
+ s = "" + error_message_prefix
1643
+ s += "No change in value or target value set"
1644
+ errors.append(s)
1645
+
1646
+ if flow_modifier.use_target_value:
1647
+ # Target flow type must match with the change type
1648
+ # Flow type must match with the ChangeType:
1649
+ # - Absolute flows must have change_type == ChangeType.Value
1650
+ # - Relative flows must have change_type == ChangeType.Proportional
1651
+ source_to_target_id = "{} {}".format(flow_modifier.source_process_id, flow_modifier.target_process_id)
1652
+ start_year_processes = df_year_to_process_flows.loc[flow_modifier.start_year]
1653
+
1654
+ # Get source-to-target flow mappings at start year
1655
+ source_process_entry: ProcessEntry = start_year_processes[source_process_id]
1656
+ source_process_outflows = source_process_entry.outflows
1657
+ flow_id_to_flow = {flow.id: flow for flow in source_process_outflows}
1658
+ if source_to_target_id not in flow_id_to_flow:
1659
+ s = "" + error_message_prefix
1660
+ s += "Source Process ID '{}' does not have outflow to target Process ID '{}'".format(
1661
+ source_process_id, target_process_id)
1662
+ errors.append(s)
1663
+ continue
1664
+
1665
+ source_to_target_flow = flow_id_to_flow[source_to_target_id]
1666
+ is_flow_abs = source_to_target_flow.is_unit_absolute_value
1667
+ is_flow_rel = not is_flow_abs
1668
+ if is_flow_abs and flow_modifier.change_type is not ChangeType.Value:
1669
+ s = "" + error_message_prefix
1670
+ s += "Target value change type must be Value for absolute flow"
1671
+ errors.append(s)
1672
+
1673
+ if is_flow_rel and flow_modifier.change_type is not ChangeType.Proportional:
1674
+ s = "" + error_message_prefix
1675
+ s += "Target value change type must be % for relative flow"
1676
+ errors.append(s)
1677
+
1678
+ if is_flow_rel and flow_modifier.target_value > 100:
1679
+ s = "" + error_message_prefix
1680
+ s += "Target value must be equal or less than 100% for relative flow"
1681
+ errors.append(s)
1682
+
1683
+ if flow_modifier.target_value is not None and flow_modifier.target_value < 0.0:
1684
+ s = "" + error_message_prefix
1685
+ s += "Target value must be >= 0.0 (no negative values)"
1686
+ errors.append(s)
1687
+ else:
1688
+ # NOTE: Implement checking change in delta
1689
+ # Change in delta, change flow value either by value or by factor
1690
+ # - If target flow is absolute: change_type can be either ChangeType.Value or ChangeType.Proportional
1691
+ # - If target flow is relative: change_type can be only ChangeType.Proportional
1692
+ #
1693
+ # Absolute flow:
1694
+ # Change in value (delta): 50 ABS, change in delta = 50 ABS, result = 50 ABS + 50 ABS = 100 ABS
1695
+ # Target value: 50 ABS, target value = 75, result = 50 ABS becomes 75 ABS during the defined time
1696
+
1697
+ # Relative flow:
1698
+ # Relative flow can have change in value
1699
+ # Absolute change in this case means that e.g. original value = 100, "Change in value" is 50" then
1700
+ # the resulting value is 150.
1701
+ #
1702
+ # Relative flow:
1703
+ # Absolute change here means that e.g. original value = 100 %
1704
+ pass
1705
+
1706
+ # Check if both change in value and target value is set
1707
+ if flow_modifier.use_change_in_value and flow_modifier.use_target_value:
1708
+ s = "" + error_message_prefix
1709
+ s += "Using both change in value and target value in same row is not allowed"
1710
+ errors.append(s)
1711
+
1712
+ return not errors, errors
1713
+
1714
+ def _check_color_definitions(self, colors: List[Color]) -> Tuple[bool, List[str]]:
1715
+ """
1716
+ Check if all color definitions are valid:
1717
+ - Color definition must have name
1718
+ - Color definition must have valid value (hex string starting with character '#')
1719
+
1720
+ :param colors: List of Colors
1721
+ :return: Tuple (has errors: bool, list of errors)
1722
+ """
1723
+
1724
+ # Get list of unique transformation stages
1725
+ transformation_stages = set()
1726
+ for process in self._processes:
1727
+ transformation_stages.add(process.transformation_stage)
1728
+
1729
+ errors = []
1730
+ for color in colors:
1731
+ row_errors = []
1732
+
1733
+ # Has valid name?
1734
+ if not color.name:
1735
+ s = "Color definition does not have name (row {})".format(color.row_number)
1736
+ row_errors.append(s)
1737
+
1738
+ # Has valid value?
1739
+ # - hex string prefixed with character '#'
1740
+ # - string length is 7
1741
+ if not color.value.startswith('#'):
1742
+ s = "Color definition does not start with character '#' (row {})".format(color.row_number)
1743
+ row_errors.append(s)
1744
+
1745
+ if len(color.value) != 7:
1746
+ s = "Color definition value length must be 7, example: #123456 (row {})".format(color.row_number)
1747
+ row_errors.append(s)
1748
+
1749
+ # Check if color value can be converted to hexadecimal value
1750
+ int_value = -1
1751
+ try:
1752
+ int_value = int(color.value[1:], 16)
1753
+ except ValueError:
1754
+ s = "Color definition value '{}' is not hexadecimal string (row {})".format(color.value,
1755
+ color.row_number)
1756
+ row_errors.append(s)
1757
+
1758
+ # Check that transformation stage with the name color.name exists
1759
+ if color.name:
1760
+ if color.name not in transformation_stages:
1761
+ s = "INFO: Color definition name '{}' is not transformation stage name (row {})".format(
1762
+ color.name, color.row_number)
1763
+ print(s)
1764
+
1765
+ if row_errors:
1766
+ msg = "errors" if len(row_errors) > 1 else "error"
1767
+ row_errors.insert(0, "{} {} in row {}:".format(len(row_errors), msg, color.row_number))
1768
+
1769
+ for error in row_errors:
1770
+ errors.append(error)
1771
+ errors.append("")
1772
+
1773
+ return not errors, errors
1774
+
1775
+ def _check_flow_indicators(self, df_year_to_flows: pd.DataFrame, default_conversion_factor: float = 1.0)\
1776
+ -> Tuple[bool, List[str]]:
1777
+ """
1778
+ Check and set default value to every flow that is missing value.
1779
+
1780
+ :param df_year_to_flows:
1781
+ :return: Tuple (has errors (bool), list of errors (str))
1782
+ """
1783
+ errors = []
1784
+ for year in df_year_to_flows.index:
1785
+ for flow_id in df_year_to_flows:
1786
+ flow = df_year_to_flows.at[year, flow_id]
1787
+ # NOTE: NEW, allow making "holes" to DataFrame
1788
+ if not isinstance(flow, Flow):
1789
+ continue
1790
+
1791
+ for name, indicator in flow.indicator_name_to_indicator.items():
1792
+ if indicator.conversion_factor is None:
1793
+ s = "Flow '{}' has no conversion factor defined for year {}, using default={} (row {})"\
1794
+ .format(flow_id, year, default_conversion_factor, flow.row_number)
1795
+ print("INFO: {}".format(s))
1796
+ indicator.conversion_factor = default_conversion_factor
1797
+
1798
+ try:
1799
+ # Try casting value to float and if exception happens then
1800
+ # value was not float
1801
+ conversion_factor = float(indicator.conversion_factor)
1802
+ indicator.conversion_factor = conversion_factor
1803
+ except ValueError as ex:
1804
+ s = "Flow '{}' has invalid conversion factor defined for year {} (row {})".format(
1805
+ flow_id, year, flow.row_number)
1806
+ errors.append(s)
1807
+
1808
+ return not errors, errors