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,1686 @@
1
+ from typing import Tuple, List, Union, Dict, Any
2
+ from builtins import float
3
+ import copy
4
+ import pandas as pd
5
+ from aiphoria.lib.odym.modules.ODYM_Classes import MFAsystem
6
+ from .parameters import StockDistributionParameterValueType
7
+ from .types import FunctionType, ChangeType
8
+
9
+
10
+ class ObjectBase(object):
11
+ """
12
+ Base class for Process, Flow and Stock.
13
+ Keeps track of row number, validity of read row and virtual state.
14
+ """
15
+ def __init__(self):
16
+ self._id: Union[str, any] = -1
17
+ self._row_number: int = -1
18
+ self._is_valid: bool = False
19
+ self._is_virtual: bool = False
20
+
21
+ @property
22
+ def is_valid(self) -> bool:
23
+ """
24
+ Return validity of object.
25
+
26
+ :return: Boolean
27
+ """
28
+ return False
29
+
30
+ @property
31
+ def id(self) -> Union[str, any]:
32
+ """
33
+ Get ID of the object.
34
+
35
+ :return: Object ID (string)
36
+ """
37
+ return self._id
38
+
39
+ @id.setter
40
+ def id(self, new_id: str) -> None:
41
+ """
42
+ Set Object ID.
43
+
44
+ :param new_id: New ID
45
+ :return: None
46
+ """
47
+ self._id = new_id
48
+
49
+ @property
50
+ def row_number(self) -> int:
51
+ """
52
+ Get row number where Object was defined.
53
+ This is valid only for the Flows and Processes defined in settings file.
54
+
55
+ :return: Row number where Object was defined
56
+ """
57
+ return self._row_number
58
+
59
+ @row_number.setter
60
+ def row_number(self, value: int) -> None:
61
+ """
62
+ Set row number where Object was defined.
63
+
64
+ :param value: Target row number
65
+ :return: None
66
+ """
67
+ self._row_number = value
68
+
69
+ @property
70
+ def is_virtual(self) -> bool:
71
+ """
72
+ Check if Object is virtual.
73
+
74
+ :return: True if Object is virtual, False otherwise.
75
+ """
76
+ return self._is_virtual
77
+
78
+ @is_virtual.setter
79
+ def is_virtual(self, value: bool) -> None:
80
+ """
81
+ Set Object virtual state.
82
+
83
+ :param value: State
84
+ :return: None
85
+ """
86
+ self._is_virtual = value
87
+
88
+
89
+ class Indicator(object):
90
+ def __init__(self, name: str = None, conversion_factor: float = 1.0, comment: str = None, unit: str = None):
91
+ super().__init__()
92
+ self._name: Union[str, None] = name
93
+ self._conversion_factor: Union[float, None] = conversion_factor
94
+ self._comment: Union[str, None] = comment
95
+ self._unit: Union[str, None] = unit
96
+
97
+ @property
98
+ def name(self) -> str:
99
+ """
100
+ Get the indicator name.
101
+
102
+ :return: Indicator name (str)
103
+ """
104
+ return self._name
105
+
106
+ @name.setter
107
+ def name(self, new_name: str):
108
+ """
109
+ Set the indicator name.
110
+
111
+ :param new_name: New indicator name (str)
112
+ """
113
+ self._name = new_name
114
+
115
+ @property
116
+ def conversion_factor(self) -> float:
117
+ """
118
+ Get the indicator conversion factor.
119
+
120
+ :return: Conversion factor (float)
121
+ """
122
+ return self._conversion_factor
123
+
124
+ @conversion_factor.setter
125
+ def conversion_factor(self, new_conversion_factor: float):
126
+ """
127
+ Set the indicator conversion factor.
128
+
129
+ :param new_value: New conversion factor (float)
130
+ """
131
+ self._conversion_factor = new_conversion_factor
132
+
133
+ @property
134
+ def comment(self) -> str:
135
+ """
136
+ Get indicator comment.
137
+
138
+ :return: Indicator comment (str)
139
+ """
140
+ return self._comment
141
+
142
+ @comment.setter
143
+ def comment(self, new_comment: str):
144
+ """
145
+ Set the indicator comment.
146
+
147
+ :param new_comment: New indicator comment (str)
148
+ """
149
+ self._comment = new_comment
150
+
151
+ @property
152
+ def unit(self) -> str:
153
+ """
154
+ Get the indicator unit.
155
+
156
+ :return: Indicator unit (str)
157
+ """
158
+ return self._unit
159
+
160
+ @unit.setter
161
+ def unit(self, new_unit: str):
162
+ """
163
+ Set the indicator unit.
164
+
165
+ :param new_unit: New Indicator unit (str)
166
+ """
167
+ self._unit = new_unit
168
+
169
+
170
+ class Process(ObjectBase):
171
+ """
172
+ aiphoria Process-object:
173
+
174
+ Used to store data for Process.
175
+ """
176
+ def __init__(self, params: pd.Series = None, row_number=-1):
177
+ super().__init__()
178
+
179
+ self._name = None
180
+ self._location = None
181
+ self._id = None
182
+ self._transformation_stage = None
183
+ self._stock_lifetime = None
184
+ self._stock_lifetime_source = None
185
+ self._stock_distribution_type = None
186
+ self._stock_distribution_params = None
187
+ self._wood_content = None
188
+ self._wood_content_source = None
189
+ self._density = None
190
+ self._density_source = None
191
+ self._modelling_status = None
192
+ self._comment = None
193
+ self._row_number = -1
194
+ self._depth = -1
195
+ self._position_x = None
196
+ self._position_y = None
197
+ self._label_in_graph = None
198
+
199
+ # Leave instance to default state if params is not provided
200
+ if params is None:
201
+ return
202
+
203
+ # Skip totally empty row
204
+ if params.isna().all():
205
+ return
206
+
207
+ self._name = params.iloc[0]
208
+ self._location = params.iloc[1]
209
+ self._id = params.iloc[2]
210
+ self._transformation_stage = params.iloc[3]
211
+
212
+ # Parse stock lifetime, default to zero if None
213
+ self._stock_lifetime = self._parse_stock_lifetime(params.iloc[4], row_number)
214
+
215
+ self._stock_lifetime_source = params.iloc[5]
216
+ self._stock_distribution_type = params.iloc[6]
217
+ self._stock_distribution_params = params.iloc[7]
218
+
219
+ # Parse stock distribution parameters
220
+ # NOTE: Event invalid key-value -pairs are stored to _stock_distribution_params after parsin
221
+ # and those are checked in datachecker
222
+ self._parse_and_set_distribution_params(params.iloc[7])
223
+
224
+ self._wood_content = params.iloc[8]
225
+ self._wood_content_source = params.iloc[9]
226
+ self._density = params.iloc[10]
227
+ self._density_source = params.iloc[11]
228
+ self._modelling_status = params.iloc[12]
229
+ self._comment = params.iloc[13]
230
+ self._position_x = params.iloc[14]
231
+ self._position_y = params.iloc[15]
232
+ self._label_in_graph = params.iloc[16]
233
+ self._row_number = row_number
234
+ self._meta = {}
235
+
236
+ def __str__(self) -> str:
237
+ s = "Process '{}': Lifetime: {}".format(self.id, self.stock_lifetime)
238
+ return s
239
+
240
+ def __hash__(self):
241
+ return hash(self.id)
242
+
243
+ def __eq__(self, other) -> bool:
244
+ if not isinstance(other, Process):
245
+ return NotImplemented
246
+
247
+ return self.id == other.id
248
+
249
+ def is_valid(self) -> bool:
250
+ is_valid = True
251
+ is_valid = is_valid and self.name is not None
252
+ is_valid = is_valid and self.location is not None
253
+ is_valid = is_valid and self.id is not None
254
+ return is_valid
255
+
256
+ @property
257
+ def name(self) -> str:
258
+ return self._name
259
+
260
+ @name.setter
261
+ def name(self, value: str) -> None:
262
+ self._name = value
263
+
264
+ @name.setter
265
+ def name(self, value) -> None:
266
+ self._name = value
267
+
268
+ @property
269
+ def location(self) -> str:
270
+ return self._location
271
+
272
+ @location.setter
273
+ def location(self, value: str):
274
+ self._location = value
275
+
276
+ @property
277
+ def transformation_stage(self) -> str:
278
+ return self._transformation_stage
279
+
280
+ @transformation_stage.setter
281
+ def transformation_stage(self, value: str):
282
+ self._transformation_stage = value
283
+
284
+ @property
285
+ def stock_lifetime(self) -> int:
286
+ return self._stock_lifetime
287
+
288
+ @stock_lifetime.setter
289
+ def stock_lifetime(self, value: int) -> None:
290
+ self._stock_lifetime = value
291
+
292
+ @property
293
+ def stock_lifetime_source(self) -> str:
294
+ return self._stock_lifetime_source
295
+
296
+ @stock_lifetime_source.setter
297
+ def stock_lifetime_source(self, value: str):
298
+ self._stock_lifetime_source = value
299
+
300
+ @property
301
+ def stock_distribution_type(self) -> str:
302
+ return self._stock_distribution_type
303
+
304
+ @stock_distribution_type.setter
305
+ def stock_distribution_type(self, value: str):
306
+ self._stock_distribution_type = value
307
+
308
+ @property
309
+ def stock_distribution_params(self) -> str:
310
+ return self._stock_distribution_params
311
+
312
+ @stock_distribution_params.setter
313
+ def stock_distribution_params(self, value: str):
314
+ self._stock_distribution_params = value
315
+
316
+ @property
317
+ def wood_content(self) -> float:
318
+ return self._wood_content
319
+
320
+ @wood_content.setter
321
+ def wood_content(self, value: float):
322
+ self._wood_content = value
323
+
324
+ @property
325
+ def wood_content_source(self) -> str:
326
+ return self._wood_content_source
327
+
328
+ @wood_content_source.setter
329
+ def wood_content_source(self, value: str):
330
+ self._wood_content_source = value
331
+
332
+ @property
333
+ def density(self) -> float:
334
+ return self._density
335
+
336
+ @density.setter
337
+ def density(self, value: float):
338
+ self._density = value
339
+
340
+ @property
341
+ def density_source(self) -> str:
342
+ return self._density_source
343
+
344
+ @density_source.setter
345
+ def density_source(self, value: str):
346
+ self._density_source = value
347
+
348
+ @property
349
+ def modelling_status(self) -> str:
350
+ return self._modelling_status
351
+
352
+ @modelling_status.setter
353
+ def modelling_status(self, value: str):
354
+ self._modelling_status = value
355
+
356
+ @property
357
+ def comment(self) -> str:
358
+ return self._comment
359
+
360
+ @comment.setter
361
+ def comment(self, value: str):
362
+ self._comment = value
363
+
364
+ @property
365
+ def depth(self) -> int:
366
+ return self._depth
367
+
368
+ @depth.setter
369
+ def depth(self, value: int):
370
+ self._depth = value
371
+
372
+ @property
373
+ def position_x(self) -> float:
374
+ return self._position_x
375
+
376
+ @position_x.setter
377
+ def position_x(self, value: float):
378
+ self._position_x = value
379
+
380
+ @property
381
+ def position_y(self) -> float:
382
+ return self._position_y
383
+
384
+ @position_y.setter
385
+ def position_y(self, value: float):
386
+ self._position_y = value
387
+
388
+ @property
389
+ def label_in_graph(self) -> str:
390
+ return self._label_in_graph
391
+
392
+ @label_in_graph.setter
393
+ def label_in_graph(self, value: str):
394
+ self._label_in_graph = value
395
+
396
+ @property
397
+ def meta(self) -> Dict[Any, Any]:
398
+ return self._meta
399
+
400
+ @meta.setter
401
+ def meta(self, value: Dict[Any, Any]) -> None:
402
+ self._meta = value
403
+
404
+ def _parse_stock_lifetime(self, s: str, row_number: int = -1):
405
+ """
406
+ Parse stock lifetime from string.
407
+
408
+ :param s: String
409
+ :return: Stock lifetime (int)
410
+ """
411
+ lifetime = 0
412
+ if s is None:
413
+ return lifetime
414
+
415
+ try:
416
+ lifetime = int(s)
417
+ except (ValueError, TypeError) as ex:
418
+ raise Exception("Stock lifetime must be number (row {})".format(row_number))
419
+
420
+ return lifetime
421
+
422
+ def _parse_and_set_distribution_params(self, s: str):
423
+ """
424
+ Parse keys from string for distribution parameters.
425
+ """
426
+
427
+ try:
428
+ # Check if cell contains only value
429
+ self._stock_distribution_params = float(s)
430
+ return
431
+ except (ValueError, TypeError):
432
+ if s is None:
433
+ return
434
+
435
+ params = {}
436
+ has_multiple_params = s.find(',') > 0
437
+ if not has_multiple_params:
438
+ # Single parameter
439
+ entry = s
440
+ k = None
441
+ v = None
442
+ if entry.find("=") >= 0:
443
+ # Has key=value
444
+ k, v = entry.split("=")
445
+ k = k.strip()
446
+ v = v.strip()
447
+ else:
448
+ # Only key, no value
449
+ k = entry.strip()
450
+ v = None
451
+
452
+ # Convert to target value type if definition exists
453
+ value_type = StockDistributionParameterValueType[k]
454
+ if value_type is not None:
455
+ v = value_type(v)
456
+
457
+ params[k] = v
458
+
459
+ else:
460
+ # Multiple parameters, separated by ','
461
+ for entry in s.split(","):
462
+ k = None
463
+ v = None
464
+ if entry.find("=") >= 0:
465
+ k, v = entry.split("=")
466
+ k = k.strip()
467
+ v = v.strip()
468
+ else:
469
+ k = entry.strip()
470
+ v = None
471
+
472
+ # Convert to target value type if definition exists
473
+ value_type = StockDistributionParameterValueType[k]
474
+ if value_type is not None:
475
+ v = value_type(v)
476
+
477
+ params[k] = v
478
+
479
+ self._stock_distribution_params = params
480
+
481
+
482
+ class Flow(ObjectBase):
483
+ """
484
+ aiphoria Flow-object:
485
+
486
+ Used to store data for Flow.
487
+ """
488
+ def __init__(self, params: pd.Series = None, row_number=-1):
489
+ super().__init__()
490
+
491
+ self._source_process = None
492
+ self._source_process_transformation_stage = None
493
+ self._source_process_location = None
494
+ self._target_process = None
495
+ self._target_process_transformation_stage = None
496
+ self._target_process_location = None
497
+ self._source_process_id = None
498
+ self._target_process_id = None
499
+ self._value = None
500
+ self._unit = None
501
+ self._year = None
502
+ self._data_source = None
503
+ self._data_source_comment = None
504
+ self._comment = None
505
+
506
+ # Evaluated per timestep
507
+ self._is_evaluated = False
508
+ self._evaluated_share = 0.0
509
+ self._evaluated_value = 0.0
510
+
511
+ # Indicator name to Indicator
512
+ self._indicator_name_to_indicator = {}
513
+ self._indicator_name_to_evaluated_value = {}
514
+
515
+ # Flow prioritization
516
+ self._is_prioritized = False
517
+
518
+ if params is None:
519
+ return
520
+
521
+ # Skip totally empty row
522
+ if params.isna().all():
523
+ return
524
+
525
+ self._source_process = params.iloc[0]
526
+ self._source_process_transformation_stage = params.iloc[1]
527
+ self._source_process_location = params.iloc[2]
528
+ self._target_process = params.iloc[3]
529
+ self._target_process_transformation_stage = params.iloc[4]
530
+ self._target_process_location = params.iloc[5]
531
+ self._source_process_id = params.iloc[6]
532
+ self._target_process_id = params.iloc[7]
533
+ self._value = params.iloc[8]
534
+ self._unit = params.iloc[9]
535
+ self._year = int(params.iloc[10])
536
+ self._data_source = params.iloc[11]
537
+ self._data_source_comment = params.iloc[12]
538
+
539
+ # Rest of the elements except last element are indicators
540
+ # There should be even number of indicators because each indicator has value and comment
541
+ first_indicator_index = 13
542
+ indicators = params[first_indicator_index:]
543
+ if len(indicators) % 2:
544
+ s = "Not even number of indicator columns in settings file.\n"
545
+ s += "Each indicator needs two columns (value and comment) in this order."
546
+ raise Exception(s)
547
+
548
+ # Build indicator name to Indicator mappings
549
+ for i in range(0, len(indicators), 2):
550
+ indicator_name = indicators.index[i]
551
+ conversion_factor = indicators.iloc[i]
552
+ comment = indicators.iloc[i+1]
553
+
554
+ # Strip substring inside characters '(' and ')'
555
+ # and use that as a unit
556
+ indicator_unit = ""
557
+ start_index = indicator_name.find("(")
558
+ end_index = indicator_name.find(")")
559
+ if start_index >= 0 and end_index >= 0:
560
+ unit_name = indicator_name[start_index:end_index + 1]
561
+ indicator_name = indicator_name.replace(unit_name, '').strip()
562
+ indicator_unit = unit_name[1:-1].strip()
563
+
564
+ # NOTE: Set indicator conversion factor to 0.0
565
+ # if not defined in the settings file
566
+ if conversion_factor is None:
567
+ conversion_factor = 0.0
568
+
569
+ new_indicator = Indicator(indicator_name, conversion_factor, comment, indicator_unit)
570
+ self._indicator_name_to_indicator[indicator_name] = new_indicator
571
+ self._indicator_name_to_evaluated_value[indicator_name] = 0.0
572
+
573
+ self._row_number = row_number # Track Excel file row number
574
+
575
+ def __str__(self):
576
+ s = "Flow '{}' -> '{}': Value={}, Unit={}, " \
577
+ "is_evaluated={}, evaluated_share={}, evaluated_value={}, " \
578
+ "year={}, is_virtual={}".format(
579
+ self.source_process_id, self.target_process_id, self.value, self.unit,
580
+ self.is_evaluated, self.evaluated_share, self.evaluated_value, self.year,
581
+ self.is_virtual)
582
+ return s
583
+
584
+ def __hash__(self):
585
+ return hash(self.id)
586
+
587
+ def __eq__(self, other):
588
+ if not isinstance(other, Flow):
589
+ return NotImplemented
590
+
591
+ return self.id == other.id
592
+
593
+ @staticmethod
594
+ def make_flow_id(source_process_id: str, target_process_id: str) -> str:
595
+ """
596
+ Make Flow ID from source Process ID and target Process ID.
597
+
598
+ :param source_process_id: Source Process ID (string)
599
+ :param target_process_id: Target Process ID (string)
600
+ :return: Flow ID (string)
601
+ """
602
+ return "{} {}".format(source_process_id, target_process_id)
603
+
604
+ @property
605
+ def id(self) -> str:
606
+ """
607
+ Returns Flow ID.
608
+
609
+ :return: Flow ID (string)
610
+ """
611
+ return Flow.make_flow_id(self.source_process_id, self.target_process_id)
612
+
613
+ def is_valid(self):
614
+ is_valid = True
615
+ is_valid = is_valid and self.value is not None
616
+ is_valid = is_valid and (self.source_process is not None)
617
+ is_valid = is_valid and (self.target_process is not None)
618
+ is_valid = is_valid and (self.source_process_id is not None)
619
+ is_valid = is_valid and (self.target_process_id is not None)
620
+ return is_valid
621
+
622
+ @property
623
+ def is_unit_absolute_value(self):
624
+ # Default to absolute value if unit is missing
625
+ unit_str = self.unit
626
+ if unit_str is None:
627
+ return True
628
+
629
+ unit_str = unit_str.strip()
630
+ if unit_str == "%":
631
+ return False
632
+
633
+ return True
634
+
635
+ @property
636
+ def source_process(self) -> str:
637
+ """
638
+ Get source Process name.
639
+
640
+ :return: Source Process name (str)
641
+ """
642
+ return self._source_process
643
+
644
+ @property
645
+ def source_process_transformation_stage(self) -> str:
646
+ return self._source_process_transformation_stage
647
+
648
+ @property
649
+ def source_process_location(self) -> str:
650
+ return self._source_process_location
651
+
652
+ @property
653
+ def target_process(self) -> str:
654
+ """
655
+ Get target Process name.
656
+ :return: Target Process name (str)
657
+ """
658
+ return self._target_process
659
+
660
+ @property
661
+ def target_process_transformation_stage(self) -> str:
662
+ return self._target_process_transformation_stage
663
+
664
+ @property
665
+ def target_process_location(self) -> str:
666
+ return self._target_process_location
667
+
668
+ @property
669
+ def source_process_id(self) -> str:
670
+ """
671
+ Get source Process ID.
672
+ :return: Source Process ID (str)
673
+ """
674
+ return self._source_process_id
675
+
676
+ @source_process_id.setter
677
+ def source_process_id(self, source_process_id: str):
678
+ """
679
+ Set source Process ID.
680
+
681
+ :param source_process_id: Source Process ID (str)
682
+ """
683
+ self._source_process_id = source_process_id
684
+
685
+ @property
686
+ def target_process_id(self) -> str:
687
+ """
688
+ Get target Process ID.
689
+
690
+ :return: Target Process ID (str)
691
+ """
692
+ return self._target_process_id
693
+
694
+ @target_process_id.setter
695
+ def target_process_id(self, target_process_id: str):
696
+ """
697
+ Set target Process ID.
698
+
699
+ :param target_process_id: New target Process ID
700
+ """
701
+ self._target_process_id = target_process_id
702
+
703
+ # Original value from Excel row
704
+ @property
705
+ def value(self) -> float:
706
+ """
707
+ Get original baseline value.
708
+
709
+ :return: Original baseline value (float)
710
+ """
711
+ return self._value
712
+
713
+ @value.setter
714
+ def value(self, value: float):
715
+ """
716
+ Set original base value.
717
+
718
+ :param value: New original base value (float)
719
+ """
720
+ self._value = value
721
+
722
+ @property
723
+ def unit(self) -> str:
724
+ return self._unit
725
+
726
+ @unit.setter
727
+ def unit(self, unit: str):
728
+ self._unit = unit
729
+
730
+ @property
731
+ def year(self) -> int:
732
+ return self._year
733
+
734
+ @year.setter
735
+ def year(self, value: int):
736
+ self._year = value
737
+
738
+ @property
739
+ def data_source(self) -> str:
740
+ return self._data_source
741
+
742
+ @property
743
+ def data_source_comment(self) -> str:
744
+ return self._data_source_comment
745
+
746
+ @property
747
+ def comment(self) -> str:
748
+ return self._comment
749
+
750
+ @property
751
+ def is_evaluated(self) -> bool:
752
+ """
753
+ Get flow evaluated state.
754
+
755
+ :return: True if Flow is evaluated, False otherwise.
756
+ """
757
+ return self._is_evaluated
758
+
759
+ @is_evaluated.setter
760
+ def is_evaluated(self, value: bool):
761
+ """
762
+ Set flow evaluated state.
763
+
764
+ :param value: New evaluated state (bool)
765
+ """
766
+ self._is_evaluated = value
767
+
768
+ @property
769
+ def evaluated_value(self) -> float:
770
+ """
771
+ Get evaluated base value.
772
+
773
+ :return: Evaluated base value (float)
774
+ """
775
+ return self._evaluated_value
776
+
777
+ @evaluated_value.setter
778
+ def evaluated_value(self, value: float):
779
+ """
780
+ Set evaluated base value.
781
+
782
+ :param value: New evaluated base value (float)
783
+ """
784
+ self._evaluated_value = value
785
+
786
+ @property
787
+ def evaluated_share(self) -> float:
788
+ return self._evaluated_share
789
+
790
+ @evaluated_share.setter
791
+ def evaluated_share(self, value: float):
792
+ self._evaluated_share = value
793
+
794
+ @property
795
+ def is_prioritized(self) -> bool:
796
+ return self._is_prioritized
797
+
798
+ @is_prioritized.setter
799
+ def is_prioritized(self, is_prioritized: bool):
800
+ self._is_prioritized = is_prioritized
801
+
802
+ @property
803
+ def indicator_name_to_indicator(self) -> Dict[str, Indicator]:
804
+ return self._indicator_name_to_indicator
805
+
806
+ @property
807
+ def indicator_name_to_evaluated_value(self) -> Dict[str, float]:
808
+ return self._indicator_name_to_evaluated_value
809
+
810
+ def get_indicator_names(self) -> List[str]:
811
+ """
812
+ Get list of Indicator names (including baseline indicator name).
813
+
814
+ :return: List of Indicator names
815
+ """
816
+ return list(self._indicator_name_to_indicator.keys())
817
+
818
+ def get_indicator_units(self) -> List[str]:
819
+ """
820
+ Get list of Indicator unit names (including baseline indicator unit).
821
+
822
+ :return: List of Indicator unit names
823
+ """
824
+ indicator_units = []
825
+ for indicator_name, indicator in self._indicator_name_to_indicator.items():
826
+ indicator_units.append(indicator.unit)
827
+
828
+ return indicator_units
829
+
830
+ def get_indicator_conversion_factor(self, indicator_name: str) -> float:
831
+ """
832
+
833
+ :param indicator_name:
834
+ :return:
835
+ """
836
+ return self._indicator_name_to_indicator[indicator_name].conversion_factor
837
+
838
+ def get_evaluated_value_for_indicator(self, indicator_name: str) -> float:
839
+ """
840
+ Get evaluated value for Indicator.
841
+
842
+ :param indicator_name: Target Indicator name (str)
843
+ :return: Evaluated value for Indicator (float)
844
+ """
845
+ return self._indicator_name_to_evaluated_value[indicator_name]
846
+
847
+ def set_evaluated_value_for_indicator(self, indicator_name: str, value: float):
848
+ """
849
+ Set evaluated value for Indicator.
850
+
851
+ :param indicator_name: Target Indicator name (str)
852
+ :param value: New evaluated value for Indicator (float)
853
+ """
854
+ self._indicator_name_to_evaluated_value[indicator_name] = value
855
+
856
+ def evaluate_indicator_values_from_baseline_value(self):
857
+ """
858
+ Evaluated indicator evaluated value from baseline value.
859
+ """
860
+ for indicator_name, indicator in self._indicator_name_to_indicator.items():
861
+ evaluated_value = self.evaluated_value * indicator.conversion_factor
862
+ self.set_evaluated_value_for_indicator(indicator_name, evaluated_value)
863
+
864
+ def get_all_evaluated_values(self) -> List[float]:
865
+ """
866
+ Get list of all evaluated values.
867
+ First index is always the evaluated baseline value.
868
+ Other indices are evaluated indicator values
869
+
870
+ :return: List of evaluated values (list of float)
871
+ """
872
+
873
+ return [self.evaluated_value] + [value for name, value in self.indicator_name_to_evaluated_value.items()]
874
+
875
+
876
+ # Stock is created for each process that has lifetime
877
+ class Stock(ObjectBase):
878
+ """
879
+ aiphoria Stock-object:
880
+
881
+ Used to store data for Stock.
882
+ """
883
+ def __init__(self, params: Process = None, row_number=-1):
884
+ super().__init__()
885
+ self._process = None
886
+ self._id = -1
887
+
888
+ if params is None:
889
+ return
890
+
891
+ self._process = params
892
+ self._id = params.id
893
+ self._row_number = row_number
894
+
895
+ def __str__(self):
896
+ if not self.is_valid():
897
+ return "Stock: no process"
898
+
899
+ s = "Stock: Process='{}', lifetime={}".format(self.id, self.stock_lifetime)
900
+ return s
901
+
902
+ def is_valid(self):
903
+ if not self._process:
904
+ return False
905
+
906
+ return True
907
+
908
+ def __hash__(self):
909
+ return hash(self._process.id)
910
+
911
+ def __eq__(self, other):
912
+ return self.id == other.id
913
+
914
+ @property
915
+ def name(self):
916
+ return self._process.name
917
+
918
+ @property
919
+ def stock_lifetime(self):
920
+ return self._process.stock_lifetime
921
+
922
+ @property
923
+ def stock_distribution_type(self):
924
+ return self._process.stock_distribution_type
925
+
926
+ @property
927
+ def stock_distribution_params(self):
928
+ return self._process.stock_distribution_params
929
+
930
+
931
+ class FlowModifier(ObjectBase):
932
+ """
933
+ aiphoria FlowModifier-object:
934
+
935
+ Used to store data for scenario related flow modifications.
936
+ """
937
+ def __init__(self, params: pd.Series = None):
938
+ super().__init__()
939
+
940
+ self._scenario_name: str = ""
941
+ self._source_process_id: str = ""
942
+ self._target_process_id: str = ""
943
+ self._change_in_value: Union[float, None] = None
944
+ self._target_value: Union[float, None] = None
945
+ self._change_type: str = ""
946
+ self._start_year: int = 0
947
+ self._end_year: int = 0
948
+ self._function_type: str = ""
949
+ self._apply_to_targets: bool = True
950
+ self._opposite_target_process_ids = []
951
+
952
+ if params is None:
953
+ # Invalid: no parameters
954
+ return
955
+
956
+ if all(not elem for elem in params):
957
+ # Invalid: all parameters None
958
+ return
959
+
960
+ # Alias parameters to more readable form
961
+ param_scenario_name = params.iloc[0]
962
+ param_source_process_id = params.iloc[1]
963
+ param_target_process_id = params.iloc[2]
964
+ param_change_in_value = params.iloc[3]
965
+ param_target_value = params.iloc[4]
966
+ param_change_type = params.iloc[5]
967
+ param_start_year = params.iloc[6]
968
+ param_end_year = params.iloc[7]
969
+ param_function_type = params.iloc[8]
970
+ param_apply_to_targets = params.iloc[9]
971
+
972
+ self._scenario_name = self._parse_as(param_scenario_name, str)[0]
973
+ self._source_process_id = self._parse_as(param_source_process_id, str)[0]
974
+ self._target_process_id = self._parse_as(param_target_process_id, str)[0]
975
+
976
+ # This is the delta change of the value and means that it's error
977
+ # if target flow has ABS type and the 'change in value' is REL
978
+ # NOTE: Either of self._change_in_value of self._target_value must be defined
979
+ if param_change_in_value is not None:
980
+ self._change_in_value = self._parse_as(param_change_in_value, float)[0]
981
+
982
+ if param_target_value is not None:
983
+ self._target_value = self._parse_as(param_target_value, float)[0]
984
+
985
+ # Change type
986
+ # NOTE: Convert change type to valid ChangeType enum if found. Otherwise use the value from parameter.
987
+ self._change_type = param_change_type
988
+ for change_type in ChangeType:
989
+ if self._parse_as(self._change_type, str)[0].lower() == change_type.lower():
990
+ self._change_type = change_type
991
+
992
+ self._start_year = self._parse_as(param_start_year, int)[0]
993
+ self._end_year = self._parse_as(param_end_year, int)[0]
994
+
995
+ # Function type
996
+ # NOTE: Convert function type to valid FunctionType enum if found. Otherwise use the value from parameter.
997
+ self._function_type = param_function_type
998
+ for function_type in FunctionType:
999
+ if self._parse_as(self._function_type, str)[0].lower() == function_type:
1000
+ self._function_type = function_type
1001
+
1002
+ # Apply to siblings
1003
+ if param_apply_to_targets is not None:
1004
+ self._apply_to_targets = self._parse_as(param_apply_to_targets, bool)[0]
1005
+
1006
+ # Check how many target nodes with opposite effect there is
1007
+ for process_id in list(params[10:]):
1008
+ if process_id is not None:
1009
+ self._opposite_target_process_ids.append(process_id)
1010
+
1011
+ def __str__(self):
1012
+ s = "Flow modifier: scenario_name='{}', source_process_id='{}', target_process_id='{}', change_in_value='{}', " \
1013
+ "target_value='{}', change_type='{}', start_year='{}', end_year='{}', function_type='{}'".format(
1014
+ self.scenario_name,
1015
+ self.source_process_id,
1016
+ self.target_process_id,
1017
+ self.change_in_value,
1018
+ self.target_value,
1019
+ self.change_type,
1020
+ self.start_year,
1021
+ self.end_year,
1022
+ self.function_type,
1023
+ )
1024
+ return s
1025
+
1026
+ def is_valid(self) -> bool:
1027
+ return True
1028
+
1029
+ @property
1030
+ def use_change_in_value(self) -> bool:
1031
+ return self.change_in_value is not None
1032
+
1033
+ @property
1034
+ def use_target_value(self) -> bool:
1035
+ return self.target_value is not None
1036
+
1037
+ @property
1038
+ def is_change_type_value(self) -> bool:
1039
+ """
1040
+ Check if change type is value type (= absolute change in flow value or in flow share)
1041
+ :return: True if change type is value type, False otherwise.
1042
+ """
1043
+ return self.change_type == ChangeType.Value
1044
+
1045
+ @property
1046
+ def is_change_type_proportional(self) -> bool:
1047
+ """
1048
+ Is change type proportional (= relative change in flow value or in flow share)
1049
+ :return: True if change type is proportional, False otherwise.
1050
+ """
1051
+ return self.change_type == ChangeType.Proportional
1052
+
1053
+ @property
1054
+ def has_target(self) -> bool:
1055
+ """
1056
+ Does flow modifier target any flow?
1057
+ :return: Bool
1058
+ """
1059
+ return self.target_process_id != ""
1060
+
1061
+ @property
1062
+ def has_opposite_targets(self) -> bool:
1063
+ """
1064
+ Does flow modifier have opposite targets?
1065
+ :return: Bool
1066
+ """
1067
+ return len(self.opposite_target_process_ids) > 0
1068
+
1069
+
1070
+ @staticmethod
1071
+ def _parse_as(val: any, target_type: any) -> (bool, any):
1072
+ """
1073
+ Parse variable as type.
1074
+ Returns tuple (bool, value).
1075
+ If parsing fails then value is the original val.
1076
+
1077
+ :param val: Variable
1078
+ :param type: Target type
1079
+ :return: Tuple (bool, value)
1080
+ """
1081
+ ok = True
1082
+ result = val
1083
+ try:
1084
+ result = target_type(val)
1085
+ except ValueError as ex:
1086
+ ok = False
1087
+
1088
+ return result, ok
1089
+
1090
+ @property
1091
+ def scenario_name(self) -> str:
1092
+ return self._scenario_name
1093
+
1094
+ @property
1095
+ def source_process_id(self) -> str:
1096
+ return self._source_process_id
1097
+
1098
+ @property
1099
+ def target_process_id(self) -> str:
1100
+ return self._target_process_id
1101
+
1102
+ @property
1103
+ def target_flow_id(self) -> str:
1104
+ return Flow.make_flow_id(self.source_process_id, self.target_process_id)
1105
+
1106
+ @property
1107
+ def change_in_value(self) -> float:
1108
+ return self._change_in_value
1109
+
1110
+ @property
1111
+ def target_value(self) -> float:
1112
+ return self._target_value
1113
+
1114
+ @property
1115
+ def change_type(self) -> str:
1116
+ return self._change_type
1117
+
1118
+ @property
1119
+ def start_year(self) -> int:
1120
+ return self._start_year
1121
+
1122
+ @property
1123
+ def end_year(self) -> int:
1124
+ return self._end_year
1125
+
1126
+ @property
1127
+ def function_type(self) -> Union[FunctionType, str]:
1128
+ return self._function_type
1129
+
1130
+ @property
1131
+ def apply_to_targets(self) -> bool:
1132
+ return self._apply_to_targets
1133
+
1134
+ @property
1135
+ def opposite_target_process_ids(self) -> List[str]:
1136
+ return self._opposite_target_process_ids
1137
+
1138
+ def get_year_range(self) -> List[int]:
1139
+ """
1140
+ Get list of years FlowModifier is used.
1141
+
1142
+ :return: List of years (integers)
1143
+ """
1144
+ return [year for year in range(self.start_year, self.end_year + 1)]
1145
+
1146
+ def get_opposite_target_flow_ids(self) -> List[str]:
1147
+ """
1148
+ Get list of opposite target flow IDs.
1149
+
1150
+ :return: List of opposite target flow IDs.
1151
+ """
1152
+ opposite_flow_ids = []
1153
+ for opposite_process_id in self.opposite_target_process_ids:
1154
+ flow_id = Flow.make_flow_id(self.source_process_id, opposite_process_id)
1155
+ opposite_flow_ids.append(flow_id)
1156
+ return opposite_flow_ids
1157
+
1158
+
1159
+ class ScenarioData(object):
1160
+ """
1161
+ Data class for holding Scenario data.
1162
+
1163
+ DataChecker builds ScenarioData-object that can be used for FlowSolver.
1164
+ """
1165
+
1166
+ def __init__(self,
1167
+ years: List[int] = None,
1168
+ year_to_process_id_to_process: Dict[int, Dict[str, Process]] = None,
1169
+ year_to_process_id_to_flow_ids: Dict[int, Dict[str, Dict[str, List[str]]]] = None,
1170
+ year_to_flow_id_to_flow: Dict[int, Dict[str, Flow]] = None,
1171
+ stocks: List[Stock] = None,
1172
+ process_id_to_stock: Dict[str, Stock] = None,
1173
+ unique_process_id_to_process: Dict[str, Process] = None,
1174
+ unique_flow_id_to_flow: Dict[str, Flow] = None,
1175
+ use_virtual_flows: bool = True,
1176
+ virtual_flows_epsilon: float = 0.1,
1177
+ baseline_value_name: str = "Baseline",
1178
+ baseline_unit_name: str = "Baseline unit",
1179
+ indicator_name_to_indicator: Dict[str, Indicator] = None
1180
+ ):
1181
+
1182
+ if years is None:
1183
+ years = []
1184
+
1185
+ if year_to_process_id_to_process is None:
1186
+ year_to_process_id_to_process = {}
1187
+
1188
+ if year_to_process_id_to_flow_ids is None:
1189
+ year_to_process_id_to_flow_ids = {}
1190
+
1191
+ if year_to_flow_id_to_flow is None:
1192
+ year_to_flow_id_to_flow = {}
1193
+
1194
+ if stocks is None:
1195
+ stocks = []
1196
+
1197
+ if process_id_to_stock is None:
1198
+ process_id_to_stock = {}
1199
+
1200
+ if unique_process_id_to_process is None:
1201
+ unique_process_id_to_process = {}
1202
+
1203
+ if unique_flow_id_to_flow is None:
1204
+ unique_flow_id_to_flow = {}
1205
+
1206
+ if indicator_name_to_indicator is None:
1207
+ indicator_name_to_indicator = {}
1208
+
1209
+ self._year_to_flow_id_to_flow = year_to_flow_id_to_flow
1210
+ self._year_to_process_id_to_process = year_to_process_id_to_process
1211
+ self._year_to_process_id_to_flow_ids = year_to_process_id_to_flow_ids
1212
+
1213
+ self._years = years
1214
+ self._year_start = 0
1215
+ self._year_end = 0
1216
+
1217
+ if self._years:
1218
+ self._year_start = min(self._years)
1219
+ self._year_end = max(self._years)
1220
+
1221
+ self._process_id_to_stock = process_id_to_stock
1222
+ self._stocks = stocks
1223
+ self._unique_process_id_to_process = unique_process_id_to_process
1224
+ self._unique_flow_id_to_flow = unique_flow_id_to_flow
1225
+ self._use_virtual_flows = use_virtual_flows
1226
+ self._virtual_flows_epsilon = virtual_flows_epsilon
1227
+ self._baseline_value_name = baseline_value_name
1228
+ self._baseline_unit_name = baseline_unit_name
1229
+ self._indicator_name_to_indicator = indicator_name_to_indicator
1230
+
1231
+ @property
1232
+ def years(self) -> List[int]:
1233
+ """
1234
+ Get list of years
1235
+ :return: List of years
1236
+ """
1237
+ return self._years
1238
+
1239
+ @property
1240
+ def year_to_process_id_to_process(self) -> Dict[int, Dict[str, Process]]:
1241
+ """
1242
+ Get year to Process ID to Process mappings.
1243
+
1244
+ :return: Dictionary (Year -> Process ID -> Process)
1245
+ """
1246
+ return self._year_to_process_id_to_process
1247
+
1248
+ @property
1249
+ def year_to_process_id_to_flow_ids(self) -> Dict[int, Dict[str, Dict[str, List[str]]]]:
1250
+ """
1251
+ Get year to Process ID to In/Out to -> List of Flow ID mappings.
1252
+
1253
+ :return: Dictionary (Year -> Process ID -> Dictionary(keys "in", "out") -> List of Flow IDS)
1254
+ """
1255
+
1256
+ return self._year_to_process_id_to_flow_ids
1257
+
1258
+ @property
1259
+ def year_to_flow_id_to_flow(self) -> Dict[int, Dict[str, Flow]]:
1260
+ """
1261
+ Get year to Flow ID to Flow mappings.
1262
+
1263
+ :return: Dictionary (Year -> Flow ID -> Flow)
1264
+ """
1265
+ return self._year_to_flow_id_to_flow
1266
+
1267
+ @property
1268
+ def stocks(self) -> List[Stock]:
1269
+ """
1270
+ Get list of Stocks
1271
+ :return: List of Stocks
1272
+ """
1273
+ return self._stocks
1274
+
1275
+ @property
1276
+ def process_id_to_stock(self) -> Dict[str, Stock]:
1277
+ """
1278
+ Get mapping of Process ID to Stock
1279
+ :return: Dictionary
1280
+ """
1281
+ return self._process_id_to_stock
1282
+
1283
+ @property
1284
+ def unique_process_id_to_process(self) -> Dict[str, Process]:
1285
+ """
1286
+ Get mapping of unique Process ID to Process
1287
+ :return: Dictionary
1288
+ """
1289
+ return self._unique_process_id_to_process
1290
+
1291
+ @property
1292
+ def unique_flow_id_to_flow(self) -> Dict[str, Flow]:
1293
+ """
1294
+ Get mapping of unique Flow ID to Flows
1295
+ :return: Dictionary
1296
+ """
1297
+ return self._unique_flow_id_to_flow
1298
+
1299
+ @property
1300
+ def use_virtual_flows(self) -> bool:
1301
+ """
1302
+ Get boolean flag if using virtual flows
1303
+ :return: bool
1304
+ """
1305
+ return self._use_virtual_flows
1306
+
1307
+ @property
1308
+ def virtual_flows_epsilon(self) -> float:
1309
+ """
1310
+ Get maximum allowed difference between input and output flows before creating virtual flows.
1311
+ This is only used if using the virtual flows.
1312
+ :return: Float
1313
+ """
1314
+ return self._virtual_flows_epsilon
1315
+
1316
+ @property
1317
+ def start_year(self) -> int:
1318
+ """
1319
+ Get starting year
1320
+ :return: Starting year (int)
1321
+ """
1322
+ return self._year_start
1323
+
1324
+ @property
1325
+ def end_year(self) -> int:
1326
+ """
1327
+ Get ending year
1328
+ Ending year is included in simulation.
1329
+
1330
+ :return: Ending year (int)
1331
+ """
1332
+ return self._year_end
1333
+
1334
+ @property
1335
+ def baseline_value_name(self) -> str:
1336
+ """
1337
+ Get baseline value name (e.g. "Solid wood equivalent")
1338
+
1339
+ :return: Baseline value name (str)
1340
+ """
1341
+ return self._baseline_value_name
1342
+
1343
+ @baseline_value_name.setter
1344
+ def baseline_value_name(self, new_name: str):
1345
+ """
1346
+ Set new baseline value name.
1347
+
1348
+ :param new_name: New baseline value name (str)
1349
+ """
1350
+ self._baseline_value_name = new_name
1351
+
1352
+ @property
1353
+ def baseline_unit_name(self) -> str:
1354
+ """
1355
+ Get baseline unit name (e.g. "Mm3")
1356
+
1357
+ :return: Baseline unit name (str)
1358
+ """
1359
+ return self._baseline_unit_name
1360
+
1361
+ @baseline_unit_name.setter
1362
+ def baseline_unit_name(self, new_name: str):
1363
+ """
1364
+ Set new baseline unt name.
1365
+
1366
+ :param new_name: New baseline name (str)
1367
+ """
1368
+ self._baseline_unit_name = new_name
1369
+
1370
+ @property
1371
+ def indicator_name_to_indicator(self) -> Dict[str, Indicator]:
1372
+ """
1373
+ Get dictionary of Indicator name to Indicator.
1374
+
1375
+ :return: Dictionary (indicator name (str), Indicator)
1376
+ """
1377
+ return self._indicator_name_to_indicator
1378
+
1379
+ @indicator_name_to_indicator.setter
1380
+ def indicator_name_to_indicator(self, new_indicator_name_to_indicator: Dict[str, Indicator]):
1381
+ """
1382
+ Set new Indicator name to Indicator mapping.
1383
+
1384
+ :param new_indicator_name_to_indicator: New Indicator name to Indicator dictionary
1385
+ """
1386
+ self._indicator_name_to_indicator = new_indicator_name_to_indicator
1387
+
1388
+
1389
+ class ScenarioDefinition(object):
1390
+ """
1391
+ ScenarioDefinition is wrapper object that contains scenario name and all flow modifiers that are applied
1392
+ for the Scenario.
1393
+
1394
+ Actual building of Scenarios happens inside DataChecker.build_scenarios()
1395
+ """
1396
+ def __init__(self, name: str = None, flow_modifiers: List[FlowModifier] = None):
1397
+ if name is None:
1398
+ name = "Baseline scenario"
1399
+
1400
+ if flow_modifiers is None:
1401
+ flow_modifiers = []
1402
+
1403
+ self._name = name
1404
+ self._flow_modifiers = flow_modifiers
1405
+
1406
+ @property
1407
+ def name(self) -> str:
1408
+ """
1409
+ Get scenario name.
1410
+
1411
+ :return: Scenario name
1412
+ """
1413
+ return self._name
1414
+
1415
+ @property
1416
+ def flow_modifiers(self) -> List[FlowModifier]:
1417
+ """
1418
+ Get list of FlowModifiers.
1419
+ These are the rules that are applied to Scenario.
1420
+
1421
+ :return: List of FlowModifier-objects
1422
+ """
1423
+ return self._flow_modifiers
1424
+
1425
+
1426
+ class Scenario(object):
1427
+ """
1428
+ Scenario is wrapper object that contains scenario name and all flow modifiers that are
1429
+ happening in the scenario
1430
+ """
1431
+
1432
+ def __init__(self, definition: ScenarioDefinition = None, data: ScenarioData = None, model_params=None):
1433
+ if definition is None:
1434
+ definition = ScenarioDefinition()
1435
+
1436
+ if data is None:
1437
+ data = ScenarioData()
1438
+
1439
+ if model_params is None:
1440
+ model_params = {}
1441
+
1442
+ self._scenario_definition = definition
1443
+ self._scenario_data = data
1444
+ self._flow_solver = None
1445
+ self._odym_data = None
1446
+ self._model_params = model_params
1447
+ self._mfa_system = None
1448
+
1449
+ @property
1450
+ def name(self) -> str:
1451
+ return self._scenario_definition.name
1452
+
1453
+ @property
1454
+ def scenario_definition(self) -> ScenarioDefinition:
1455
+ return self._scenario_definition
1456
+
1457
+ @property
1458
+ def scenario_data(self) -> ScenarioData:
1459
+ return self._scenario_data
1460
+
1461
+ # Flow solver
1462
+ @property
1463
+ def flow_solver(self):
1464
+ """
1465
+ Get FlowSolver that is assigned to Scenario.
1466
+ :return: FlowSolver (FlowSolver)
1467
+ """
1468
+ return self._flow_solver
1469
+
1470
+ @flow_solver.setter
1471
+ def flow_solver(self, flow_solver):
1472
+ self._flow_solver = flow_solver
1473
+
1474
+ @property
1475
+ def model_params(self) -> Dict[str, Any]:
1476
+ return self._model_params
1477
+
1478
+ @property
1479
+ def mfa_system(self) -> MFAsystem:
1480
+ """
1481
+ Get stored ODYM MFA system.
1482
+ :return: MFAsystem-object
1483
+ """
1484
+ return self._mfa_system
1485
+
1486
+ @mfa_system.setter
1487
+ def mfa_system(self, mfa_system) -> None:
1488
+ """
1489
+ Set new MFAsystem
1490
+ :param mfa_system: Target MFAsystem-object
1491
+ """
1492
+ self._mfa_system = mfa_system
1493
+
1494
+ def copy_from_baseline_scenario_data(self, scenario_data: ScenarioData):
1495
+ """
1496
+ Copy ScenarioData from baseline Scenario.
1497
+ Data is deep copied and is not referencing to original data anymore.
1498
+
1499
+ :param scenario_data: ScenarioData from baseline FlowSolver.
1500
+ """
1501
+ self._scenario_data = copy.deepcopy(scenario_data)
1502
+
1503
+
1504
+ class Color(ObjectBase):
1505
+ def __init__(self, params: Union[List, pd.Series] = None, row_number=-1):
1506
+ super().__init__()
1507
+ self._name: str = ""
1508
+ self._value: str = ""
1509
+ self.row_number = row_number
1510
+
1511
+ if params is None:
1512
+ return
1513
+
1514
+ # Handle processing list and pd.Series differently
1515
+ name_val = ""
1516
+ value_val = ""
1517
+ if isinstance(params, list):
1518
+ name_val = str(params[0])
1519
+ value_val = str(params[1])
1520
+
1521
+ if isinstance(params, pd.Series):
1522
+ name_val = str(params.iloc[0])
1523
+ value_val = str(params.iloc[1])
1524
+
1525
+ self.name = name_val
1526
+ self.value = value_val
1527
+
1528
+ def __str__(self) -> str:
1529
+ """
1530
+ Returns the string presentation of the Color in format:
1531
+ #rrggbb
1532
+ where
1533
+ rr = red component
1534
+ gg = green component
1535
+ bb = blue component
1536
+
1537
+ :return: Color as hexadecimal string, prefixed with character '#'
1538
+ """
1539
+ return self.value.lower()
1540
+
1541
+ def is_valid(self) -> bool:
1542
+ return not self.name and self.value
1543
+
1544
+ @property
1545
+ def name(self) -> str:
1546
+ """
1547
+ Get color name.
1548
+
1549
+ :return: Color name (str)
1550
+ """
1551
+ return self._name
1552
+
1553
+ @name.setter
1554
+ def name(self, new_name: str):
1555
+ """
1556
+ Set color name.
1557
+
1558
+ :param new_name: New color name
1559
+ """
1560
+ self._name = new_name
1561
+
1562
+ @property
1563
+ def value(self) -> str:
1564
+ """
1565
+ Color value (hexadecimal, e.g. #AABBCC) prefixed with the character '#'
1566
+ """
1567
+ return self._value
1568
+
1569
+ @value.setter
1570
+ def value(self, new_value: str):
1571
+ """
1572
+ Set new color value (hexadecimal).
1573
+ New color must be prefixed with the character '#'.
1574
+
1575
+ :param new_value: New color value (hexadecimal)
1576
+ """
1577
+ self._value = new_value
1578
+
1579
+ def get_red_as_float(self) -> float:
1580
+ """
1581
+ Get red component value in range [0, 1]
1582
+ :return: Red value (float)
1583
+ """
1584
+ return self._hex_to_normalized_float(self.value[1:][0:2])
1585
+
1586
+ def get_green_as_float(self) -> float:
1587
+ """
1588
+ Get green component value in range [0, 1]
1589
+ :return: Green value (float)
1590
+ """
1591
+ return self._hex_to_normalized_float(self.value[1:][2:4])
1592
+
1593
+ def get_blue_as_float(self) -> float:
1594
+ """
1595
+ Get blue component value in range [0, 1]
1596
+ :return: Blue value (float)
1597
+ """
1598
+ return self._hex_to_normalized_float(self.value[1:][4:6])
1599
+
1600
+ def _hex_to_normalized_float(self, hex_value: str) -> float:
1601
+ return int(hex_value, 16) / 255.0
1602
+
1603
+
1604
+ class ProcessEntry(object):
1605
+ """
1606
+ Internal storage class for Process entry (process, inflows, and outflows).
1607
+ Used when storing Process data in DataFrames.
1608
+ """
1609
+
1610
+ KEY_IN: str = "in"
1611
+ KEY_OUT: str = "out"
1612
+
1613
+ def __init__(self, process: Process = None):
1614
+ """
1615
+ Initialize ProcessEntry.
1616
+ Makes deep copy of target Process.
1617
+
1618
+ :param process: Target Process
1619
+ """
1620
+ self._process = process
1621
+ self._flows = {self.KEY_IN: {}, self.KEY_OUT: {}}
1622
+
1623
+ @property
1624
+ def process(self) -> Process:
1625
+ return self._process
1626
+
1627
+ @property
1628
+ def flows(self) -> Dict[str, Dict[str, Flow]]:
1629
+ return self._flows
1630
+
1631
+ @flows.setter
1632
+ def flows(self, flows: Dict[str, Dict[str, Flow]]):
1633
+ self._flows = flows
1634
+
1635
+ @property
1636
+ def inflows(self) -> List[Flow]:
1637
+ return list(self._flows[self.KEY_IN].values())
1638
+
1639
+ @inflows.setter
1640
+ def inflows(self, flows: List[Flow]):
1641
+ self._flows[self.KEY_IN] = {flow.id: flow for flow in flows}
1642
+
1643
+ @property
1644
+ def inflows_as_dict(self) -> Dict[str, Flow]:
1645
+ return self._flows[self.KEY_IN]
1646
+
1647
+ @property
1648
+ def outflows(self) -> List[Flow]:
1649
+ return list(self._flows[self.KEY_OUT].values())
1650
+
1651
+ @outflows.setter
1652
+ def outflows(self, flows: List[Flow]):
1653
+ self._flows[self.KEY_OUT] = {flow.id: flow for flow in flows}
1654
+
1655
+ def outflows_as_dict(self) -> Dict[str, Flow]:
1656
+ return self._flows[self.KEY_OUT]
1657
+
1658
+ def add_inflow(self, flow: Flow):
1659
+ self._flows[self.KEY_IN][flow.id] = flow
1660
+
1661
+ def add_outflow(self, flow: Flow):
1662
+ self._flows[self.KEY_OUT][flow.id] = flow
1663
+
1664
+ def remove_inflow(self, flow_id: str):
1665
+ """
1666
+ Remove inflow by Flow ID.
1667
+ Raises Exception if Flow ID is not found in inflows.
1668
+
1669
+ :param flow_id: Target Flow ID
1670
+ :raises Exception If Flow ID is not found
1671
+ """
1672
+ removed_flow_id = self._flows[self.KEY_IN].pop(flow_id, None)
1673
+ if not removed_flow_id:
1674
+ raise Exception("No flow_id {} in inflows".format(flow_id))
1675
+
1676
+ def remove_outflow(self, flow_id: str):
1677
+ """
1678
+ Remove outflow by Flow ID.
1679
+ Raises Exception if Flow ID is not found in outflows.
1680
+
1681
+ :param flow_id: Target Flow ID
1682
+ :raises Exception If Flow ID is not found
1683
+ """
1684
+ removed_flow_id = self._flows[self.KEY_OUT].pop(flow_id, None)
1685
+ if not removed_flow_id:
1686
+ raise Exception("No flow_id {} in outflows".format(flow_id))