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,269 @@
1
+ # Parameters names are defined in this file
2
+ # and DataProvider file provides the default
3
+ # values for each parameter
4
+ from enum import Enum
5
+ from typing import Dict, Any
6
+
7
+
8
+ class ParameterName(str, Enum):
9
+ """
10
+ Parameter enumerations used in settings file
11
+ """
12
+
13
+ # ***********************
14
+ # * Required parameters *
15
+ # ***********************
16
+
17
+ # Process related
18
+ SheetNameProcesses: str = "sheet_name_processes"
19
+ SkipNumRowsProcesses: str = "skip_num_rows_processes"
20
+ IgnoreColumnsProcesses: str = "ignore_columns_processes"
21
+
22
+ # Flow related
23
+ SheetNameFlows: str = "sheet_name_flows"
24
+ SkipNumRowsFlows: str = "skip_num_rows_flows"
25
+ IgnoreColumnsFlows: str = "ignore_columns_flows"
26
+
27
+ # Model parameters
28
+ StartYear: str = "start_year"
29
+ EndYear: str = "end_year"
30
+ DetectYearRange: str = "detect_year_range"
31
+ UseVirtualFlows: str = "use_virtual_flows"
32
+ VirtualFlowsEpsilon: str = "virtual_flows_epsilon"
33
+
34
+ # ***********************
35
+ # * Optional parameters *
36
+ # ***********************
37
+ ConversionFactorCToCO2: str = "conversion_factor_c_to_co2"
38
+ FillMissingAbsoluteFlows: str = "fill_missing_absolute_flows"
39
+ FillMissingRelativeFlows: str = "fill_missing_relative_flows"
40
+ FillMethod: str = "fill_method"
41
+
42
+ # Scenarios related
43
+ UseScenarios: str = "use_scenarios"
44
+ SheetNameScenarios: str = "sheet_name_scenarios"
45
+ SkipNumRowsScenarios: str = "skip_num_rows_scenarios"
46
+ IgnoreColumnsScenarios: str = "ignore_columns_scenarios"
47
+ ScenarioType: str = "scenario_type"
48
+
49
+ # Sheet name for colors
50
+ SheetNameColors: str = "sheet_name_colors"
51
+ SkipNumRowsColors: str = "skip_num_rows_colors"
52
+ IgnoreColumnsColors: str = "ignore_columns_colors"
53
+
54
+ # Network graph
55
+ CreateNetworkGraphs: str = "create_network_graphs"
56
+ CreateSankeyCharts: str = "create_sankey_charts"
57
+
58
+ # Output path
59
+ OutputPath: str = "output_path"
60
+
61
+ # Show plots
62
+ ShowPlots: str = "show_plots"
63
+
64
+ # Visualize inflows to process IDs
65
+ VisualizeInflowsToProcesses: str = "visualize_inflows_to_processes"
66
+
67
+ # Baseline unit
68
+ # BaseLineValueName: Name of the baseline value e.g. "Solid wood equivalent"
69
+ # BaselineUnitName: Name of the baseline unit e.g. Mm3 SWE
70
+ BaselineValueName: str = "baseline_value_name"
71
+ BaselineUnitName: str = "baseline_unit_name"
72
+
73
+ # Flow prioritization
74
+ PrioritizeTransformationStages: str = "prioritize_transformation_stages"
75
+ PrioritizeLocations: str = "prioritize_locations"
76
+
77
+ # Sheet name for process positions
78
+ SheetNameProcessPositions: str = "sheet_name_process_positions"
79
+
80
+ # Check errors
81
+ CheckErrors: str = "check_errors"
82
+
83
+
84
+ class ParameterFillMethod(str, Enum):
85
+ """
86
+ Valid values for parameter FillMethod
87
+ """
88
+
89
+ Zeros: str = "Zeros"
90
+ Previous: str = "Previous"
91
+ Next: str = "Next"
92
+ Interpolate: str = "Interpolate"
93
+
94
+
95
+ class ParameterScenarioType(str, Enum):
96
+ """
97
+ Valid values for parameter ScenarioType
98
+ """
99
+
100
+ Constrained: str = "Constrained"
101
+ Unconstrained: str = "Unconstrained"
102
+
103
+
104
+ # Parameters used for Process/Flows/Stocks in settings file
105
+ class StockDistributionType(str, Enum):
106
+ """
107
+ Stock distribution types and decay functions
108
+ """
109
+ Fixed: str = "Fixed"
110
+ Normal: str = "Normal"
111
+ LogNormal: str = "LogNormal"
112
+ FoldedNormal: str = "FoldedNormal"
113
+ Weibull: str = "Weibull"
114
+ Simple: str = "Simple"
115
+ LandfillDecayWood: str = "LandfillDecayWood"
116
+ LandfillDecayPaper: str = "LandfillDecayPaper"
117
+
118
+
119
+ class LandfillDecayParameter(str, Enum):
120
+ """
121
+ Parameter names for LandFillDecay* types
122
+ """
123
+ Condition: str = "condition"
124
+
125
+
126
+ # *********************************
127
+ # * Stock distribution parameters *
128
+ # *********************************
129
+
130
+ class StockDistributionParameter(str, Enum):
131
+ """
132
+ Stock distribution parameters.
133
+ """
134
+ StdDev: str = "stddev"
135
+ Shape: str = "shape"
136
+ Scale: str = "scale"
137
+ Condition: str = "condition"
138
+
139
+
140
+ class StockDistributionParameterValueType(object):
141
+ """
142
+ Storage class for stock distribution parameters.
143
+ Emulates Dictionary behaviour allowing to get value using
144
+ the [] notation e.g.
145
+ StockDistributionParameterValueType[StockDistributionParameter.StdDev]
146
+ StockDistributionParameterValueType["stddev"]
147
+ """
148
+ parameter_to_value_type = {
149
+ StockDistributionParameter.StdDev: float,
150
+ StockDistributionParameter.Shape: float,
151
+ StockDistributionParameter.Scale: float,
152
+ StockDistributionParameter.Condition: str,
153
+ }
154
+
155
+ def __class_getitem__(cls, item):
156
+ """
157
+ Get value type for StockDistributionParameter
158
+
159
+ :param item: Item (str or StockDistributionParameter)
160
+ :return:
161
+ """
162
+ item_enum = None
163
+ if isinstance(item, str):
164
+ try:
165
+ item_enum = StockDistributionParameter(item)
166
+ except ValueError:
167
+ pass
168
+
169
+ if item_enum is None:
170
+ #raise Exception("{} is not valid StockDistributionParameter".format(item_enum))
171
+ pass
172
+
173
+ value_type = cls.parameter_to_value_type.get(item_enum, None)
174
+ if value_type is None:
175
+ #raise Exception("No value type for {}!".format(item_enum))
176
+ pass
177
+
178
+ return value_type
179
+
180
+
181
+ class RequiredStockDistributionParameters(object):
182
+ """
183
+ Storage class for required stock distribution parameters.
184
+ Emulates Dictionary behaviour allowing to get value using
185
+ the [] notation e.g.
186
+ RequiredStockDistributionParameters[StockDistributionType.Fixed]
187
+ RequiredStockDistributionParameters["Fixed"]
188
+ """
189
+
190
+ # NOTE: value must be dictionary
191
+ # NOTE: Key = StockDistributionType, Value: Dictionary (str -> required type)
192
+ stock_distribution_to_required_params = {
193
+ StockDistributionType.Fixed: {},
194
+ StockDistributionType.Normal: {"stddev": float},
195
+ StockDistributionType.LogNormal: {"stddev": float},
196
+ StockDistributionType.FoldedNormal: {"stddev": float},
197
+ StockDistributionType.Weibull: {"shape": float, "scale": float},
198
+ StockDistributionType.Simple: {},
199
+ StockDistributionType.LandfillDecayWood: {"condition": str},
200
+ StockDistributionType.LandfillDecayPaper: {"condition": str},
201
+ }
202
+
203
+ def __class_getitem__(cls, item) -> Dict[str, Any]:
204
+ """
205
+ Get required parameters for StockDistributionType.
206
+ Parameter 'item' can be either string or StockDistributionType-instance.
207
+
208
+ :param item: String or instance of StockDistributionType
209
+ :return: Dictionary of required parameters (key = parameter name, value = type of required parameter e.g. float)
210
+ """
211
+
212
+ # Convert string to StockDistributionType Enum
213
+ item_enum = None
214
+ if isinstance(item, str):
215
+ try:
216
+ item_enum = StockDistributionType(item)
217
+ except ValueError:
218
+ pass
219
+
220
+ if item_enum is None:
221
+ # raise Exception("{} is not in RequiredStockDistributionParameters".format(item_name))
222
+ pass
223
+
224
+ params = cls.stock_distribution_to_required_params.get(item_enum, None)
225
+ if params is None:
226
+ # raise Exception("{} is not valid StockDistributionType!".format(item_enum))
227
+ pass
228
+
229
+ return params
230
+
231
+
232
+ class AllowedStockDistributionParameterValues(object):
233
+ """
234
+ Storage class for required stock distribution parameter values.
235
+ Emulates Dictionary behaviour allowing to get value using
236
+ the [] notation e.g.
237
+ AllowedStockDistributionParameterValues[StockDistributionType.Fixed]
238
+ AllowedStockDistributionParameterValues["Fixed"]
239
+ """
240
+
241
+ # NOTE: value must be dictionary
242
+ # NOTE: Key = StockDistributionType, Value: Dictionary (str -> required type)
243
+ stock_distribution_param_to_allowed_values = {
244
+ StockDistributionParameter.Condition: {"Dry": str, "Wet": str, "Managed": str}
245
+ }
246
+
247
+ def __class_getitem__(cls, item) -> Dict[str, Any]:
248
+ """
249
+ Get allowed parameters for StockDistributionParameter.
250
+ Parameter 'item' can be either string or StockDistributionParameter-instance.
251
+
252
+ :param item: String or instance of StockDistributionParameter
253
+ :return: Dictionary of allowed entries (key: StockDistributionParameter-instance, value: type)
254
+ """
255
+
256
+ # Convert string to StockDistributionParameter Enum
257
+ item_enum = None
258
+ if isinstance(item, str):
259
+ try:
260
+ item_enum = StockDistributionParameter(item)
261
+ except ValueError:
262
+ pass
263
+
264
+ # If item_enum is not found in list of allowed values then return empty Dictionary
265
+ params = cls.stock_distribution_param_to_allowed_values.get(item_enum, None)
266
+ if params is None:
267
+ params = {}
268
+
269
+ return params
aiphoria/core/types.py ADDED
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+
3
+
4
+ # Flow modifier parameters
5
+ class FunctionType(str, Enum):
6
+ Constant: str = "constant"
7
+ Linear: str = "linear"
8
+ Exponential: str = "exponential"
9
+ Sigmoid: str = "sigmoid"
10
+
11
+
12
+ class ChangeType(str, Enum):
13
+ Value: str = "Value"
14
+ Proportional: str = "%"
15
+
16
+
17
+ class FlowType(str, Enum):
18
+ Absolute: str = "ABS"
19
+ Relative: str = "REL"
20
+
aiphoria/core/utils.py ADDED
@@ -0,0 +1,362 @@
1
+ import os
2
+ import shutil
3
+ import sys
4
+ import re
5
+ from typing import List, Dict, Any, Union, Optional
6
+ import numpy as np
7
+ import pandas as pd
8
+ from IPython import get_ipython
9
+ from .datastructures import Scenario, Flow
10
+ import aiphoria.lib.odym.modules.ODYM_Classes as msc
11
+
12
+ global_path_to_output_dir: Union[str, None] = None
13
+
14
+
15
+ def show_model_parameters(model_params: Dict[str, Any]):
16
+ """
17
+ Show model parameters (= prints the model parameters to console).
18
+
19
+ :param model_params: Dictionary of model parameters
20
+ """
21
+ print("Using following parameters for running the model:")
22
+ max_param_len = max([len(name) for name in model_params])
23
+ for param_name, param_value in model_params.items():
24
+ print("\t{:{}} = {}".format(param_name, max_param_len, param_value))
25
+
26
+
27
+ def show_exception_errors(exception: Exception, msg: str = ""):
28
+ """
29
+ Show Exception errors.
30
+
31
+ :param msg: Message about exception.
32
+ :param exception: Exception
33
+ """
34
+
35
+ if msg:
36
+ print(msg)
37
+
38
+ errors = exception.args[0]
39
+ if type(errors) is str:
40
+ print(errors)
41
+
42
+ if type(errors) is list:
43
+ for error in errors:
44
+ print("\t{}".format(error))
45
+
46
+
47
+ def setup_current_working_directory():
48
+ """
49
+ Setup current working directory.
50
+ """
51
+ if get_ipython() is None:
52
+ # Running in terminal
53
+ os.chdir(os.path.dirname(os.path.abspath(sys.argv[0])))
54
+ else:
55
+ # Running Notebook, cwd is already set
56
+ pass
57
+
58
+
59
+ def create_output_directory(output_dir_path: str):
60
+ """
61
+ Create output directory.
62
+ Deletes existing directory.
63
+ """
64
+
65
+ # If exists then delete existing directory and create new
66
+ if os.path.exists(output_dir_path):
67
+ try:
68
+ shutil.rmtree(output_dir_path)
69
+ except Exception as ex:
70
+ raise ex
71
+
72
+ # Create output directory and directories for all scenarios
73
+ os.makedirs(output_dir_path)
74
+
75
+
76
+ def setup_odym_directories():
77
+ """
78
+ Setup ODYM directories. Appends to path to ODYM files to sys.path.
79
+ """
80
+ sys.path.insert(0, os.path.join(
81
+ os.getcwd(), '.', 'lib', 'odym', 'modules'))
82
+
83
+
84
+ def setup_scenario_output_directories(output_dir_path: str, scenario_names: List[str]) -> Dict[str, str]:
85
+ """
86
+ Setup scenario output directories. Deletes existing scenario directory if exists.
87
+ Returns Dictionary of scenario name to scenario directory (absolute path)
88
+
89
+ :param output_dir_name: Output directory name
90
+ :param scenario_names: List of scenario names
91
+ :return: Dictionary [str, str]: Scenario name to scenario directory (absolute path)
92
+ """
93
+
94
+ scenario_name_to_abs_scenario_output_path = {}
95
+ for scenario_name in scenario_names:
96
+ abs_scenario_output_path = os.path.join(output_dir_path, scenario_name)
97
+ try:
98
+ shutil.rmtree(abs_scenario_output_path)
99
+ except FileNotFoundError as ex:
100
+ pass
101
+ except Exception as ex:
102
+ raise ex
103
+
104
+ os.makedirs(abs_scenario_output_path)
105
+ scenario_name_to_abs_scenario_output_path[scenario_name] = abs_scenario_output_path
106
+
107
+ return scenario_name_to_abs_scenario_output_path
108
+
109
+
110
+ def build_mfa_system_for_scenario(scenario: Scenario):
111
+ """
112
+ Build MFA system for scenario.
113
+
114
+ :param scenario: Scenario-object
115
+ :param progress_bar: Progress bar instance (optional)
116
+ :return: ODYM MFASystem
117
+ """
118
+
119
+ # Track solid wood equivalent and carbon.
120
+ # Dictionary of classifications enters the index table defined for the system.
121
+ # The index table lists all aspects needed and assigns a classification and index letter to each aspect.
122
+ scenario_data = scenario.scenario_data
123
+ flow_solver = scenario.flow_solver
124
+ years = scenario_data.years
125
+
126
+ # Baseline value and unit name: e.g. name = "Solid wood equivalent and unit = "Mm3"
127
+ baseline_value_name = scenario_data.baseline_value_name
128
+ baseline_unit_name = scenario_data.baseline_unit_name
129
+
130
+ # Indicator names are used as Elements in ODYM MFASystem
131
+ indicator_names = flow_solver.get_indicator_names()
132
+
133
+ model_time_start = scenario_data.start_year
134
+ model_time_end = scenario_data.end_year
135
+ model_elements = [baseline_value_name] + indicator_names
136
+ model_years = years
137
+
138
+ model_classifications = {
139
+ 'Time': msc.Classification(Name='Time', Dimension='Time', ID=1, Items=model_years),
140
+ 'Cohort': msc.Classification(Name='Age-cohort', Dimension='Time', ID=2, Items=model_years),
141
+ 'Element': msc.Classification(Name='Elements', Dimension='Element', ID=3, Items=model_elements),
142
+ }
143
+
144
+ index_table = pd.DataFrame({'Aspect': ['Time', 'Age-cohort', 'Element'], # 'Time' and 'Element' must be present!
145
+ 'Description': ['Model aspect "time"', 'Model aspect "age-cohort"', 'Model aspect "Element"'],
146
+ # 'Time' and 'Element' are also dimensions
147
+ 'Dimension': ['Time', 'Time', 'Element'],
148
+ 'Classification': [model_classifications[Aspect] for Aspect in ['Time', 'Cohort', 'Element']],
149
+ # Unique one letter (upper or lower case) indices to be used later for calculations.
150
+ 'IndexLetter': ['t', 'c', 'e']})
151
+
152
+ # Default indexing of IndexTable, other indices are produced on the fly
153
+ index_table.set_index('Aspect', inplace=True)
154
+
155
+ # ****************************
156
+ # Initialize ODYM MFA system *
157
+ # ****************************
158
+ flow_solver = scenario.flow_solver
159
+ mfa_system = msc.MFAsystem(Name='MFA System',
160
+ Geogr_Scope='Not defined',
161
+ Unit=baseline_unit_name,
162
+ ProcessList=[], FlowDict={}, StockDict={}, ParameterDict={},
163
+ Time_Start=model_time_start, Time_End=model_time_end, IndexTable=index_table,
164
+ Elements=index_table.loc['Element'].Classification.Items)
165
+
166
+ # Get inflow values to stock
167
+ year_index_to_year = dict(enumerate(model_years))
168
+ unique_processes = flow_solver.get_unique_processes()
169
+ unique_flows = flow_solver.get_unique_flows()
170
+
171
+ # Create ODYM objects
172
+ # print("Building ODYM processes...")
173
+ odym_processes = []
174
+ process_id_to_index = {}
175
+ for process_id, process in unique_processes.items():
176
+ process_index = len(odym_processes)
177
+ process_id_to_index[process_id] = process_index
178
+ new_process = msc.Process(ID=process_index, Name=process.name)
179
+ odym_processes.append(new_process)
180
+
181
+ # print("Building ODYM flows...")
182
+ odym_flows = {}
183
+ for flow_id, flow in unique_flows.items():
184
+ source_process_index = process_id_to_index[flow.source_process_id]
185
+ target_process_index = process_id_to_index[flow.target_process_id]
186
+ new_flow = msc.Flow(ID=flow.id, P_Start=source_process_index,
187
+ P_End=target_process_index, Indices='t,e', Values=None)
188
+ odym_flows[flow.id] = new_flow
189
+
190
+ # print("Building ODYM stocks...")
191
+ odym_stocks = {}
192
+ for stock in flow_solver.get_all_stocks():
193
+ process_index = process_id_to_index[stock.id]
194
+ new_stock = msc.Stock(ID=stock.id, Name=stock.name,
195
+ P_Res=process_index, Indices='t,e', Type=0, Values=None)
196
+ odym_stocks[stock.id] = new_stock
197
+
198
+ mfa_system.ProcessList = odym_processes
199
+ mfa_system.FlowDict = odym_flows
200
+ mfa_system.StockDict = odym_stocks
201
+ mfa_system.Initialize_FlowValues()
202
+ mfa_system.Initialize_StockValues()
203
+ mfa_system.Consistency_Check()
204
+
205
+ # Update ODYM flow values from flow values DataFrame
206
+ for flow_id, flow in mfa_system.FlowDict.items():
207
+ for year_index, value in enumerate(flow.Values):
208
+ # Skip to next year if FlowSolver does not have data for this year
209
+ # This is possible because ODYM flow and stock values are already initialized to 0.0
210
+ flow_has_data_for_year = flow_solver.has_flow(
211
+ year=year_index_to_year[year_index], flow_id=flow_id)
212
+ if not flow_has_data_for_year:
213
+ continue
214
+
215
+ # NOTE: Virtual flows use default value defined in Flow for carbon content (now 1.0).
216
+ # Get all evaluated values and set those to ODYM flow
217
+ solved_flow = flow_solver.get_flow(
218
+ year=year_index_to_year[year_index], flow_id=flow_id)
219
+
220
+ # If flow is not present this year, use 0.0 value
221
+ if not isinstance(solved_flow, Flow):
222
+ for index, indicator_name in enumerate(indicator_names):
223
+ flow.Values[year_index, index] = 0.0
224
+ continue
225
+
226
+ for index, evaluated_value in enumerate(solved_flow.get_all_evaluated_values()):
227
+ flow.Values[year_index, index] = evaluated_value
228
+
229
+ # Process stocks (fill with data)
230
+ for stock_id, stock in odym_stocks.items():
231
+ # Baseline DSM
232
+ # These are needed for triggering DSM calculations
233
+ baseline_dsm = flow_solver.get_baseline_dynamic_stocks()[stock_id]
234
+ baseline_stock_by_cohort = baseline_dsm.compute_s_c_inflow_driven()
235
+ baseline_outflow_by_cohort = baseline_dsm.compute_o_c_from_s_c()
236
+ baseline_stock_total = baseline_dsm.compute_stock_total()
237
+ baseline_stock_change = baseline_dsm.compute_stock_change()
238
+ stock.Values[:, 0] = baseline_stock_change
239
+
240
+ # Indicator DSMs
241
+ for indicator_index, indicator_name in enumerate(indicator_names):
242
+ # These are needed for triggering DSM calculations
243
+ indicator_dsm = flow_solver.get_indicator_dynamic_stocks()[
244
+ stock_id][indicator_name]
245
+ indicator_stock_by_cohort = indicator_dsm.compute_s_c_inflow_driven()
246
+ indicator_outflow_by_cohort = indicator_dsm.compute_o_c_from_s_c()
247
+ indicator_stock_total = indicator_dsm.compute_stock_total()
248
+ indicator_stock_change = indicator_dsm.compute_stock_change()
249
+
250
+ # Baseline values are at index 0 so offset by +1
251
+ stock.Values[:, indicator_index + 1] = indicator_stock_change
252
+
253
+ return mfa_system
254
+
255
+
256
+ def calculate_scenario_mass_balance(mfa_system: msc.MFAsystem) -> pd.DataFrame:
257
+ """
258
+ Get scenario mass balance difference per year.
259
+
260
+ :param mfa_system: MFASystem-object
261
+ :param model_years: List of years
262
+ :return: DataFrame
263
+ """
264
+
265
+ mb = mfa_system.MassBalance()
266
+ # print("Mass balance result shape: {}".format(mb.shape))
267
+ df_mass_balance = pd.DataFrame(
268
+ columns=["Year", "Process 0", "Rest", "Difference"])
269
+ for year_index, year in enumerate(mfa_system.Time_L):
270
+ # Calculate mass balance using the first element in MFA system (= base element)
271
+ # Negative value in process 0 means that process 0 has no inflows so this mass
272
+ # is coming from outside system boundaries
273
+ p0 = np.sum(mb[year_index][0][0])
274
+ rest = np.sum(mb[year_index][1:, 0])
275
+ abs_diff = abs(p0) - abs(rest)
276
+ df_mass_balance.loc[year_index] = np.array([year, p0, rest, abs_diff])
277
+ df_mass_balance = df_mass_balance.astype({"Year": "int32"})
278
+ # df_mass_balance.set_index(["Year"], inplace=True)
279
+ return df_mass_balance
280
+
281
+
282
+ def shorten_sheet_name(name, max_length=31):
283
+ """Shorten and sanitize Excel sheet names to comply with the 31-character limit."""
284
+ # Sanitize name
285
+ sanitized_name = re.sub(r'[:/\\]', '_', name)
286
+
287
+ # Split by underscores to maintain structure
288
+ parts = sanitized_name.split('_')
289
+
290
+ # Identify fixed parts: these are short words (<= 3 chars) or common connectors
291
+ fixed_parts = {i: p for i, p in enumerate(
292
+ parts) if len(p) <= 3 or not p.isalnum()}
293
+ variable_parts = [p for i, p in enumerate(parts) if i not in fixed_parts]
294
+
295
+ # Determine available length for variable parts (accounting for underscores and fixed parts)
296
+ num_underscores = len(parts) - 1
297
+ fixed_length = sum(len(p) for p in fixed_parts.values())
298
+ available_length = max_length - num_underscores - fixed_length
299
+
300
+ # Reduce variable parts proportionally
301
+ total_variable_length = sum(len(p) for p in variable_parts)
302
+ if total_variable_length <= available_length:
303
+ return sanitized_name[:max_length] # Trim if needed
304
+
305
+ reduced_parts = []
306
+ for part in variable_parts:
307
+ reduced_length = max(
308
+ 1, len(part) * available_length // total_variable_length)
309
+ reduced_parts.append(part[:reduced_length])
310
+
311
+ # Reconstruct the name, preserving fixed parts
312
+ final_parts = []
313
+ variable_index = 0
314
+ for i in range(len(parts)):
315
+ if i in fixed_parts:
316
+ final_parts.append(fixed_parts[i])
317
+ else:
318
+ final_parts.append(reduced_parts[variable_index])
319
+ variable_index += 1
320
+
321
+ # Ensure final length
322
+ shortened_name = "_".join(final_parts)
323
+ return shortened_name[:max_length]
324
+
325
+
326
+ def get_output_directory() -> str:
327
+ """
328
+ Get output path directory.
329
+ NOTE: Must be using the set_output_directory(), otherwise raises Exception
330
+ """
331
+
332
+ if global_path_to_output_dir is None:
333
+ raise Exception("Output path not set")
334
+
335
+ return global_path_to_output_dir
336
+
337
+
338
+ def set_output_directory(path_to_output_dir: str) -> None:
339
+ """
340
+ Set output directory for results.
341
+ Path is converted to absolute path (e.g. .., . and ~ are converted to
342
+ actual directory names).
343
+
344
+ :param path_to_output_dir: Path to directory
345
+
346
+ """
347
+ # NOTE: os.path.realpath follows symlinks where os.path.abspath does not
348
+ # NOTE: ~ is not expanded to user home directory in Linux
349
+ global_path_to_output_dir = os.path.realpath(path_to_output_dir)
350
+
351
+
352
+ def make_output_directory(path_to_output: str = "", remove_existing: bool = True) -> None:
353
+ """
354
+ Make output directory.
355
+ If path to output directory is not set then creates directory "output"
356
+ by default to the directory of the running script.
357
+
358
+ :param remove_existing: If True then removes existing directory.
359
+ """
360
+ # TODO: Implement creating output directory
361
+
362
+ os.makedirs(exist_ok=True)
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class VisualizerParameters(str, Enum):
5
+ SmallNodeThreshold = "small_node_threshold"
6
+ FlowAlpha = "flow_alpha"
7
+ TransformationStageNameToColor = "transformation_stage_name_to_color"
Binary file