aiphoria 0.0.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. aiphoria/__init__.py +59 -0
  2. aiphoria/core/__init__.py +55 -0
  3. aiphoria/core/builder.py +305 -0
  4. aiphoria/core/datachecker.py +1808 -0
  5. aiphoria/core/dataprovider.py +806 -0
  6. aiphoria/core/datastructures.py +1686 -0
  7. aiphoria/core/datavisualizer.py +431 -0
  8. aiphoria/core/datavisualizer_data/LICENSE +21 -0
  9. aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
  10. aiphoria/core/datavisualizer_data/pako.min.js +2 -0
  11. aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
  12. aiphoria/core/flowmodifiersolver.py +1754 -0
  13. aiphoria/core/flowsolver.py +1472 -0
  14. aiphoria/core/logger.py +113 -0
  15. aiphoria/core/network_graph.py +136 -0
  16. aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
  17. aiphoria/core/network_graph_data/echarts_min.js +45 -0
  18. aiphoria/core/network_graph_data/network_graph.html +76 -0
  19. aiphoria/core/network_graph_data/network_graph.js +1391 -0
  20. aiphoria/core/parameters.py +269 -0
  21. aiphoria/core/types.py +20 -0
  22. aiphoria/core/utils.py +362 -0
  23. aiphoria/core/visualizer_parameters.py +7 -0
  24. aiphoria/data/example_scenario.xlsx +0 -0
  25. aiphoria/example.py +66 -0
  26. aiphoria/lib/docs/dynamic_stock.py +124 -0
  27. aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
  28. aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
  29. aiphoria/lib/odym/modules/__init__.py +1 -0
  30. aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
  31. aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
  32. aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
  33. aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
  34. aiphoria/lib/odym/modules/test/__init__.py +2 -0
  35. aiphoria/runner.py +678 -0
  36. aiphoria-0.8.0.dist-info/METADATA +119 -0
  37. aiphoria-0.8.0.dist-info/RECORD +40 -0
  38. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
  39. aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
  40. aiphoria-0.0.1.dist-info/METADATA +0 -5
  41. aiphoria-0.0.1.dist-info/RECORD +0 -5
  42. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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