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,1754 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Tuple, List, Dict, Any
|
|
4
|
+
import numpy as np
|
|
5
|
+
from .flowsolver import FlowSolver
|
|
6
|
+
from .datastructures import Flow, Scenario, FlowModifier, Process
|
|
7
|
+
from .parameters import ParameterScenarioType
|
|
8
|
+
from .types import FunctionType
|
|
9
|
+
from .logger import log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlowErrorType(str, Enum):
|
|
13
|
+
"""
|
|
14
|
+
Flow error type
|
|
15
|
+
"""
|
|
16
|
+
Undefined: str = "none"
|
|
17
|
+
NotEnoughTotalOutflows: str = "not_enough_total_outflows"
|
|
18
|
+
NotEnoughOppositeFlowShares: str = "not_enough_opposite_flow_shares"
|
|
19
|
+
NotEnoughSiblingFlowShares: str = "not_enough_sibling_flow_shares"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FlowModifierSolver(object):
|
|
23
|
+
|
|
24
|
+
class FlowChangeEntry(object):
|
|
25
|
+
"""
|
|
26
|
+
Helper class for storing evaluated flow values
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self,
|
|
29
|
+
year: int = 0,
|
|
30
|
+
flow_id: str = None,
|
|
31
|
+
value: float = 0.0,
|
|
32
|
+
evaluated_share: float = 0.0,
|
|
33
|
+
evaluated_value: float = 0.0,
|
|
34
|
+
evaluated_offset: float = 0.0,
|
|
35
|
+
evaluated_share_offset: float = 0.0,
|
|
36
|
+
):
|
|
37
|
+
self._year = year
|
|
38
|
+
self._flow_id = flow_id
|
|
39
|
+
self._value = value
|
|
40
|
+
self._evaluated_share = evaluated_share
|
|
41
|
+
self._evaluated_value = evaluated_value
|
|
42
|
+
self._evaluated_offset = evaluated_offset
|
|
43
|
+
self._evaluated_share_offset = evaluated_share_offset
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
return "FlowChangeEntry: year={}, flow_id={}, value={}, evaluated_share={}, evaluated_value={}, " \
|
|
47
|
+
"evaluated_offset={}, evaluated_share_offset={}".format(
|
|
48
|
+
self.year, self.flow_id, self.value, self.evaluated_share, self.evaluated_value, self.evaluated_offset,
|
|
49
|
+
self.evaluated_share_offset)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def year(self) -> int:
|
|
53
|
+
return self._year
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def flow_id(self) -> str:
|
|
57
|
+
return self._flow_id
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def value(self) -> float:
|
|
61
|
+
return self._value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def evaluated_share(self) -> float:
|
|
65
|
+
return self._evaluated_share
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def evaluated_value(self) -> float:
|
|
69
|
+
return self._evaluated_value
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def evaluated_offset(self) -> float:
|
|
73
|
+
return self._evaluated_offset
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def evaluated_share_offset(self) -> float:
|
|
77
|
+
return self._evaluated_share_offset
|
|
78
|
+
|
|
79
|
+
class FlowErrorEntry(object):
|
|
80
|
+
def __init__(self, year: int,
|
|
81
|
+
total_outflows: float,
|
|
82
|
+
required_outflows: float,
|
|
83
|
+
flow_modifier_index: int,
|
|
84
|
+
error_type: FlowErrorType = FlowErrorType.Undefined,
|
|
85
|
+
data: Dict[str, Any] = None
|
|
86
|
+
):
|
|
87
|
+
|
|
88
|
+
if data is None:
|
|
89
|
+
data = {}
|
|
90
|
+
|
|
91
|
+
self._year = year
|
|
92
|
+
self._outflows_total = total_outflows
|
|
93
|
+
self._outflows_required = required_outflows
|
|
94
|
+
self._flow_modifier_index = flow_modifier_index
|
|
95
|
+
self._error_type = error_type
|
|
96
|
+
self._data = data
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def year(self) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Get year
|
|
102
|
+
|
|
103
|
+
:return: Year (int)
|
|
104
|
+
"""
|
|
105
|
+
return self._year
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def outflows_total(self) -> float:
|
|
109
|
+
"""
|
|
110
|
+
Get total outflows
|
|
111
|
+
|
|
112
|
+
:return: Total outflows (float)
|
|
113
|
+
"""
|
|
114
|
+
return self._outflows_total
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def outflows_required(self) -> float:
|
|
118
|
+
"""
|
|
119
|
+
Get required outflows
|
|
120
|
+
|
|
121
|
+
:return: Required outflows (float)
|
|
122
|
+
"""
|
|
123
|
+
return self._outflows_required
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def outflows_missing(self) -> float:
|
|
127
|
+
"""
|
|
128
|
+
Calculate missing outflows (total outflows - required outflows)
|
|
129
|
+
|
|
130
|
+
:return: Missing outflows (float)
|
|
131
|
+
"""
|
|
132
|
+
return self.outflows_total - self.outflows_required
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def flow_modifier_index(self) -> int:
|
|
136
|
+
"""
|
|
137
|
+
Get flow modifier index causing the error
|
|
138
|
+
|
|
139
|
+
:return: Flow modifier index (int)
|
|
140
|
+
"""
|
|
141
|
+
return self._flow_modifier_index
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def error_type(self) -> FlowErrorType:
|
|
145
|
+
"""
|
|
146
|
+
Get flow error type
|
|
147
|
+
|
|
148
|
+
:return: FlowErrorType (Enum)
|
|
149
|
+
"""
|
|
150
|
+
return self._error_type
|
|
151
|
+
|
|
152
|
+
@error_type.setter
|
|
153
|
+
def error_type(self, new_error_type) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Set flow error type
|
|
156
|
+
|
|
157
|
+
:param new_error_type: New flow error type
|
|
158
|
+
"""
|
|
159
|
+
self._error_type = new_error_type
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def data(self) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Get error data.
|
|
165
|
+
|
|
166
|
+
:return: Dictionary [str, Any]
|
|
167
|
+
"""
|
|
168
|
+
return self._data
|
|
169
|
+
|
|
170
|
+
@data.setter
|
|
171
|
+
def data(self, data: Dict[str, Any]) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Set error data.
|
|
174
|
+
|
|
175
|
+
:param data: New data (Dictionary [str, Any])
|
|
176
|
+
"""
|
|
177
|
+
self._data = data
|
|
178
|
+
|
|
179
|
+
def __init__(self, flow_solver: FlowSolver, scenario_type: ParameterScenarioType):
|
|
180
|
+
self._flow_solver: FlowSolver = flow_solver
|
|
181
|
+
self._scenario_type: ParameterScenarioType = scenario_type
|
|
182
|
+
|
|
183
|
+
def solve(self):
|
|
184
|
+
if self._scenario_type == ParameterScenarioType.Unconstrained:
|
|
185
|
+
log("Solving unconstrained scenario...")
|
|
186
|
+
ok, errors = self._solve_unconstrained_scenario()
|
|
187
|
+
if not ok:
|
|
188
|
+
sys.stdout.flush()
|
|
189
|
+
log("Errors in unconstrained scenario:", level="error")
|
|
190
|
+
for error in errors:
|
|
191
|
+
print("\t" + error)
|
|
192
|
+
log("Unconstrained scenario contained errors, stopping now...", level="error")
|
|
193
|
+
|
|
194
|
+
if self._scenario_type == ParameterScenarioType.Constrained:
|
|
195
|
+
log("Solving constrained scenario...")
|
|
196
|
+
ok, errors = self._solve_constrained_scenario()
|
|
197
|
+
if not ok:
|
|
198
|
+
sys.stdout.flush()
|
|
199
|
+
log("Errors in constrained scenario:", level="error")
|
|
200
|
+
for error in errors:
|
|
201
|
+
print("\t" + error)
|
|
202
|
+
log("Unconstrained scenario contained errors, stopping now...", level="error")
|
|
203
|
+
sys.exit(-1)
|
|
204
|
+
|
|
205
|
+
log("Scenario solving done")
|
|
206
|
+
|
|
207
|
+
def _solve_unconstrained_scenario(self) -> Tuple[bool, List[str]]:
|
|
208
|
+
# ***************************************************************************
|
|
209
|
+
# * Solve unconstrained scenario *
|
|
210
|
+
# * Introduces virtual flows if detecting that processes do not have enough *
|
|
211
|
+
# * outflows and does not stop execution *
|
|
212
|
+
# ***************************************************************************
|
|
213
|
+
errors: List[str] = []
|
|
214
|
+
flow_solver: FlowSolver = self._flow_solver
|
|
215
|
+
scenario: Scenario = self._flow_solver.get_scenario()
|
|
216
|
+
|
|
217
|
+
scenario_type = ParameterScenarioType.Unconstrained
|
|
218
|
+
flow_solver._reset_evaluated_values = True
|
|
219
|
+
|
|
220
|
+
# Evaluate new values for each flow modifier in the requested year range
|
|
221
|
+
# and group flow modifiers by source process ID. This is needed when multiple
|
|
222
|
+
# flow modifiers are affecting the same source process.
|
|
223
|
+
source_process_id_to_flow_modifier_indices = {}
|
|
224
|
+
flow_modifier_index_to_new_values = {}
|
|
225
|
+
flow_modifier_index_to_new_offsets = {}
|
|
226
|
+
flow_modifiers = scenario.scenario_definition.flow_modifiers
|
|
227
|
+
for flow_modifier_index, flow_modifier in enumerate(flow_modifiers):
|
|
228
|
+
new_values, new_offsets = self._calculate_new_flow_values(flow_modifier)
|
|
229
|
+
flow_modifier_index_to_new_values[flow_modifier_index] = new_values
|
|
230
|
+
flow_modifier_index_to_new_offsets[flow_modifier_index] = new_offsets
|
|
231
|
+
source_process_id = flow_modifier.source_process_id
|
|
232
|
+
if source_process_id not in source_process_id_to_flow_modifier_indices:
|
|
233
|
+
source_process_id_to_flow_modifier_indices[source_process_id] = []
|
|
234
|
+
source_process_id_to_flow_modifier_indices[source_process_id].append(flow_modifier_index)
|
|
235
|
+
|
|
236
|
+
# Separate into entries that affect relative and absolute flows by
|
|
237
|
+
# checking what type of flow (absolute/relative) flow_modifier is targeting.
|
|
238
|
+
# This is needed because the FlowModifiers only affect the same type of flows
|
|
239
|
+
# as the source-to-target flow is.
|
|
240
|
+
has_errors = False
|
|
241
|
+
for source_process_id, flow_modifier_indices in source_process_id_to_flow_modifier_indices.items():
|
|
242
|
+
flow_modifier_indices_for_abs_flows = []
|
|
243
|
+
flow_modifier_indices_for_rel_flows = []
|
|
244
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
245
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
246
|
+
flow = flow_solver.get_flow(flow_modifier.target_flow_id, flow_modifier.start_year)
|
|
247
|
+
if flow.is_unit_absolute_value:
|
|
248
|
+
flow_modifier_indices_for_abs_flows.append(flow_modifier_index)
|
|
249
|
+
else:
|
|
250
|
+
flow_modifier_indices_for_rel_flows.append(flow_modifier_index)
|
|
251
|
+
|
|
252
|
+
# Cache process total absolute and relative outflows for every year
|
|
253
|
+
# before any changes applied. This is used when recalculating new flow share
|
|
254
|
+
year_to_total_outflows_abs = {}
|
|
255
|
+
year_to_total_outflows_rel = {}
|
|
256
|
+
for year in scenario.scenario_data.years:
|
|
257
|
+
# NOTE: Every year might not contain all process IDs to skip those years
|
|
258
|
+
has_total_abs = False
|
|
259
|
+
has_total_rel = False
|
|
260
|
+
try:
|
|
261
|
+
year_to_total_outflows_abs[year] = flow_solver.get_process_outflows_total_abs(source_process_id, year)
|
|
262
|
+
has_total_abs = True
|
|
263
|
+
except KeyError:
|
|
264
|
+
pass
|
|
265
|
+
try:
|
|
266
|
+
year_to_total_outflows_rel[year] = flow_solver.get_process_outflows_total_rel(source_process_id, year)
|
|
267
|
+
has_total_rel = True
|
|
268
|
+
except KeyError:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
if not has_total_abs and not has_total_rel:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Solve absolute flows and relative flows independently
|
|
275
|
+
abs_flow_modifier_index_to_error_entry, abs_changeset = self._process_absolute_flows(
|
|
276
|
+
source_process_id,
|
|
277
|
+
flow_solver,
|
|
278
|
+
flow_modifier_indices_for_abs_flows,
|
|
279
|
+
flow_modifiers,
|
|
280
|
+
flow_modifier_index_to_new_values,
|
|
281
|
+
flow_modifier_index_to_new_offsets,
|
|
282
|
+
scenario_type)
|
|
283
|
+
|
|
284
|
+
rel_flow_modifier_index_to_error_entry, rel_changeset = self._process_relative_flows(
|
|
285
|
+
source_process_id,
|
|
286
|
+
flow_solver,
|
|
287
|
+
flow_modifier_indices_for_rel_flows,
|
|
288
|
+
flow_modifiers,
|
|
289
|
+
flow_modifier_index_to_new_values,
|
|
290
|
+
flow_modifier_index_to_new_offsets,
|
|
291
|
+
scenario_type)
|
|
292
|
+
|
|
293
|
+
# Check if target opposite flows or sibling flows have enough flows for the flow modifiers
|
|
294
|
+
self._check_flow_modifier_changes(flow_solver,
|
|
295
|
+
flow_modifier_indices,
|
|
296
|
+
flow_modifiers,
|
|
297
|
+
rel_changeset)
|
|
298
|
+
|
|
299
|
+
# *************************************************************
|
|
300
|
+
# * Apply changesets (order: absolute flows, relative flows) *
|
|
301
|
+
# * This order is needed because relative flow values depends *
|
|
302
|
+
# * on the remaining process outflows after applying absolute *
|
|
303
|
+
# * flows values *
|
|
304
|
+
# *************************************************************
|
|
305
|
+
# Apply changes targeting absolute flows
|
|
306
|
+
for flow_modifier_index, changeset in abs_changeset.items():
|
|
307
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
308
|
+
entry: FlowModifierSolver.FlowChangeEntry
|
|
309
|
+
for entry in changeset:
|
|
310
|
+
flow = flow_solver.get_flow(entry.flow_id, entry.year)
|
|
311
|
+
if flow.id == flow_modifier.target_flow_id:
|
|
312
|
+
# Apply calculated changes to source-to-target flow
|
|
313
|
+
# This is because that entry is always first in the list
|
|
314
|
+
# Overwrites the flow value
|
|
315
|
+
flow.value = entry.value
|
|
316
|
+
flow.evaluated_value = entry.evaluated_value
|
|
317
|
+
flow.evaluated_share = entry.evaluated_share
|
|
318
|
+
else:
|
|
319
|
+
# Apply calculated offset to evaluated value, these are all sibling flows
|
|
320
|
+
# or the target opposite flows
|
|
321
|
+
flow.value += entry.evaluated_offset
|
|
322
|
+
flow.evaluated_value += entry.evaluated_offset
|
|
323
|
+
|
|
324
|
+
# Apply changes targeting relative flows
|
|
325
|
+
for flow_modifier_index, changeset in rel_changeset.items():
|
|
326
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
327
|
+
entry: FlowModifierSolver.FlowChangeEntry
|
|
328
|
+
for entry in changeset:
|
|
329
|
+
flow = flow_solver.get_flow(entry.flow_id, entry.year)
|
|
330
|
+
if flow.id == flow_modifier.target_flow_id:
|
|
331
|
+
# Apply calculated changes to source-to-target flow
|
|
332
|
+
# This is because that entry is always first in the list
|
|
333
|
+
# Overwrites the flow share
|
|
334
|
+
flow.value = entry.value
|
|
335
|
+
flow.evaluated_value = entry.evaluated_value
|
|
336
|
+
flow.evaluated_share = entry.evaluated_share
|
|
337
|
+
else:
|
|
338
|
+
# Apply calculated offset to evaluated value, these are all sibling flows
|
|
339
|
+
# or the target opposite flows
|
|
340
|
+
total_outflows_rel = year_to_total_outflows_rel[entry.year]
|
|
341
|
+
new_evaluated_value = flow.evaluated_value + entry.evaluated_offset
|
|
342
|
+
new_value = new_evaluated_value / total_outflows_rel * 100.0
|
|
343
|
+
new_evaluated_share = new_value / 100.0
|
|
344
|
+
|
|
345
|
+
flow.value += entry.evaluated_share_offset
|
|
346
|
+
flow.evaluated_value = new_evaluated_value
|
|
347
|
+
flow.evaluated_share = new_evaluated_share
|
|
348
|
+
|
|
349
|
+
if abs_flow_modifier_index_to_error_entry:
|
|
350
|
+
# Errors in absolute flows: Unpack error entries and show errors but do not stop execution
|
|
351
|
+
print("[Unconstrained scenario] Found issues in scenarios targeting absolute flows:")
|
|
352
|
+
has_errors = True
|
|
353
|
+
min_error_entry = None
|
|
354
|
+
flow_modifier_index: int
|
|
355
|
+
error_entry: FlowModifierSolver.FlowErrorEntry
|
|
356
|
+
for flow_modifier_index, error_entry in abs_flow_modifier_index_to_error_entry.items():
|
|
357
|
+
# Gather only total outflow errors
|
|
358
|
+
if error_entry.error_type != FlowErrorType.NotEnoughTotalOutflows:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
if not min_error_entry:
|
|
362
|
+
min_error_entry = error_entry
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
if error_entry.outflows_missing < min_error_entry.outflows_missing:
|
|
366
|
+
min_error_entry = error_entry
|
|
367
|
+
|
|
368
|
+
if min_error_entry:
|
|
369
|
+
year = min_error_entry.year
|
|
370
|
+
total = min_error_entry.outflows_total
|
|
371
|
+
required = min_error_entry.outflows_required
|
|
372
|
+
missing = min_error_entry.outflows_missing
|
|
373
|
+
flow_modifier = flow_modifiers[min_error_entry.flow_modifier_index]
|
|
374
|
+
|
|
375
|
+
# TODO: Show target relative share that allows to scenario to work in
|
|
376
|
+
# TODO: error instead of absolute numbers
|
|
377
|
+
# TODO: Show easy-to-understand error message saying that
|
|
378
|
+
# TODO: a) increase/decrease the change in value to this amount to make this work
|
|
379
|
+
# TODO: b) this is the minimum/maximum target value that can be used here
|
|
380
|
+
|
|
381
|
+
s = "Process '{}'".format(source_process_id)
|
|
382
|
+
s += " "
|
|
383
|
+
s += "does not have enough outflows for absolute flows in year {}".format(year)
|
|
384
|
+
s += " "
|
|
385
|
+
s += "(total={}, required={}, missing={})".format(total, required, missing)
|
|
386
|
+
s += " "
|
|
387
|
+
s += "(row number {})".format(flow_modifier.row_number)
|
|
388
|
+
print("\tERROR: {}".format(s))
|
|
389
|
+
|
|
390
|
+
if rel_flow_modifier_index_to_error_entry:
|
|
391
|
+
# All flow modifiers in rel_flow_modifier_index_to_error_entry-map points to same source process ID
|
|
392
|
+
print("[Unconstrained scenario] Found issues in scenarios targeting relative flows:")
|
|
393
|
+
has_errors = True
|
|
394
|
+
min_error_entry = None
|
|
395
|
+
flow_modifier_index: int
|
|
396
|
+
error_entry: FlowModifierSolver.FlowErrorEntry
|
|
397
|
+
for flow_modifier_index, error_entry in rel_flow_modifier_index_to_error_entry.items():
|
|
398
|
+
# Gather only total outflow errors
|
|
399
|
+
if error_entry.error_type != FlowErrorType.NotEnoughTotalOutflows:
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if not min_error_entry:
|
|
403
|
+
min_error_entry = error_entry
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
if error_entry.outflows_missing < min_error_entry.outflows_missing:
|
|
407
|
+
min_error_entry = error_entry
|
|
408
|
+
|
|
409
|
+
if min_error_entry:
|
|
410
|
+
year = min_error_entry.year
|
|
411
|
+
total = min_error_entry.outflows_total
|
|
412
|
+
required = min_error_entry.outflows_required
|
|
413
|
+
missing = min_error_entry.outflows_missing
|
|
414
|
+
flow_modifier = flow_modifiers[min_error_entry.flow_modifier_index]
|
|
415
|
+
|
|
416
|
+
s = "Process '{}'".format(source_process_id)
|
|
417
|
+
s += " "
|
|
418
|
+
s += "does not have enough outflows for relative flows in year {}".format(year)
|
|
419
|
+
s += " "
|
|
420
|
+
s += "(total={}, required={}, missing={})".format(total, required, missing)
|
|
421
|
+
s += " "
|
|
422
|
+
s += "(row number {})".format(flow_modifier.row_number)
|
|
423
|
+
print("\tERROR: {}".format(s))
|
|
424
|
+
|
|
425
|
+
for flow_modifier_index, error_entry in rel_flow_modifier_index_to_error_entry.items():
|
|
426
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
427
|
+
if error_entry.error_type is not FlowErrorType.NotEnoughOppositeFlowShares:
|
|
428
|
+
continue
|
|
429
|
+
|
|
430
|
+
error_data = error_entry.data
|
|
431
|
+
year = error_data["year"]
|
|
432
|
+
if flow_modifier.use_change_in_value:
|
|
433
|
+
opposite_target_flow_ids = error_data["opposite_target_flow_ids"]
|
|
434
|
+
|
|
435
|
+
# required_flow_shares is flow share value between [0, 1]
|
|
436
|
+
# available_flow_shares is flow share value between [0, 1]
|
|
437
|
+
required_flow_shares = error_data["required_flow_shares"]
|
|
438
|
+
available_flow_shares = error_data["available_flow_shares"]
|
|
439
|
+
|
|
440
|
+
# Calculate maximum evaluated share for the flow modifier in range [0, 1]
|
|
441
|
+
# and then convert maximum evaluated share to maximum possible change in value
|
|
442
|
+
target_flow = flow_solver.get_flow(flow_modifier.target_flow_id, flow_modifier.start_year)
|
|
443
|
+
start_year_evaluated_share = target_flow.evaluated_share
|
|
444
|
+
max_evaluated_share = start_year_evaluated_share + available_flow_shares
|
|
445
|
+
max_change_in_value = ((max_evaluated_share / start_year_evaluated_share) - 1) * 100.0
|
|
446
|
+
|
|
447
|
+
# Round value down so there value shown here is less than the absolute maximum value
|
|
448
|
+
max_change_in_value -= 0.001
|
|
449
|
+
s = "Flow modifier in row {} targets opposite flows that do not have enough flow shares".format(
|
|
450
|
+
flow_modifier.row_number)
|
|
451
|
+
s += " "
|
|
452
|
+
s += "(required={}, available={}).".format(required_flow_shares * 100.0,
|
|
453
|
+
available_flow_shares * 100.0)
|
|
454
|
+
s += " "
|
|
455
|
+
s += "Maximum available change in value is {:.3f}%".format(max_change_in_value)
|
|
456
|
+
print("\tERROR: {}".format(s))
|
|
457
|
+
else:
|
|
458
|
+
print("TODO: Implement showing for error when using target value")
|
|
459
|
+
|
|
460
|
+
if has_errors:
|
|
461
|
+
sys.exit(-1)
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
# Recalculate process inflows/outflows
|
|
465
|
+
self._recalculate_relative_flow_evaluated_shares(flow_solver)
|
|
466
|
+
|
|
467
|
+
# Check if flow modifiers caused negative flows to target opposite flows
|
|
468
|
+
errors += self._check_flow_modifier_results(flow_solver, flow_modifiers)
|
|
469
|
+
|
|
470
|
+
# Clamp all flows to minimum of 0.0 to introduce virtual flows
|
|
471
|
+
flow_solver.clamp_flow_values_below_zero()
|
|
472
|
+
|
|
473
|
+
return not errors, errors
|
|
474
|
+
|
|
475
|
+
def _solve_constrained_scenario(self) -> Tuple[bool, List[str]]:
|
|
476
|
+
# ********************************************************************************
|
|
477
|
+
# * Solve constrained scenario *
|
|
478
|
+
# * Difference to unconstrained scenario is that no virtual flows are introduced *
|
|
479
|
+
# * and execution stops if errors are found *
|
|
480
|
+
# ********************************************************************************
|
|
481
|
+
errors: List[str] = []
|
|
482
|
+
flow_solver: FlowSolver = self._flow_solver
|
|
483
|
+
scenario: Scenario = self._flow_solver.get_scenario()
|
|
484
|
+
|
|
485
|
+
scenario_type = ParameterScenarioType.Constrained
|
|
486
|
+
flow_solver._reset_evaluated_values = True
|
|
487
|
+
|
|
488
|
+
# Evaluate new values for each flow modifier in the requested year range
|
|
489
|
+
# and group flow modifiers by source process ID. This is needed when multiple
|
|
490
|
+
# flow modifiers are affecting the same source process.
|
|
491
|
+
source_process_id_to_flow_modifier_indices = {}
|
|
492
|
+
flow_modifier_index_to_new_values = {}
|
|
493
|
+
flow_modifier_index_to_new_offsets = {}
|
|
494
|
+
flow_modifiers = scenario.scenario_definition.flow_modifiers
|
|
495
|
+
for flow_modifier_index, flow_modifier in enumerate(flow_modifiers):
|
|
496
|
+
new_values, new_offsets = self._calculate_new_flow_values(flow_modifier)
|
|
497
|
+
flow_modifier_index_to_new_values[flow_modifier_index] = new_values
|
|
498
|
+
flow_modifier_index_to_new_offsets[flow_modifier_index] = new_offsets
|
|
499
|
+
source_process_id = flow_modifier.source_process_id
|
|
500
|
+
if source_process_id not in source_process_id_to_flow_modifier_indices:
|
|
501
|
+
source_process_id_to_flow_modifier_indices[source_process_id] = []
|
|
502
|
+
source_process_id_to_flow_modifier_indices[source_process_id].append(flow_modifier_index)
|
|
503
|
+
|
|
504
|
+
# Separate into entries that affect relative and absolute flows by
|
|
505
|
+
# checking what type of flow (absolute/relative) flow_modifier is targeting.
|
|
506
|
+
# This is needed because the FlowModifiers only affect the same type of flows
|
|
507
|
+
# as the source-to-target flow is.
|
|
508
|
+
has_errors = False
|
|
509
|
+
for source_process_id, flow_modifier_indices in source_process_id_to_flow_modifier_indices.items():
|
|
510
|
+
flow_modifier_indices_for_abs_flows = []
|
|
511
|
+
flow_modifier_indices_for_rel_flows = []
|
|
512
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
513
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
514
|
+
flow = flow_solver.get_flow(flow_modifier.target_flow_id, flow_modifier.start_year)
|
|
515
|
+
if flow.is_unit_absolute_value:
|
|
516
|
+
flow_modifier_indices_for_abs_flows.append(flow_modifier_index)
|
|
517
|
+
else:
|
|
518
|
+
flow_modifier_indices_for_rel_flows.append(flow_modifier_index)
|
|
519
|
+
|
|
520
|
+
# Cache process total absolute and relative outflows for every year
|
|
521
|
+
# before any changes applied. This is used when recalculating new flow share
|
|
522
|
+
year_to_total_outflows_abs = {}
|
|
523
|
+
year_to_total_outflows_rel = {}
|
|
524
|
+
for year in scenario.scenario_data.years:
|
|
525
|
+
# NOTE: Every year might not contain all process IDs to skip those years
|
|
526
|
+
has_total_abs = False
|
|
527
|
+
has_total_rel = False
|
|
528
|
+
try:
|
|
529
|
+
year_to_total_outflows_abs[year] = flow_solver.get_process_outflows_total_abs(source_process_id, year)
|
|
530
|
+
has_total_abs = True
|
|
531
|
+
except KeyError:
|
|
532
|
+
pass
|
|
533
|
+
try:
|
|
534
|
+
year_to_total_outflows_rel[year] = flow_solver.get_process_outflows_total_rel(source_process_id, year)
|
|
535
|
+
has_total_rel = True
|
|
536
|
+
except KeyError:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
if not has_total_abs and not has_total_rel:
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
# Solve absolute flows and relative flows independently
|
|
543
|
+
abs_flow_modifier_index_to_error_entry, abs_changeset = self._process_absolute_flows(
|
|
544
|
+
source_process_id,
|
|
545
|
+
flow_solver,
|
|
546
|
+
flow_modifier_indices_for_abs_flows,
|
|
547
|
+
flow_modifiers,
|
|
548
|
+
flow_modifier_index_to_new_values,
|
|
549
|
+
flow_modifier_index_to_new_offsets,
|
|
550
|
+
scenario_type)
|
|
551
|
+
|
|
552
|
+
rel_flow_modifier_index_to_error_entry, rel_changeset = self._process_relative_flows(
|
|
553
|
+
source_process_id,
|
|
554
|
+
flow_solver,
|
|
555
|
+
flow_modifier_indices_for_rel_flows,
|
|
556
|
+
flow_modifiers,
|
|
557
|
+
flow_modifier_index_to_new_values,
|
|
558
|
+
flow_modifier_index_to_new_offsets,
|
|
559
|
+
scenario_type)
|
|
560
|
+
|
|
561
|
+
# *************************************************************
|
|
562
|
+
# * Apply changesets (order: absolute flows, relative flows) *
|
|
563
|
+
# * This order is needed because relative flow values depends *
|
|
564
|
+
# * on the remaining process outflows after applying absolute *
|
|
565
|
+
# * flows values *
|
|
566
|
+
# *************************************************************
|
|
567
|
+
# Apply changes targeting absolute flows
|
|
568
|
+
for flow_modifier_index, changeset in abs_changeset.items():
|
|
569
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
570
|
+
entry: FlowModifierSolver.FlowChangeEntry
|
|
571
|
+
for entry in changeset:
|
|
572
|
+
flow = flow_solver.get_flow(entry.flow_id, entry.year)
|
|
573
|
+
if flow.id == flow_modifier.target_flow_id:
|
|
574
|
+
# Apply calculated changes to source-to-target flow
|
|
575
|
+
# This is because that entry is always first in the list
|
|
576
|
+
# Overwrites the flow value
|
|
577
|
+
flow.value = entry.value
|
|
578
|
+
flow.evaluated_value = entry.evaluated_value
|
|
579
|
+
flow.evaluated_share = entry.evaluated_share
|
|
580
|
+
else:
|
|
581
|
+
# Apply calculated offset to evaluated value, these are all sibling flows
|
|
582
|
+
# or the target opposite flows
|
|
583
|
+
flow.value += entry.evaluated_offset
|
|
584
|
+
flow.evaluated_value += entry.evaluated_offset
|
|
585
|
+
|
|
586
|
+
# Apply changes targeting relative flows
|
|
587
|
+
for flow_modifier_index, changeset in rel_changeset.items():
|
|
588
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
589
|
+
entry: FlowModifierSolver.FlowChangeEntry
|
|
590
|
+
for entry in changeset:
|
|
591
|
+
flow = flow_solver.get_flow(entry.flow_id, entry.year)
|
|
592
|
+
if flow.id == flow_modifier.target_flow_id:
|
|
593
|
+
# Apply calculated changes to source-to-target flow
|
|
594
|
+
# This is because that entry is always first in the list
|
|
595
|
+
# Overwrites the flow share
|
|
596
|
+
flow.value = entry.value
|
|
597
|
+
flow.evaluated_value = entry.evaluated_value
|
|
598
|
+
flow.evaluated_share = entry.evaluated_share
|
|
599
|
+
else:
|
|
600
|
+
# Apply calculated offset to evaluated value, these are all sibling flows
|
|
601
|
+
# or the target opposite flows
|
|
602
|
+
total_outflows_rel = year_to_total_outflows_rel[entry.year]
|
|
603
|
+
new_evaluated_value = flow.evaluated_value + entry.evaluated_offset
|
|
604
|
+
new_value = new_evaluated_value / total_outflows_rel * 100.0
|
|
605
|
+
new_evaluated_share = new_value / 100.0
|
|
606
|
+
|
|
607
|
+
flow.value += entry.evaluated_share_offset
|
|
608
|
+
flow.evaluated_value = new_evaluated_value
|
|
609
|
+
flow.evaluated_share = new_evaluated_share
|
|
610
|
+
|
|
611
|
+
if abs_flow_modifier_index_to_error_entry:
|
|
612
|
+
# Errors in absolute flows: Unpack error entries and show errors but do not stop execution
|
|
613
|
+
print("[Constrained scenario] Found issues in scenarios targeting absolute flows:")
|
|
614
|
+
has_errors = True
|
|
615
|
+
min_error_entry = None
|
|
616
|
+
flow_modifier_index: int
|
|
617
|
+
error_entry: FlowModifierSolver.FlowErrorEntry
|
|
618
|
+
for flow_modifier_index, error_entry in abs_flow_modifier_index_to_error_entry.items():
|
|
619
|
+
# Gather only total outflow errors
|
|
620
|
+
if not error_entry.error_type == FlowErrorType.NotEnoughTotalOutflows:
|
|
621
|
+
continue
|
|
622
|
+
|
|
623
|
+
if not min_error_entry:
|
|
624
|
+
min_error_entry = error_entry
|
|
625
|
+
continue
|
|
626
|
+
|
|
627
|
+
if error_entry.outflows_missing < min_error_entry.outflows_missing:
|
|
628
|
+
min_error_entry = error_entry
|
|
629
|
+
|
|
630
|
+
if min_error_entry:
|
|
631
|
+
year = min_error_entry.year
|
|
632
|
+
total = min_error_entry.outflows_total
|
|
633
|
+
required = min_error_entry.outflows_required
|
|
634
|
+
missing = min_error_entry.outflows_missing
|
|
635
|
+
flow_modifier = flow_modifiers[min_error_entry.flow_modifier_index]
|
|
636
|
+
|
|
637
|
+
s = "Process '{}'".format(source_process_id)
|
|
638
|
+
s += " "
|
|
639
|
+
s += "does not have enough outflows for absolute flows in year {}".format(year)
|
|
640
|
+
s += " "
|
|
641
|
+
s += "(total={}, required={}, missing={})".format(total, required, missing)
|
|
642
|
+
s += " "
|
|
643
|
+
s += "(row number {})".format(flow_modifier.row_number)
|
|
644
|
+
print("\tERROR: {}".format(s))
|
|
645
|
+
|
|
646
|
+
if rel_flow_modifier_index_to_error_entry:
|
|
647
|
+
# All flow modifiers in rel_flow_modifier_index_to_error_entry-map points to same source process ID
|
|
648
|
+
print("[Constrained scenario] Found issues in scenarios targeting relative flows:")
|
|
649
|
+
has_errors = True
|
|
650
|
+
min_error_entry = None
|
|
651
|
+
flow_modifier_index: int
|
|
652
|
+
error_entry: FlowModifierSolver.FlowErrorEntry
|
|
653
|
+
for flow_modifier_index, error_entry in rel_flow_modifier_index_to_error_entry.items():
|
|
654
|
+
# Gather only total outflow errors
|
|
655
|
+
if not error_entry.error_type == FlowErrorType.NotEnoughTotalOutflows:
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
if not min_error_entry:
|
|
659
|
+
min_error_entry = error_entry
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
if error_entry.outflows_missing < min_error_entry.outflows_missing:
|
|
663
|
+
min_error_entry = error_entry
|
|
664
|
+
|
|
665
|
+
if min_error_entry:
|
|
666
|
+
year = min_error_entry.year
|
|
667
|
+
total = min_error_entry.outflows_total
|
|
668
|
+
required = min_error_entry.outflows_required
|
|
669
|
+
missing = min_error_entry.outflows_missing
|
|
670
|
+
flow_modifier = flow_modifiers[min_error_entry.flow_modifier_index]
|
|
671
|
+
|
|
672
|
+
s = "Process '{}'".format(source_process_id)
|
|
673
|
+
s += " "
|
|
674
|
+
s += "does not have enough outflows for relative flows in year {}".format(year)
|
|
675
|
+
s += " "
|
|
676
|
+
s += "(total={}, required={}, missing={})".format(total, required, missing)
|
|
677
|
+
s += " "
|
|
678
|
+
s += "(row number {})".format(flow_modifier.row_number)
|
|
679
|
+
print("\tERROR: {}".format(s))
|
|
680
|
+
|
|
681
|
+
if has_errors:
|
|
682
|
+
# Stop execution of constrained solver
|
|
683
|
+
log("Errors in Constrained scenario solver, stopping execution...", level="error")
|
|
684
|
+
sys.exit(-1)
|
|
685
|
+
|
|
686
|
+
# Recalculate process inflows/outflows
|
|
687
|
+
self._recalculate_relative_flow_evaluated_shares(flow_solver)
|
|
688
|
+
|
|
689
|
+
# Check if flow modifiers caused negative flows to target opposite flows
|
|
690
|
+
errors += self._check_flow_modifier_results(flow_solver, flow_modifiers)
|
|
691
|
+
|
|
692
|
+
return not errors, errors
|
|
693
|
+
|
|
694
|
+
def _get_process_outflow_siblings(self,
|
|
695
|
+
process_id: str = None,
|
|
696
|
+
flow_id: str = None,
|
|
697
|
+
year: int = -1,
|
|
698
|
+
only_same_type: bool = False,
|
|
699
|
+
excluded_flow_ids: List[str] = None
|
|
700
|
+
) -> List[Flow]:
|
|
701
|
+
"""
|
|
702
|
+
Get all sibling outflows for process ID and outflow ID.
|
|
703
|
+
If only_same_type is True then return only outflows that are same type as flow_id.
|
|
704
|
+
NOTE: Target flow (flow_id) is not included in the list of siblings.
|
|
705
|
+
|
|
706
|
+
:param flow_id: Target Flow ID (excluded from results)
|
|
707
|
+
:param year: Target year
|
|
708
|
+
:param only_same_type: True to return only same type sibling outflows as flow_id, False returns all siblings.
|
|
709
|
+
:param excluded_flow_ids: List of Flow IDs to exclude from siblings (optional)
|
|
710
|
+
:return: List of Flows (List[Flow])
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
if excluded_flow_ids is None:
|
|
714
|
+
excluded_flow_ids = []
|
|
715
|
+
unique_excluded_flow_ids = set(excluded_flow_ids)
|
|
716
|
+
|
|
717
|
+
all_outflows = {flow.id: flow for flow in self._flow_solver.get_process_outflows(process_id, year)}
|
|
718
|
+
target_flow = all_outflows[flow_id]
|
|
719
|
+
sibling_outflows = []
|
|
720
|
+
for outflow_id, outflow in all_outflows.items():
|
|
721
|
+
if outflow_id == flow_id:
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
if outflow_id in unique_excluded_flow_ids:
|
|
725
|
+
continue
|
|
726
|
+
|
|
727
|
+
if only_same_type:
|
|
728
|
+
if outflow.is_unit_absolute_value == target_flow.is_unit_absolute_value:
|
|
729
|
+
sibling_outflows.append(outflow)
|
|
730
|
+
else:
|
|
731
|
+
sibling_outflows.append(outflow)
|
|
732
|
+
return sibling_outflows
|
|
733
|
+
|
|
734
|
+
def _process_absolute_flows(self,
|
|
735
|
+
source_process_id: str,
|
|
736
|
+
flow_solver: FlowSolver,
|
|
737
|
+
flow_modifier_indices: List[int],
|
|
738
|
+
flow_modifiers: List[FlowModifier],
|
|
739
|
+
flow_modifier_index_to_new_values: Dict[int, List[float]],
|
|
740
|
+
flow_modifier_index_to_new_offsets: Dict[int, List[float]],
|
|
741
|
+
scenario_type: ParameterScenarioType
|
|
742
|
+
) -> Tuple[Dict[int, FlowErrorEntry], Dict[int, List[FlowChangeEntry]]]:
|
|
743
|
+
"""
|
|
744
|
+
Process absolute flows for flow modifier.
|
|
745
|
+
|
|
746
|
+
:param source_process_id: Source Process ID
|
|
747
|
+
:param flow_solver: FlowSolver
|
|
748
|
+
:param flow_modifier_indices: List of flow modifier indices that affect absolute flows
|
|
749
|
+
:param flow_modifiers: List of FlowModifiers
|
|
750
|
+
:param flow_modifier_index_to_new_values: Mapping of flow modifier index to list of new values
|
|
751
|
+
:param flow_modifier_index_to_new_offsets: Mapping of flow modifier index to list of offset values
|
|
752
|
+
:return: Tuple (Dictionary (flow modifier index to FlowErrorEntry), Dictionary (flow modifier index to changeset)
|
|
753
|
+
"""
|
|
754
|
+
# Flow modifier index to list of error entries
|
|
755
|
+
flow_modifier_index_to_error_entry = {}
|
|
756
|
+
|
|
757
|
+
# Flow modifier index to list of FlowChangeEntry-objects
|
|
758
|
+
flow_modifier_index_to_changeset = {}
|
|
759
|
+
|
|
760
|
+
# Flow modifier index to list of evaluated flow offset values
|
|
761
|
+
flow_modifier_index_to_new_values_offset = {}
|
|
762
|
+
|
|
763
|
+
# Flow modifier index to list of evaluated flow values
|
|
764
|
+
flow_modifier_index_to_new_values_actual = {}
|
|
765
|
+
|
|
766
|
+
# Year to total outflows required
|
|
767
|
+
year_to_total_outflows_required = {}
|
|
768
|
+
|
|
769
|
+
# Build yearly required outflows mapping
|
|
770
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
771
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
772
|
+
new_values_offset = flow_modifier_index_to_new_offsets[flow_modifier_index]
|
|
773
|
+
new_values = flow_modifier_index_to_new_values[flow_modifier_index]
|
|
774
|
+
flow_modifier_index_to_new_values_offset[flow_modifier_index] = new_values_offset
|
|
775
|
+
flow_modifier_index_to_new_values_actual[flow_modifier_index] = new_values
|
|
776
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
777
|
+
if year not in year_to_total_outflows_required:
|
|
778
|
+
year_to_total_outflows_required[year] = 0.0
|
|
779
|
+
year_to_total_outflows_required[year] += new_values[year_index]
|
|
780
|
+
|
|
781
|
+
# Store year to source process total absolute outflows before applying changes
|
|
782
|
+
year_to_process_total_outflows = {}
|
|
783
|
+
for flow_modifier_index in flow_modifier_index_to_new_values_actual:
|
|
784
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
785
|
+
for year in flow_modifier.get_year_range():
|
|
786
|
+
# Update the yearly total absolute outflows only once because it stays the
|
|
787
|
+
# same for all flow modifiers
|
|
788
|
+
if year in year_to_process_total_outflows:
|
|
789
|
+
continue
|
|
790
|
+
|
|
791
|
+
year_to_process_total_outflows[year] = flow_solver.get_process_outflows_total_abs(source_process_id, year)
|
|
792
|
+
|
|
793
|
+
# Check if there is enough total outflows from the source process to fulfill the flow modifier requirements
|
|
794
|
+
# before applying the changes
|
|
795
|
+
year_to_total_outflows_available = {}
|
|
796
|
+
for year, total_outflows in year_to_process_total_outflows.items():
|
|
797
|
+
total_outflows_required = year_to_total_outflows_required[year]
|
|
798
|
+
total_outflows_available = total_outflows - total_outflows_required
|
|
799
|
+
year_to_total_outflows_available[year] = total_outflows_available
|
|
800
|
+
|
|
801
|
+
# Find entry with minimum value in list
|
|
802
|
+
year_to_value = {year: value for year, value in year_to_total_outflows_available.items() if value < 0.0}
|
|
803
|
+
if year_to_value:
|
|
804
|
+
entry = min(year_to_value.items(), key=lambda x: x[1])
|
|
805
|
+
year, value = entry
|
|
806
|
+
|
|
807
|
+
# Create new error entry
|
|
808
|
+
outflows_total = year_to_process_total_outflows[year]
|
|
809
|
+
outflows_required = year_to_total_outflows_required[year]
|
|
810
|
+
new_error_entry = FlowModifierSolver.FlowErrorEntry(year,
|
|
811
|
+
outflows_total,
|
|
812
|
+
outflows_required,
|
|
813
|
+
flow_modifier_index,
|
|
814
|
+
FlowErrorType.NotEnoughTotalOutflows)
|
|
815
|
+
|
|
816
|
+
flow_modifier_index_to_error_entry[flow_modifier_index] = new_error_entry
|
|
817
|
+
|
|
818
|
+
# Exit early if there is errors
|
|
819
|
+
if scenario_type == ParameterScenarioType.Constrained and flow_modifier_index_to_error_entry:
|
|
820
|
+
return flow_modifier_index_to_error_entry, flow_modifier_index_to_changeset
|
|
821
|
+
|
|
822
|
+
# Apply changes to source to target flows
|
|
823
|
+
year_to_total_outflows_required = {}
|
|
824
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
825
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
826
|
+
new_values_offset = flow_modifier_index_to_new_offsets[flow_modifier_index]
|
|
827
|
+
new_values = flow_modifier_index_to_new_values_actual[flow_modifier_index]
|
|
828
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
829
|
+
target_flow_id = flow_modifier.target_flow_id
|
|
830
|
+
target_flow = flow_solver.get_flow(target_flow_id, year)
|
|
831
|
+
|
|
832
|
+
value_offset = new_values_offset[year_index]
|
|
833
|
+
value_actual = new_values[year_index]
|
|
834
|
+
new_value = value_actual
|
|
835
|
+
new_evaluated_share = 1.0
|
|
836
|
+
new_evaluated_value = new_value * new_evaluated_share
|
|
837
|
+
new_evaluated_offset = value_offset
|
|
838
|
+
|
|
839
|
+
# Build evaluated offset mapping
|
|
840
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
841
|
+
target_flow_id,
|
|
842
|
+
new_value,
|
|
843
|
+
new_evaluated_share,
|
|
844
|
+
new_evaluated_value,
|
|
845
|
+
new_evaluated_offset)
|
|
846
|
+
|
|
847
|
+
if flow_modifier_index not in flow_modifier_index_to_changeset:
|
|
848
|
+
flow_modifier_index_to_changeset[flow_modifier_index] = []
|
|
849
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
850
|
+
|
|
851
|
+
# Recalculate year_to_total_outflows_required
|
|
852
|
+
if year not in year_to_total_outflows_required:
|
|
853
|
+
year_to_total_outflows_required[year] = 0.0
|
|
854
|
+
year_to_total_outflows_required[year] += target_flow.evaluated_value
|
|
855
|
+
|
|
856
|
+
# Get list of all unique flow IDs used in all flow modifiers, these flows should be excluded from sibling flows
|
|
857
|
+
excluded_flow_ids = set()
|
|
858
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
859
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
860
|
+
|
|
861
|
+
# Ignore source to target flow ID
|
|
862
|
+
source_to_target_flow_id = flow_modifier.target_flow_id
|
|
863
|
+
excluded_flow_ids.add(source_to_target_flow_id)
|
|
864
|
+
|
|
865
|
+
# Ignore all opposite target flow IDs
|
|
866
|
+
for flow_id in flow_modifier.opposite_target_process_ids:
|
|
867
|
+
excluded_flow_ids.add(flow_id)
|
|
868
|
+
|
|
869
|
+
# Convert unique list of excluded flow IDs back to list
|
|
870
|
+
excluded_flow_ids = list(excluded_flow_ids)
|
|
871
|
+
|
|
872
|
+
# Apply flow modifiers to opposite targets or to all same type sibling flows
|
|
873
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
874
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
875
|
+
year_range = flow_modifier.get_year_range()
|
|
876
|
+
|
|
877
|
+
# NOTE: Skip applying changes to target flows (either siblings or target opposite flows) if set
|
|
878
|
+
if not flow_modifier.apply_to_targets:
|
|
879
|
+
continue
|
|
880
|
+
|
|
881
|
+
# Flow value offset from first year flow value
|
|
882
|
+
new_values_offset = flow_modifier_index_to_new_values_offset[flow_modifier_index]
|
|
883
|
+
if flow_modifier.has_opposite_targets:
|
|
884
|
+
for year_index, year in enumerate(year_range):
|
|
885
|
+
value_offset = new_values_offset[year_index]
|
|
886
|
+
|
|
887
|
+
# Calculate opposite flow share of the total opposite evaluated value
|
|
888
|
+
# Applying this factor allows the opposite flows to have different flow shares
|
|
889
|
+
total_opposite_flow_value = 0.0
|
|
890
|
+
for opposite_target_process_id in flow_modifier.opposite_target_process_ids:
|
|
891
|
+
opposite_flow_id = Flow.make_flow_id(source_process_id, opposite_target_process_id)
|
|
892
|
+
opposite_flow = flow_solver.get_flow(opposite_flow_id, year)
|
|
893
|
+
total_opposite_flow_value += opposite_flow.evaluated_value
|
|
894
|
+
|
|
895
|
+
for opposite_target_process_id in flow_modifier.opposite_target_process_ids:
|
|
896
|
+
opposite_flow_id = Flow.make_flow_id(source_process_id, opposite_target_process_id)
|
|
897
|
+
opposite_flow = flow_solver.get_flow(opposite_flow_id, year)
|
|
898
|
+
opposite_flow_share = opposite_flow.evaluated_value / total_opposite_flow_value
|
|
899
|
+
|
|
900
|
+
# Calculate changes, create new FlowChangeEntry and append it to changeset
|
|
901
|
+
new_value = (opposite_flow.evaluated_value - value_offset) * opposite_flow_share
|
|
902
|
+
new_evaluated_share = 1.0
|
|
903
|
+
new_evaluated_value = new_value
|
|
904
|
+
new_evaluated_offset = -value_offset * opposite_flow_share
|
|
905
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
906
|
+
opposite_flow_id,
|
|
907
|
+
new_value,
|
|
908
|
+
new_evaluated_share,
|
|
909
|
+
new_evaluated_value,
|
|
910
|
+
new_evaluated_offset)
|
|
911
|
+
|
|
912
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
913
|
+
|
|
914
|
+
else:
|
|
915
|
+
# *************************************************************************
|
|
916
|
+
# * Apply changes to proportionally to all siblings outflows of same type *
|
|
917
|
+
# *************************************************************************
|
|
918
|
+
for year_index, year in enumerate(year_range):
|
|
919
|
+
# Get total absolute outflows
|
|
920
|
+
total_outflows_abs = year_to_process_total_outflows[year]
|
|
921
|
+
total_outflows_required = year_to_total_outflows_required[year]
|
|
922
|
+
total_outflows_available = total_outflows_abs - total_outflows_required
|
|
923
|
+
value_offset = new_values_offset[year_index]
|
|
924
|
+
|
|
925
|
+
# Get all same type sibling outflows (= outflows that start from same source process
|
|
926
|
+
# and are same type as the source to target flow)
|
|
927
|
+
sibling_outflows = self._get_process_outflow_siblings(source_process_id,
|
|
928
|
+
flow_modifier.target_flow_id,
|
|
929
|
+
year,
|
|
930
|
+
only_same_type=True,
|
|
931
|
+
excluded_flow_ids=excluded_flow_ids)
|
|
932
|
+
|
|
933
|
+
# Get total sibling outflows, used to check if there is enough outflows
|
|
934
|
+
# to fulfill the flow_modifier request
|
|
935
|
+
total_sibling_outflows = np.sum([flow.evaluated_value for flow in sibling_outflows])
|
|
936
|
+
|
|
937
|
+
# Calculate new sibling values and update sibling flows
|
|
938
|
+
for flow in sibling_outflows:
|
|
939
|
+
# Calculate changes, create new FlowChangeEntry and append it to changeset
|
|
940
|
+
sibling_flow_id = flow.id
|
|
941
|
+
new_value = 0.0
|
|
942
|
+
new_evaluated_share = 1.0
|
|
943
|
+
new_evaluated_value = 0.0
|
|
944
|
+
new_evaluated_offset = 0.0
|
|
945
|
+
if total_sibling_outflows > 0.0:
|
|
946
|
+
sibling_share = flow.evaluated_value / total_sibling_outflows
|
|
947
|
+
sibling_offset = -value_offset
|
|
948
|
+
new_value = (total_outflows_available * sibling_share) + sibling_offset * sibling_share
|
|
949
|
+
new_evaluated_share = 1.0
|
|
950
|
+
new_evaluated_value = new_value
|
|
951
|
+
new_evaluated_offset = sibling_offset * sibling_share
|
|
952
|
+
|
|
953
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
954
|
+
sibling_flow_id,
|
|
955
|
+
new_value,
|
|
956
|
+
new_evaluated_share,
|
|
957
|
+
new_evaluated_value,
|
|
958
|
+
new_evaluated_offset)
|
|
959
|
+
|
|
960
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
961
|
+
|
|
962
|
+
return flow_modifier_index_to_error_entry, flow_modifier_index_to_changeset
|
|
963
|
+
|
|
964
|
+
def _process_relative_flows(self,
|
|
965
|
+
source_process_id: str,
|
|
966
|
+
flow_solver: FlowSolver,
|
|
967
|
+
flow_modifier_indices: List[int],
|
|
968
|
+
flow_modifiers: List[FlowModifier],
|
|
969
|
+
flow_modifier_index_to_new_values: Dict[int, List[float]],
|
|
970
|
+
flow_modifier_index_to_new_offsets: Dict[int, List[float]],
|
|
971
|
+
scenario_type: ParameterScenarioType
|
|
972
|
+
) -> Tuple[Dict[int, FlowErrorEntry], Dict[int, List[FlowChangeEntry]]]:
|
|
973
|
+
"""
|
|
974
|
+
Process relative flows for flow modifier.
|
|
975
|
+
|
|
976
|
+
:param source_process_id: Source Process ID
|
|
977
|
+
:param flow_solver: FlowSolver
|
|
978
|
+
:param flow_modifier_indices: List of flow modifier indices that affect relative flows
|
|
979
|
+
:param flow_modifiers: List of FlowModifiers
|
|
980
|
+
:param flow_modifier_index_to_new_values: Mapping of flow modifier index to list of new values
|
|
981
|
+
:param flow_modifier_index_to_new_offsets: Mapping of flow modifier index to list of offset values
|
|
982
|
+
:return: Tuple (Dictionary (flow modifier index to FlowErrorEntry), Dictionary (flow modifier index to changeset))
|
|
983
|
+
"""
|
|
984
|
+
# Flow modifier index to list of error entries
|
|
985
|
+
flow_modifier_index_to_error_entry = {}
|
|
986
|
+
|
|
987
|
+
# Flow modifier index to list of FlowChangeEntry-objects
|
|
988
|
+
flow_modifier_index_to_changeset = {}
|
|
989
|
+
|
|
990
|
+
# Flow modifier index to list of evaluated flow offset values
|
|
991
|
+
flow_modifier_index_to_new_values_offset = {}
|
|
992
|
+
|
|
993
|
+
# Flow modifier index to list of evaluated flow values
|
|
994
|
+
flow_modifier_index_to_new_values_actual = {}
|
|
995
|
+
|
|
996
|
+
# Year to total outflows required
|
|
997
|
+
year_to_total_outflows_required = {}
|
|
998
|
+
|
|
999
|
+
# Build yearly required outflows mapping
|
|
1000
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1001
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1002
|
+
new_values_offset = flow_modifier_index_to_new_offsets[flow_modifier_index]
|
|
1003
|
+
new_values = flow_modifier_index_to_new_values[flow_modifier_index]
|
|
1004
|
+
flow_modifier_index_to_new_values_offset[flow_modifier_index] = new_values_offset
|
|
1005
|
+
flow_modifier_index_to_new_values_actual[flow_modifier_index] = new_values
|
|
1006
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1007
|
+
total_outflows_rel = flow_solver.get_process_outflows_total_rel(flow_modifier.source_process_id, year)
|
|
1008
|
+
evaluated_value = (new_values[year_index] / 100.0) * total_outflows_rel
|
|
1009
|
+
if year not in year_to_total_outflows_required:
|
|
1010
|
+
year_to_total_outflows_required[year] = 0.0
|
|
1011
|
+
year_to_total_outflows_required[year] += evaluated_value
|
|
1012
|
+
|
|
1013
|
+
# Store year to source process total relative outflows before applying changes
|
|
1014
|
+
year_to_process_total_outflows = {}
|
|
1015
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1016
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1017
|
+
for year in flow_modifier.get_year_range():
|
|
1018
|
+
# Update the yearly total relative outflows only once because it stays the
|
|
1019
|
+
# same for all flow modifiers
|
|
1020
|
+
if year in year_to_process_total_outflows:
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
year_to_process_total_outflows[year] = flow_solver.get_process_outflows_total_rel(source_process_id, year)
|
|
1024
|
+
|
|
1025
|
+
# Check if there is enough total outflows from the source process to fulfill the flow modifier requirements
|
|
1026
|
+
# before applying the changes
|
|
1027
|
+
year_to_total_outflows_available = {}
|
|
1028
|
+
for year, total_outflows in year_to_process_total_outflows.items():
|
|
1029
|
+
total_outflows_required = year_to_total_outflows_required[year]
|
|
1030
|
+
total_outflows_available = total_outflows - total_outflows_required
|
|
1031
|
+
year_to_total_outflows_available[year] = total_outflows_available
|
|
1032
|
+
|
|
1033
|
+
# Find entry with minimum value in list
|
|
1034
|
+
year_to_value = {year: value for year, value in year_to_total_outflows_available.items() if value < 0.0}
|
|
1035
|
+
if year_to_value:
|
|
1036
|
+
entry = min(year_to_value.items(), key=lambda x: x[1])
|
|
1037
|
+
year, value = entry
|
|
1038
|
+
|
|
1039
|
+
# Create new error entry
|
|
1040
|
+
outflows_total = year_to_process_total_outflows[year]
|
|
1041
|
+
outflows_required = year_to_total_outflows_required[year]
|
|
1042
|
+
new_error_entry = FlowModifierSolver.FlowErrorEntry(year,
|
|
1043
|
+
outflows_total,
|
|
1044
|
+
outflows_required,
|
|
1045
|
+
flow_modifier_index,
|
|
1046
|
+
FlowErrorType.NotEnoughTotalOutflows)
|
|
1047
|
+
|
|
1048
|
+
flow_modifier_index_to_error_entry[flow_modifier_index] = new_error_entry
|
|
1049
|
+
|
|
1050
|
+
# Check that there is enough flow share in target opposite flows
|
|
1051
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1052
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1053
|
+
source_process_id = flow_modifier.source_process_id
|
|
1054
|
+
new_values_offset = flow_modifier_index_to_new_values_offset[flow_modifier_index]
|
|
1055
|
+
new_values_actual = flow_modifier_index_to_new_values_actual[flow_modifier_index]
|
|
1056
|
+
|
|
1057
|
+
# Check if target opposite flows have enough flow share to fulfill the target flow change
|
|
1058
|
+
if flow_modifier.has_opposite_targets:
|
|
1059
|
+
opposite_target_flow_ids = flow_modifier.get_opposite_target_flow_ids()
|
|
1060
|
+
|
|
1061
|
+
year_to_required_flow_share = {}
|
|
1062
|
+
year_to_available_flow_shares = {}
|
|
1063
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1064
|
+
available_opposite_flow_shares = 0.0
|
|
1065
|
+
for opposite_flow_id in opposite_target_flow_ids:
|
|
1066
|
+
opposite_flow = flow_solver.get_flow(opposite_flow_id, year)
|
|
1067
|
+
available_opposite_flow_shares += opposite_flow.evaluated_share
|
|
1068
|
+
|
|
1069
|
+
# Map year to available flow shares and convert the required flow share to [0, 1] range
|
|
1070
|
+
year_to_available_flow_shares[year] = available_opposite_flow_shares
|
|
1071
|
+
year_to_required_flow_share[year] = (new_values_actual[year_index] - new_values_actual[0]) / 100.0
|
|
1072
|
+
|
|
1073
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1074
|
+
required_flow_shares = year_to_required_flow_share[year]
|
|
1075
|
+
available_sibling_flow_shares = year_to_available_flow_shares[year]
|
|
1076
|
+
if required_flow_shares > available_sibling_flow_shares:
|
|
1077
|
+
outflows_total = year_to_process_total_outflows[year]
|
|
1078
|
+
outflows_required = year_to_total_outflows_required[year]
|
|
1079
|
+
data = {
|
|
1080
|
+
"year": year,
|
|
1081
|
+
"opposite_target_flow_ids": [opposite_target_flow_ids],
|
|
1082
|
+
"required_flow_shares": required_flow_shares,
|
|
1083
|
+
"available_flow_shares": available_sibling_flow_shares,
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
# Create new error entry
|
|
1087
|
+
new_error_entry = FlowModifierSolver.FlowErrorEntry(year,
|
|
1088
|
+
outflows_total,
|
|
1089
|
+
outflows_required,
|
|
1090
|
+
flow_modifier_index,
|
|
1091
|
+
FlowErrorType.NotEnoughOppositeFlowShares,
|
|
1092
|
+
data,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
flow_modifier_index_to_error_entry[flow_modifier_index] = new_error_entry
|
|
1096
|
+
|
|
1097
|
+
else:
|
|
1098
|
+
# Check if all sibling flows have enough flow share to fulfill the target flow change
|
|
1099
|
+
year_to_required_flow_share = {}
|
|
1100
|
+
year_to_available_flow_shares = {}
|
|
1101
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1102
|
+
# Get available total sibling share for this year
|
|
1103
|
+
sibling_flows = self._get_process_outflow_siblings(flow_modifier.source_process_id,
|
|
1104
|
+
flow_modifier.target_flow_id,
|
|
1105
|
+
year,
|
|
1106
|
+
only_same_type=True)
|
|
1107
|
+
|
|
1108
|
+
available_sibling_flow_shares = 0.0
|
|
1109
|
+
for flow in sibling_flows:
|
|
1110
|
+
available_sibling_flow_shares += flow.evaluated_share
|
|
1111
|
+
|
|
1112
|
+
# Map year to available flow shares and convert the required flow share to [0, 1] range
|
|
1113
|
+
year_to_available_flow_shares[year] = available_sibling_flow_shares
|
|
1114
|
+
year_to_required_flow_share[year] = (new_values_actual[year_index] - new_values_actual[0]) / 100.0
|
|
1115
|
+
|
|
1116
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1117
|
+
required_flow_shares = year_to_required_flow_share[year]
|
|
1118
|
+
available_flow_shares = year_to_available_flow_shares[year]
|
|
1119
|
+
if required_flow_shares > available_flow_shares:
|
|
1120
|
+
outflows_total = year_to_process_total_outflows[year]
|
|
1121
|
+
outflows_required = year_to_total_outflows_required[year]
|
|
1122
|
+
data = {
|
|
1123
|
+
"year": year,
|
|
1124
|
+
"required_flow_shares": required_flow_shares,
|
|
1125
|
+
"available_flow_shares": available_flow_shares,
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
# Create new error entry
|
|
1129
|
+
new_error_entry = FlowModifierSolver.FlowErrorEntry(year,
|
|
1130
|
+
outflows_total,
|
|
1131
|
+
outflows_required,
|
|
1132
|
+
flow_modifier_index,
|
|
1133
|
+
FlowErrorType.NotEnoughSiblingFlowShares,
|
|
1134
|
+
data,
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
flow_modifier_index_to_error_entry[flow_modifier_index] = new_error_entry
|
|
1138
|
+
|
|
1139
|
+
# Exit early if there is errors
|
|
1140
|
+
if scenario_type == ParameterScenarioType.Constrained and flow_modifier_index_to_error_entry:
|
|
1141
|
+
return flow_modifier_index_to_error_entry, flow_modifier_index_to_changeset
|
|
1142
|
+
|
|
1143
|
+
# Apply changes to source to target flows
|
|
1144
|
+
year_to_total_outflows_required = {}
|
|
1145
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1146
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1147
|
+
new_values_offset = flow_modifier_index_to_new_offsets[flow_modifier_index]
|
|
1148
|
+
new_values = flow_modifier_index_to_new_values_actual[flow_modifier_index]
|
|
1149
|
+
for year_index, year in enumerate(flow_modifier.get_year_range()):
|
|
1150
|
+
target_flow_id = flow_modifier.target_flow_id
|
|
1151
|
+
target_flow = flow_solver.get_flow(target_flow_id, year)
|
|
1152
|
+
total_outflows_rel = year_to_process_total_outflows[year]
|
|
1153
|
+
|
|
1154
|
+
value_offset = new_values_offset[year_index]
|
|
1155
|
+
value_actual = new_values[year_index]
|
|
1156
|
+
new_value = value_actual
|
|
1157
|
+
new_evaluated_share = new_value / 100.0
|
|
1158
|
+
new_evaluated_value = new_evaluated_share * total_outflows_rel
|
|
1159
|
+
new_evaluated_offset = value_offset
|
|
1160
|
+
|
|
1161
|
+
# Build evaluated offset mapping
|
|
1162
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
1163
|
+
target_flow_id,
|
|
1164
|
+
new_value,
|
|
1165
|
+
new_evaluated_share,
|
|
1166
|
+
new_evaluated_value,
|
|
1167
|
+
new_evaluated_offset)
|
|
1168
|
+
|
|
1169
|
+
if flow_modifier_index not in flow_modifier_index_to_changeset:
|
|
1170
|
+
flow_modifier_index_to_changeset[flow_modifier_index] = []
|
|
1171
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
1172
|
+
|
|
1173
|
+
# Recalculate year_to_total_outflows_required
|
|
1174
|
+
if year not in year_to_total_outflows_required:
|
|
1175
|
+
year_to_total_outflows_required[year] = 0.0
|
|
1176
|
+
year_to_total_outflows_required[year] += target_flow.evaluated_value
|
|
1177
|
+
|
|
1178
|
+
# Get list of all unique flow IDs used in all flow modifiers, these flows should be excluded from sibling flows
|
|
1179
|
+
excluded_flow_ids = set()
|
|
1180
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1181
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1182
|
+
|
|
1183
|
+
# Ignore source to target flow ID
|
|
1184
|
+
source_to_target_flow_id = flow_modifier.target_flow_id
|
|
1185
|
+
excluded_flow_ids.add(source_to_target_flow_id)
|
|
1186
|
+
|
|
1187
|
+
# Ignore all opposite target flow IDs
|
|
1188
|
+
for flow_id in flow_modifier.opposite_target_process_ids:
|
|
1189
|
+
excluded_flow_ids.add(flow_id)
|
|
1190
|
+
|
|
1191
|
+
# Convert unique list of excluded flow IDs back to list
|
|
1192
|
+
excluded_flow_ids = list(excluded_flow_ids)
|
|
1193
|
+
|
|
1194
|
+
# Apply flow modifiers to opposite targets or to all same type sibling flows
|
|
1195
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1196
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1197
|
+
year_range = flow_modifier.get_year_range()
|
|
1198
|
+
|
|
1199
|
+
# NOTE: Skip applying changes to target flows (either siblings or target opposite flows) if set
|
|
1200
|
+
if not flow_modifier.apply_to_targets:
|
|
1201
|
+
continue
|
|
1202
|
+
|
|
1203
|
+
# Flow share offset from first year flow share
|
|
1204
|
+
new_values_actual = flow_modifier_index_to_new_values_actual[flow_modifier_index]
|
|
1205
|
+
new_values_offset = flow_modifier_index_to_new_values_offset[flow_modifier_index]
|
|
1206
|
+
if flow_modifier.has_opposite_targets:
|
|
1207
|
+
for year_index, year in enumerate(year_range):
|
|
1208
|
+
value_actual = new_values_actual[year_index]
|
|
1209
|
+
value_offset = new_values_offset[year_index]
|
|
1210
|
+
total_outflows_rel = year_to_process_total_outflows[year]
|
|
1211
|
+
|
|
1212
|
+
# Calculate opposite flow share of the total opposite evaluated value
|
|
1213
|
+
# Applying this factor allows the opposite flows to have different flow shares
|
|
1214
|
+
total_opposite_flow_value = 0.0
|
|
1215
|
+
for opposite_target_process_id in flow_modifier.opposite_target_process_ids:
|
|
1216
|
+
opposite_flow_id = Flow.make_flow_id(source_process_id, opposite_target_process_id)
|
|
1217
|
+
opposite_flow = flow_solver.get_flow(opposite_flow_id, year)
|
|
1218
|
+
total_opposite_flow_value += opposite_flow.evaluated_value
|
|
1219
|
+
|
|
1220
|
+
for opposite_target_process_id in flow_modifier.opposite_target_process_ids:
|
|
1221
|
+
opposite_flow_id = Flow.make_flow_id(source_process_id, opposite_target_process_id)
|
|
1222
|
+
opposite_flow = flow_solver.get_flow(opposite_flow_id, year)
|
|
1223
|
+
opposite_flow_share = opposite_flow.evaluated_value / total_opposite_flow_value
|
|
1224
|
+
|
|
1225
|
+
# Calculate changes, create new FlowChangeEntry and append it to changeset
|
|
1226
|
+
new_value = (opposite_flow.evaluated_value - value_offset) * opposite_flow_share
|
|
1227
|
+
new_evaluated_share = new_value / total_outflows_rel
|
|
1228
|
+
new_evaluated_value = new_value
|
|
1229
|
+
new_evaluated_offset = -value_offset * opposite_flow_share
|
|
1230
|
+
new_evaluated_share_offset = -(new_values_actual[year_index] - new_values_actual[0]) * opposite_flow_share
|
|
1231
|
+
|
|
1232
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
1233
|
+
opposite_flow_id,
|
|
1234
|
+
new_value,
|
|
1235
|
+
new_evaluated_share,
|
|
1236
|
+
new_evaluated_value,
|
|
1237
|
+
new_evaluated_offset,
|
|
1238
|
+
new_evaluated_share_offset)
|
|
1239
|
+
|
|
1240
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
1241
|
+
|
|
1242
|
+
else:
|
|
1243
|
+
# *************************************************************************
|
|
1244
|
+
# * Apply changes to proportionally to all siblings outflows of same type *
|
|
1245
|
+
# *************************************************************************
|
|
1246
|
+
for year_index, year in enumerate(year_range):
|
|
1247
|
+
# Get total relative outflows for process
|
|
1248
|
+
total_outflows_rel = year_to_process_total_outflows[year]
|
|
1249
|
+
total_outflows_required = year_to_total_outflows_required[year]
|
|
1250
|
+
total_outflows_available = total_outflows_rel - total_outflows_required
|
|
1251
|
+
value_offset = new_values_offset[year_index]
|
|
1252
|
+
|
|
1253
|
+
# Get all same type sibling outflows (= outflows that start from same source process
|
|
1254
|
+
# and are same type as the source to target flow)
|
|
1255
|
+
sibling_outflows = self._get_process_outflow_siblings(source_process_id,
|
|
1256
|
+
flow_modifier.target_flow_id,
|
|
1257
|
+
year,
|
|
1258
|
+
only_same_type=True,
|
|
1259
|
+
excluded_flow_ids=excluded_flow_ids)
|
|
1260
|
+
|
|
1261
|
+
# Get total sibling outflows, used to check if there is enough outflows
|
|
1262
|
+
# to fulfill the flow_modifier request
|
|
1263
|
+
total_sibling_outflows = np.sum([flow.evaluated_value for flow in sibling_outflows])
|
|
1264
|
+
|
|
1265
|
+
# Calculate new sibling values and update sibling flows
|
|
1266
|
+
for flow in sibling_outflows:
|
|
1267
|
+
# Calculate changes, create new FlowChangeEntry and append it to changeset
|
|
1268
|
+
sibling_flow_id = flow.id
|
|
1269
|
+
new_value = 0.0
|
|
1270
|
+
new_evaluated_share = 1.0
|
|
1271
|
+
new_evaluated_value = 0.0
|
|
1272
|
+
new_evaluated_offset = 0.0
|
|
1273
|
+
new_evaluated_share_offset = 0.0
|
|
1274
|
+
if total_sibling_outflows > 0.0:
|
|
1275
|
+
sibling_share = flow.evaluated_value / total_sibling_outflows
|
|
1276
|
+
sibling_offset = -value_offset
|
|
1277
|
+
new_value = (total_outflows_available * sibling_share) + sibling_offset * sibling_share
|
|
1278
|
+
new_evaluated_share = 1.0
|
|
1279
|
+
new_evaluated_value = new_value
|
|
1280
|
+
new_evaluated_offset = sibling_offset * sibling_share
|
|
1281
|
+
new_evaluated_share_offset = -(new_values_actual[year_index] - new_values_actual[0]) * sibling_share
|
|
1282
|
+
|
|
1283
|
+
# Handle FunctionType.Constant differently for relative flows
|
|
1284
|
+
# There is no change in offset because of the offset stays the same during the whole
|
|
1285
|
+
# year range of the FlowModifier
|
|
1286
|
+
if flow_modifier.function_type == FunctionType.Constant:
|
|
1287
|
+
new_evaluated_share_offset = new_evaluated_offset
|
|
1288
|
+
|
|
1289
|
+
new_entry = FlowModifierSolver.FlowChangeEntry(year,
|
|
1290
|
+
sibling_flow_id,
|
|
1291
|
+
new_value,
|
|
1292
|
+
new_evaluated_share,
|
|
1293
|
+
new_evaluated_value,
|
|
1294
|
+
new_evaluated_offset,
|
|
1295
|
+
new_evaluated_share_offset)
|
|
1296
|
+
|
|
1297
|
+
flow_modifier_index_to_changeset[flow_modifier_index].append(new_entry)
|
|
1298
|
+
|
|
1299
|
+
return flow_modifier_index_to_error_entry, flow_modifier_index_to_changeset
|
|
1300
|
+
|
|
1301
|
+
def _calculate_new_flow_values(self, flow_modifier: FlowModifier) -> Tuple[List[float], List[float]]:
|
|
1302
|
+
"""
|
|
1303
|
+
Calculate new flow values for flow modifier.
|
|
1304
|
+
If flow_modifier targets absolute flow, returns tuple of (evaluated values, evaluated offset values)
|
|
1305
|
+
If flow_modifier targets relative flow, returns tuple of (evaluated flow shares, evaluated offset values)
|
|
1306
|
+
|
|
1307
|
+
Does not modify the target flow.
|
|
1308
|
+
|
|
1309
|
+
:param flow_modifier: Target FlowModifier
|
|
1310
|
+
:return: Tuple (list of evaluated flow values, list of evaluated flow value offsets)
|
|
1311
|
+
"""
|
|
1312
|
+
|
|
1313
|
+
flow_solver: FlowSolver = self._flow_solver
|
|
1314
|
+
year_range = flow_modifier.get_year_range()
|
|
1315
|
+
source_to_target_flow_id = flow_modifier.target_flow_id
|
|
1316
|
+
|
|
1317
|
+
# ******************************************
|
|
1318
|
+
# * Create offset values for flow modifier *
|
|
1319
|
+
# ******************************************
|
|
1320
|
+
# Absolute flows: new evaluated flow value, relative flows: new evaluated flow share (0 - 100 range)
|
|
1321
|
+
new_values = [0.0 for _ in year_range]
|
|
1322
|
+
|
|
1323
|
+
# Evaluated value from evaluated base value, always evaluated flow value (not share)
|
|
1324
|
+
new_offsets = [0.0 for _ in year_range]
|
|
1325
|
+
|
|
1326
|
+
# Get total outflows (absolute + relative) for first year
|
|
1327
|
+
first_year = year_range[0]
|
|
1328
|
+
total_outflows_abs = flow_solver.get_process_outflows_total_abs(flow_modifier.source_process_id, first_year)
|
|
1329
|
+
total_outflows_rel = flow_solver.get_process_outflows_total_rel(flow_modifier.source_process_id, first_year)
|
|
1330
|
+
total_outflows = total_outflows_abs + total_outflows_rel
|
|
1331
|
+
first_year_flow = self._flow_solver.get_flow(source_to_target_flow_id, first_year)
|
|
1332
|
+
|
|
1333
|
+
if flow_modifier.function_type == FunctionType.Constant:
|
|
1334
|
+
# NOTE: Constant replaces the values during the year range
|
|
1335
|
+
new_values = [flow_modifier.target_value for _ in year_range]
|
|
1336
|
+
|
|
1337
|
+
# Change in value (delta)
|
|
1338
|
+
if flow_modifier.use_change_in_value:
|
|
1339
|
+
value_start = 0.0
|
|
1340
|
+
if first_year_flow.is_unit_absolute_value:
|
|
1341
|
+
# Absolute flow, use flow evaluated value as value_start
|
|
1342
|
+
value_start = first_year_flow.evaluated_value
|
|
1343
|
+
else:
|
|
1344
|
+
# Relative flow, use flow share as value_start
|
|
1345
|
+
value_start = first_year_flow.evaluated_share * 100.0
|
|
1346
|
+
|
|
1347
|
+
if flow_modifier.function_type == FunctionType.Linear:
|
|
1348
|
+
new_values = np.linspace(start=0, stop=flow_modifier.change_in_value, num=len(year_range))
|
|
1349
|
+
|
|
1350
|
+
if flow_modifier.function_type == FunctionType.Exponential:
|
|
1351
|
+
# NOTE: Is this function working properly with target value?
|
|
1352
|
+
new_values = np.logspace(start=0, stop=1, num=len(year_range))
|
|
1353
|
+
|
|
1354
|
+
if flow_modifier.function_type == FunctionType.Sigmoid:
|
|
1355
|
+
# NOTE: Is this function working properly with target value?
|
|
1356
|
+
new_values = np.linspace(start=-flow_modifier.change_in_value,
|
|
1357
|
+
stop=flow_modifier.change_in_value,
|
|
1358
|
+
num=len(year_range))
|
|
1359
|
+
|
|
1360
|
+
new_values = flow_modifier.change_in_value / (1.0 + np.exp(-new_values))
|
|
1361
|
+
|
|
1362
|
+
# Target value (current to target)
|
|
1363
|
+
if flow_modifier.use_target_value:
|
|
1364
|
+
value_start = 0.0
|
|
1365
|
+
if first_year_flow.is_unit_absolute_value:
|
|
1366
|
+
# Absolute flow, use flow evaluated value as value_start
|
|
1367
|
+
value_start = first_year_flow.evaluated_value
|
|
1368
|
+
else:
|
|
1369
|
+
# Relative flow, use flow share as value_start
|
|
1370
|
+
value_start = first_year_flow.evaluated_share * 100.0
|
|
1371
|
+
|
|
1372
|
+
if flow_modifier.function_type == FunctionType.Linear:
|
|
1373
|
+
new_values = np.linspace(start=value_start, stop=flow_modifier.target_value, num=len(year_range))
|
|
1374
|
+
|
|
1375
|
+
if flow_modifier.function_type == FunctionType.Exponential:
|
|
1376
|
+
new_values = np.logspace(start=0, stop=1, num=len(year_range))
|
|
1377
|
+
|
|
1378
|
+
if flow_modifier.function_type == FunctionType.Sigmoid:
|
|
1379
|
+
new_values = np.linspace(start=-flow_modifier.change_in_value,
|
|
1380
|
+
stop=flow_modifier.change_in_value,
|
|
1381
|
+
num=len(year_range))
|
|
1382
|
+
|
|
1383
|
+
new_values = flow_modifier.change_in_value / (1.0 + np.exp(-new_values))
|
|
1384
|
+
|
|
1385
|
+
# *******************************************************************************
|
|
1386
|
+
# * Calculate target values for flow modifier from start year and offset values *
|
|
1387
|
+
# *******************************************************************************
|
|
1388
|
+
for year_index, year in enumerate(year_range):
|
|
1389
|
+
base_value = first_year_flow.value
|
|
1390
|
+
base_evaluated_value = first_year_flow.evaluated_value
|
|
1391
|
+
base_evaluated_share = first_year_flow.evaluated_share
|
|
1392
|
+
|
|
1393
|
+
# Absolute flow
|
|
1394
|
+
if first_year_flow.is_unit_absolute_value:
|
|
1395
|
+
if flow_modifier.is_change_type_value:
|
|
1396
|
+
# Change by absolute value, either delta change or move toward target value
|
|
1397
|
+
if flow_modifier.use_change_in_value:
|
|
1398
|
+
# Increase/decrease by absolute value
|
|
1399
|
+
offset = new_values[year_index]
|
|
1400
|
+
new_values[year_index] = base_evaluated_value + offset
|
|
1401
|
+
|
|
1402
|
+
# Calculate evaluated offset from base evaluated value
|
|
1403
|
+
new_offsets[year_index] = offset
|
|
1404
|
+
|
|
1405
|
+
if flow_modifier.use_target_value:
|
|
1406
|
+
# Move toward absolute target value each year
|
|
1407
|
+
offset = new_values[year_index]
|
|
1408
|
+
new_values[year_index] = offset
|
|
1409
|
+
|
|
1410
|
+
# Calculate evaluated offset from base evaluated value
|
|
1411
|
+
new_offsets[year_index] = offset - base_evaluated_value
|
|
1412
|
+
|
|
1413
|
+
if flow_modifier.is_change_type_proportional:
|
|
1414
|
+
# Proportional/percentual change of value, use delta change only
|
|
1415
|
+
offset = new_values[year_index]
|
|
1416
|
+
new_values[year_index] = base_evaluated_value + base_evaluated_value * offset / 100.0
|
|
1417
|
+
|
|
1418
|
+
# Calculate evaluated offset from base evaluated value
|
|
1419
|
+
new_offsets[year_index] = base_evaluated_value * offset / 100.0
|
|
1420
|
+
|
|
1421
|
+
# Relative flow
|
|
1422
|
+
else:
|
|
1423
|
+
if flow_modifier.use_change_in_value:
|
|
1424
|
+
offset = new_values[year_index]
|
|
1425
|
+
new_values[year_index] = base_value + base_evaluated_share * offset
|
|
1426
|
+
|
|
1427
|
+
# Calculate evaluated offset from base evaluated value
|
|
1428
|
+
new_offsets[year_index] = base_evaluated_value * offset / 100.0
|
|
1429
|
+
|
|
1430
|
+
if flow_modifier.use_target_value:
|
|
1431
|
+
offset = new_values[year_index]
|
|
1432
|
+
new_values[year_index] = offset
|
|
1433
|
+
|
|
1434
|
+
# Calculate evaluated offset from new flow share
|
|
1435
|
+
new_offset = offset - base_evaluated_share * 100
|
|
1436
|
+
new_evaluated_offset = (new_offset / 100.0) * total_outflows
|
|
1437
|
+
new_offsets[year_index] = new_evaluated_offset
|
|
1438
|
+
|
|
1439
|
+
return new_values, new_offsets
|
|
1440
|
+
|
|
1441
|
+
def _check_flow_modifier_results(self,
|
|
1442
|
+
flow_solver: FlowSolver = None,
|
|
1443
|
+
flow_modifiers: List[FlowModifier] = None) -> List[str]:
|
|
1444
|
+
"""
|
|
1445
|
+
Check if applying flow modifiers caused negative flows in target opposite flows.
|
|
1446
|
+
|
|
1447
|
+
:param flow_solver: Target FlowSolver
|
|
1448
|
+
:param flow_modifiers: List of FlowModifiers
|
|
1449
|
+
:return: List of errors (empty list == no errors)
|
|
1450
|
+
"""
|
|
1451
|
+
errors = []
|
|
1452
|
+
|
|
1453
|
+
if flow_modifiers is None:
|
|
1454
|
+
flow_modifiers = []
|
|
1455
|
+
|
|
1456
|
+
if not flow_solver:
|
|
1457
|
+
raise Exception("Parameter flow_solver is None, check calling code")
|
|
1458
|
+
|
|
1459
|
+
# Check that all flows that are affected by the flow modifiers have evaluated value >= 0.0
|
|
1460
|
+
# This could be caused by flow modifier that has opposite target flows that do not have enough flow
|
|
1461
|
+
# and it will cause negative flow
|
|
1462
|
+
affected_flow_id_to_flow_modifier_indices = {}
|
|
1463
|
+
flow_modifier_index_to_year_to_affected_flow_ids = {}
|
|
1464
|
+
for flow_modifier_index, flow_modifier in enumerate(flow_modifiers):
|
|
1465
|
+
if flow_modifier_index not in flow_modifier_index_to_year_to_affected_flow_ids:
|
|
1466
|
+
flow_modifier_index_to_year_to_affected_flow_ids[flow_modifier_index] = {}
|
|
1467
|
+
|
|
1468
|
+
for year in flow_modifier.get_year_range():
|
|
1469
|
+
year_to_affected_flows_ids = flow_modifier_index_to_year_to_affected_flow_ids[flow_modifier_index]
|
|
1470
|
+
|
|
1471
|
+
if year not in year_to_affected_flows_ids:
|
|
1472
|
+
year_to_affected_flows_ids[year] = []
|
|
1473
|
+
affected_flow_ids = year_to_affected_flows_ids[year]
|
|
1474
|
+
|
|
1475
|
+
if flow_modifier.has_opposite_targets:
|
|
1476
|
+
# # Get list of all opposite flow IDs
|
|
1477
|
+
# for target_process_id in flow_modifier.opposite_target_process_ids:
|
|
1478
|
+
# opposite_flow_id = Flow.make_flow_id(flow_modifier.source_process_id, target_process_id)
|
|
1479
|
+
# affected_flow_ids.append(opposite_flow_id)
|
|
1480
|
+
affected_flow_ids += flow_modifier.get_opposite_target_flow_ids()
|
|
1481
|
+
else:
|
|
1482
|
+
# Get list of all same type sibling flows and unpack as flow IDs
|
|
1483
|
+
sibling_flows = self._get_process_outflow_siblings(flow_modifier.source_process_id,
|
|
1484
|
+
flow_modifier.target_flow_id,
|
|
1485
|
+
year,
|
|
1486
|
+
only_same_type=True)
|
|
1487
|
+
|
|
1488
|
+
affected_flow_ids += [flow.id for flow in sibling_flows]
|
|
1489
|
+
|
|
1490
|
+
for flow_id in affected_flow_ids:
|
|
1491
|
+
if flow_id not in affected_flow_id_to_flow_modifier_indices:
|
|
1492
|
+
affected_flow_id_to_flow_modifier_indices[flow_id] = set()
|
|
1493
|
+
affected_flow_id_to_flow_modifier_indices[flow_id].add(flow_modifier_index)
|
|
1494
|
+
|
|
1495
|
+
# Check if any affected flows is < 0.0 and find year with the smallest flow value
|
|
1496
|
+
for affected_flow_id, flow_modifier_indices in affected_flow_id_to_flow_modifier_indices.items():
|
|
1497
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1498
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1499
|
+
years = flow_modifier.get_year_range()
|
|
1500
|
+
year_to_evaluated_value = {}
|
|
1501
|
+
for year in years:
|
|
1502
|
+
# NOTE: Baseline might have some processes that do not exist in the scenarios
|
|
1503
|
+
# These are mostly virtual processes.
|
|
1504
|
+
if not flow_solver.has_flow(affected_flow_id, year):
|
|
1505
|
+
continue
|
|
1506
|
+
|
|
1507
|
+
flow = flow_solver.get_flow(affected_flow_id, year)
|
|
1508
|
+
year_to_evaluated_value[year] = flow.evaluated_value
|
|
1509
|
+
|
|
1510
|
+
# Find any negative flows
|
|
1511
|
+
negative_flows = [[k, v] for k, v in year_to_evaluated_value.items() if v < 0.0]
|
|
1512
|
+
if not negative_flows:
|
|
1513
|
+
continue
|
|
1514
|
+
|
|
1515
|
+
# Find entry with the smallest value
|
|
1516
|
+
min_year_entry = min(negative_flows, key=lambda x: x[1])
|
|
1517
|
+
s = "Flow modifier in row {} targets opposite flows that do not have enough flows. ".format(
|
|
1518
|
+
flow_modifier.row_number)
|
|
1519
|
+
s += "This caused negative flow (evaluated value={}) for flow '{}' in year {}".format(
|
|
1520
|
+
min_year_entry[1], affected_flow_id, min_year_entry[0])
|
|
1521
|
+
errors.append(s)
|
|
1522
|
+
|
|
1523
|
+
# Using flow modifier solver with apply to targets = False can
|
|
1524
|
+
# make total relative outflows over 100%
|
|
1525
|
+
process_id_to_errors = {}
|
|
1526
|
+
total_rel_outflow_tolerance = 0.01
|
|
1527
|
+
for flow_modifier_index, flow_modifier in enumerate(flow_modifiers):
|
|
1528
|
+
source_process_id = flow_modifier.source_process_id
|
|
1529
|
+
for year in flow_modifier.get_year_range():
|
|
1530
|
+
outflows = flow_solver.get_process_outflows(source_process_id, year)
|
|
1531
|
+
total_outflows_rel = 0.0
|
|
1532
|
+
for flow in outflows:
|
|
1533
|
+
if not flow.is_unit_absolute_value:
|
|
1534
|
+
total_outflows_rel += flow.evaluated_share
|
|
1535
|
+
|
|
1536
|
+
if total_outflows_rel > (1.0 + total_rel_outflow_tolerance):
|
|
1537
|
+
s = "Flow modifier in row {} targeting flow {} causes the total relative outflows"
|
|
1538
|
+
s += " of source process '{}' to become over 100% in year {} (evaluated share = {:.3f}%)"
|
|
1539
|
+
s = s.format(flow_modifier.row_number,
|
|
1540
|
+
flow_modifier.target_flow_id,
|
|
1541
|
+
flow_modifier.source_process_id,
|
|
1542
|
+
year,
|
|
1543
|
+
(total_outflows_rel * 100.0)
|
|
1544
|
+
)
|
|
1545
|
+
errors.append(s)
|
|
1546
|
+
|
|
1547
|
+
return errors
|
|
1548
|
+
|
|
1549
|
+
def _check_flow_modifier_changes(self,
|
|
1550
|
+
flow_solver: FlowSolver,
|
|
1551
|
+
flow_modifier_indices: List[int],
|
|
1552
|
+
flow_modifiers: List[FlowModifier],
|
|
1553
|
+
flow_change_entries: Dict[int, List[FlowChangeEntry]]):
|
|
1554
|
+
"""
|
|
1555
|
+
Check if flow modifier changes can be done.
|
|
1556
|
+
Checks that:
|
|
1557
|
+
- target opposite flows do have enough share
|
|
1558
|
+
|
|
1559
|
+
:param flow_solver: Target FlowSolver
|
|
1560
|
+
:param flow_modifier_indices: List of flow modifier indices
|
|
1561
|
+
:param flow_modifiers: List of all FlowModifiers
|
|
1562
|
+
:param flow_change_entries: Flow modification changeset
|
|
1563
|
+
"""
|
|
1564
|
+
|
|
1565
|
+
# Get available flow share for every flow modifier source process
|
|
1566
|
+
source_process_id_to_year_to_flow_id_to_available_share = {}
|
|
1567
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1568
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1569
|
+
source_process_id = flow_modifier.source_process_id
|
|
1570
|
+
|
|
1571
|
+
if source_process_id not in source_process_id_to_year_to_flow_id_to_available_share:
|
|
1572
|
+
source_process_id_to_year_to_flow_id_to_available_share[source_process_id] = {}
|
|
1573
|
+
year_to_flow_id_to_available_share = source_process_id_to_year_to_flow_id_to_available_share[source_process_id]
|
|
1574
|
+
|
|
1575
|
+
if flow_modifier.has_opposite_targets:
|
|
1576
|
+
for year in flow_modifier.get_year_range():
|
|
1577
|
+
if year not in year_to_flow_id_to_available_share:
|
|
1578
|
+
year_to_flow_id_to_available_share[year] = {}
|
|
1579
|
+
flow_id_to_available_flow_share = year_to_flow_id_to_available_share[year]
|
|
1580
|
+
|
|
1581
|
+
opposite_flow_ids = flow_modifier.get_opposite_target_flow_ids()
|
|
1582
|
+
for flow_id in opposite_flow_ids:
|
|
1583
|
+
if flow_id in flow_id_to_available_flow_share:
|
|
1584
|
+
continue
|
|
1585
|
+
|
|
1586
|
+
flow = flow_solver.get_flow(flow_id, year)
|
|
1587
|
+
flow_id_to_available_flow_share[flow_id] = flow.evaluated_share
|
|
1588
|
+
|
|
1589
|
+
else:
|
|
1590
|
+
for year in flow_modifier.get_year_range():
|
|
1591
|
+
if year not in year_to_flow_id_to_available_share:
|
|
1592
|
+
year_to_flow_id_to_available_share[year] = {}
|
|
1593
|
+
flow_id_to_to_available_flow_share = year_to_flow_id_to_available_share[year]
|
|
1594
|
+
|
|
1595
|
+
sibling_flows = self._get_process_outflow_siblings(flow_modifier.source_process_id,
|
|
1596
|
+
flow_modifier.target_flow_id,
|
|
1597
|
+
year,
|
|
1598
|
+
only_same_type=True
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
for flow in sibling_flows:
|
|
1602
|
+
flow_id = flow.id
|
|
1603
|
+
if flow_id in flow_id_to_to_available_flow_share:
|
|
1604
|
+
continue
|
|
1605
|
+
|
|
1606
|
+
flow_id_to_to_available_flow_share[flow_id] = flow.evaluated_share
|
|
1607
|
+
|
|
1608
|
+
# Map source process ID to year to available flow share
|
|
1609
|
+
source_process_id_to_year_to_available_flow_share = {}
|
|
1610
|
+
source = source_process_id_to_year_to_flow_id_to_available_share
|
|
1611
|
+
target = source_process_id_to_year_to_available_flow_share
|
|
1612
|
+
for source_process_id, year_to_flow_id_to_available_share in source.items():
|
|
1613
|
+
if source_process_id not in target:
|
|
1614
|
+
target[source_process_id] = {}
|
|
1615
|
+
target_year_to_available_flow_share = target[source_process_id]
|
|
1616
|
+
|
|
1617
|
+
source_year_to_available_flow_share = source[source_process_id]
|
|
1618
|
+
for year, flow_id_to_available_share in source_year_to_available_flow_share.items():
|
|
1619
|
+
if year not in target_year_to_available_flow_share:
|
|
1620
|
+
target_year_to_available_flow_share[year] = 0.0
|
|
1621
|
+
|
|
1622
|
+
for flow_id, available_share in flow_id_to_available_share.items():
|
|
1623
|
+
target_year_to_available_flow_share[year] += available_share
|
|
1624
|
+
|
|
1625
|
+
# Reduce available flow shares from source_process_id_to_year_to_available_share by
|
|
1626
|
+
# applying all changeset entries
|
|
1627
|
+
source_process_id_to_year_to_required_flow_offset = {}
|
|
1628
|
+
for flow_modifier_index, changeset in flow_change_entries.items():
|
|
1629
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1630
|
+
source_process_id = flow_modifier.source_process_id
|
|
1631
|
+
|
|
1632
|
+
if source_process_id not in source_process_id_to_year_to_required_flow_offset:
|
|
1633
|
+
source_process_id_to_year_to_required_flow_offset[source_process_id] = {}
|
|
1634
|
+
year_to_required_flow_offset = source_process_id_to_year_to_required_flow_offset[source_process_id]
|
|
1635
|
+
|
|
1636
|
+
for entry in changeset:
|
|
1637
|
+
flow = flow_solver.get_flow(entry.flow_id, entry.year)
|
|
1638
|
+
flow_id = flow.id
|
|
1639
|
+
year = entry.year
|
|
1640
|
+
|
|
1641
|
+
# Process only opposite and target flows
|
|
1642
|
+
if flow_id == flow_modifier.target_flow_id:
|
|
1643
|
+
continue
|
|
1644
|
+
|
|
1645
|
+
if year not in year_to_required_flow_offset:
|
|
1646
|
+
year_to_required_flow_offset[year] = 0.0
|
|
1647
|
+
|
|
1648
|
+
year_to_required_flow_offset[year] += (entry.evaluated_share_offset / 100.0)
|
|
1649
|
+
|
|
1650
|
+
# Map source process ID to list of flow modifiers
|
|
1651
|
+
source_process_id_to_flow_modifier_indices = {}
|
|
1652
|
+
for flow_modifier_index in flow_modifier_indices:
|
|
1653
|
+
flow_modifier = flow_modifiers[flow_modifier_index]
|
|
1654
|
+
source_process_id = flow_modifier.source_process_id
|
|
1655
|
+
if source_process_id not in source_process_id_to_flow_modifier_indices:
|
|
1656
|
+
source_process_id_to_flow_modifier_indices[source_process_id] = []
|
|
1657
|
+
|
|
1658
|
+
list_of_flow_modifier_indices = source_process_id_to_flow_modifier_indices[source_process_id]
|
|
1659
|
+
list_of_flow_modifier_indices.append(flow_modifier_index)
|
|
1660
|
+
|
|
1661
|
+
# These map always have same process IDs and years
|
|
1662
|
+
source_process_id_to_error_entries = {}
|
|
1663
|
+
for source_process_id in source_process_id_to_year_to_available_flow_share:
|
|
1664
|
+
year_to_available_flow_share = source_process_id_to_year_to_available_flow_share[source_process_id]
|
|
1665
|
+
year_to_required_flow_offset = source_process_id_to_year_to_required_flow_offset[source_process_id]
|
|
1666
|
+
|
|
1667
|
+
for year in year_to_available_flow_share.keys():
|
|
1668
|
+
available_flow_share = year_to_available_flow_share[year]
|
|
1669
|
+
required_flow_offset = year_to_required_flow_offset[year]
|
|
1670
|
+
|
|
1671
|
+
if required_flow_offset > 0.0:
|
|
1672
|
+
continue
|
|
1673
|
+
|
|
1674
|
+
if available_flow_share < abs(required_flow_offset):
|
|
1675
|
+
error_entry = [source_process_id, year, required_flow_offset, available_flow_share]
|
|
1676
|
+
if source_process_id not in source_process_id_to_error_entries:
|
|
1677
|
+
source_process_id_to_error_entries[source_process_id] = []
|
|
1678
|
+
error_entries = source_process_id_to_error_entries[source_process_id]
|
|
1679
|
+
error_entries.append(error_entry)
|
|
1680
|
+
|
|
1681
|
+
# Check all error entries for source processes
|
|
1682
|
+
for source_process_id, error_entries in source_process_id_to_error_entries.items():
|
|
1683
|
+
local_flow_modifier_indices = source_process_id_to_flow_modifier_indices[source_process_id]
|
|
1684
|
+
|
|
1685
|
+
# Find entry with most negative required flow offset, this is the largest value that is needed
|
|
1686
|
+
# to fulfill the flow modifier rule
|
|
1687
|
+
min_entry = min(error_entries, key=lambda x: x[2])
|
|
1688
|
+
min_source_process_id = min_entry[0]
|
|
1689
|
+
min_entry_year = min_entry[1]
|
|
1690
|
+
min_entry_required_flow_offset = min_entry[2]
|
|
1691
|
+
min_entry_available_flow_share = min_entry[3]
|
|
1692
|
+
|
|
1693
|
+
# This is the maximum flow share that is available at the year when the flow share
|
|
1694
|
+
# became the most negative, use this value to calculate what is the maximum change in value
|
|
1695
|
+
# for every related flow modifier
|
|
1696
|
+
maximum_flow_share = min_entry_available_flow_share
|
|
1697
|
+
fmi_to_share_diff = {}
|
|
1698
|
+
fmi_to_start_value = {}
|
|
1699
|
+
for fmi in local_flow_modifier_indices:
|
|
1700
|
+
fm = flow_modifiers[fmi]
|
|
1701
|
+
start_year_flow = flow_solver.get_flow(fm.target_flow_id, fm.start_year)
|
|
1702
|
+
v0 = start_year_flow.evaluated_share
|
|
1703
|
+
v1 = start_year_flow.evaluated_share * (1.0 + (fm.change_in_value / 100.0))
|
|
1704
|
+
share_diff = v1 - v0
|
|
1705
|
+
fmi_to_share_diff[fmi] = share_diff
|
|
1706
|
+
fmi_to_start_value[fmi] = v0
|
|
1707
|
+
|
|
1708
|
+
# Calculate what is the maximum change of value for each flow modifier that affects this flow
|
|
1709
|
+
# This is proportional value how much each flow modifier needs the flow:
|
|
1710
|
+
# - bigger growth in flow share will introduce larger factor from the maximum available flow share
|
|
1711
|
+
fmi_to_share = {}
|
|
1712
|
+
fmi_to_max_change_in_value = {}
|
|
1713
|
+
total_share_diff = sum([val for val in fmi_to_share_diff.values()])
|
|
1714
|
+
for fmi in local_flow_modifier_indices:
|
|
1715
|
+
fmi_to_share[fmi] = fmi_to_share_diff[fmi] / total_share_diff
|
|
1716
|
+
start_value = fmi_to_start_value[fmi]
|
|
1717
|
+
end_value = start_value + (maximum_flow_share * fmi_to_share[fmi])
|
|
1718
|
+
fmi_to_max_change_in_value[fmi] = ((end_value / start_value) - 1.0) * 100.0
|
|
1719
|
+
|
|
1720
|
+
print("Adjust the following flow modifiers that affect the source process {}".format(source_process_id))
|
|
1721
|
+
for fmi in local_flow_modifier_indices:
|
|
1722
|
+
flow_modifier = flow_modifiers[fmi]
|
|
1723
|
+
max_change_in_value = fmi_to_max_change_in_value[fmi] - 0.001
|
|
1724
|
+
print("- Maximum change in value for flow modifier in row {} is {:.3f} %".format(
|
|
1725
|
+
flow_modifier.row_number, max_change_in_value))
|
|
1726
|
+
|
|
1727
|
+
def _recalculate_relative_flow_evaluated_shares(self, flow_solver: FlowSolver):
|
|
1728
|
+
"""
|
|
1729
|
+
Recalculates flow shares after applying flow modifiers.
|
|
1730
|
+
|
|
1731
|
+
:param flow_solver: Target FlowSolver
|
|
1732
|
+
:param flow_modifiers: List of FlowModifers
|
|
1733
|
+
:return: None
|
|
1734
|
+
"""
|
|
1735
|
+
# Recalculate all relative flow shares
|
|
1736
|
+
year_to_process_to_flows = flow_solver.get_year_to_process_to_flows()
|
|
1737
|
+
for year, process_to_flows in year_to_process_to_flows.items():
|
|
1738
|
+
for process, flows in process_to_flows.items():
|
|
1739
|
+
process_id = process.id
|
|
1740
|
+
if process.is_virtual:
|
|
1741
|
+
continue
|
|
1742
|
+
|
|
1743
|
+
if process.stock_lifetime > 0:
|
|
1744
|
+
continue
|
|
1745
|
+
|
|
1746
|
+
outflows = flow_solver.get_process_outflows(process_id, year)
|
|
1747
|
+
total_outflows_rel = flow_solver.get_process_outflows_total_rel(process_id, year)
|
|
1748
|
+
outflows_rel = [f for f in outflows if not f.is_unit_absolute_value and not f.is_virtual]
|
|
1749
|
+
|
|
1750
|
+
if total_outflows_rel <= 0.0:
|
|
1751
|
+
continue
|
|
1752
|
+
|
|
1753
|
+
for flow in outflows_rel:
|
|
1754
|
+
flow.evaluated_share = flow.evaluated_value / total_outflows_rel
|