aiphoria 0.0.1__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiphoria/__init__.py +59 -0
- aiphoria/core/__init__.py +55 -0
- aiphoria/core/builder.py +305 -0
- aiphoria/core/datachecker.py +1808 -0
- aiphoria/core/dataprovider.py +806 -0
- aiphoria/core/datastructures.py +1686 -0
- aiphoria/core/datavisualizer.py +431 -0
- aiphoria/core/datavisualizer_data/LICENSE +21 -0
- aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
- aiphoria/core/datavisualizer_data/pako.min.js +2 -0
- aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
- aiphoria/core/flowmodifiersolver.py +1754 -0
- aiphoria/core/flowsolver.py +1472 -0
- aiphoria/core/logger.py +113 -0
- aiphoria/core/network_graph.py +136 -0
- aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
- aiphoria/core/network_graph_data/echarts_min.js +45 -0
- aiphoria/core/network_graph_data/network_graph.html +76 -0
- aiphoria/core/network_graph_data/network_graph.js +1391 -0
- aiphoria/core/parameters.py +269 -0
- aiphoria/core/types.py +20 -0
- aiphoria/core/utils.py +362 -0
- aiphoria/core/visualizer_parameters.py +7 -0
- aiphoria/data/example_scenario.xlsx +0 -0
- aiphoria/example.py +66 -0
- aiphoria/lib/docs/dynamic_stock.py +124 -0
- aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
- aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
- aiphoria/lib/odym/modules/__init__.py +1 -0
- aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
- aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
- aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
- aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
- aiphoria/lib/odym/modules/test/__init__.py +2 -0
- aiphoria/runner.py +678 -0
- aiphoria-0.8.0.dist-info/METADATA +119 -0
- aiphoria-0.8.0.dist-info/RECORD +40 -0
- {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
- aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
- aiphoria-0.0.1.dist-info/METADATA +0 -5
- aiphoria-0.0.1.dist-info/RECORD +0 -5
- {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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
|