lca-modeller 0.0.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.
- lca_modeller/__init__.py +0 -0
- lca_modeller/gui/__init__.py +0 -0
- lca_modeller/gui/plots.py +88 -0
- lca_modeller/helpers.py +206 -0
- lca_modeller/io/__init__.py +0 -0
- lca_modeller/io/configuration.py +1145 -0
- lca_modeller/io/resources/__init__.py +0 -0
- lca_modeller/io/resources/configuration.json +317 -0
- lca_modeller/io/tests/__init__.py +0 -0
- lca_modeller/notebook/data/advanced_example.yaml +90 -0
- lca_modeller/notebook/data/basic_example.yaml +50 -0
- lca_modeller/notebook/data/basic_example_split_impacts.yaml +54 -0
- lca_modeller/notebook/data/custom_lcia_methods.yaml +59 -0
- lca_modeller/notebook/data/lcia_gwp100_custom.csv +3 -0
- lca_modeller/notebook/data/metadata_example.yaml +77 -0
- lca_modeller/notebook/data/prospective_lca.yaml +65 -0
- lca_modeller/notebook/tutorial-notebook.ipynb +416 -0
- lca_modeller-0.0.0.dist-info/METADATA +69 -0
- lca_modeller-0.0.0.dist-info/RECORD +20 -0
- lca_modeller-0.0.0.dist-info/WHEEL +4 -0
lca_modeller/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plots for life cycle assessments interpretation
|
|
3
|
+
"""
|
|
4
|
+
from pyvis.network import Network
|
|
5
|
+
import os
|
|
6
|
+
from IPython.display import display, HTML, IFrame
|
|
7
|
+
from lca_algebraic.activity import Activity
|
|
8
|
+
from lca_modeller.helpers import list_processes
|
|
9
|
+
import matplotlib
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
|
|
12
|
+
USER_DB = 'Foreground DB'
|
|
13
|
+
default_process_tree_filename = 'process_tree.html'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def process_tree(model: Activity,
|
|
17
|
+
foreground_only: bool = True,
|
|
18
|
+
outfile: str = default_process_tree_filename,
|
|
19
|
+
colormap: str = 'Pastel2'):
|
|
20
|
+
"""
|
|
21
|
+
Plots an interactive tree to visualize the activities and exchanges declared in the LCA module.
|
|
22
|
+
:param model: The model containing activities and exchanges.
|
|
23
|
+
:param foreground_only: Boolean flag to include only foreground activities.
|
|
24
|
+
:param outfile: Output filename for the generated tree.
|
|
25
|
+
:param colormap: Colormap to use for node coloring.
|
|
26
|
+
"""
|
|
27
|
+
# Init network
|
|
28
|
+
net = Network(notebook=True, directed=True, layout=True, cdn_resources='remote', filter_menu=False)
|
|
29
|
+
|
|
30
|
+
# Get processes hierarchy
|
|
31
|
+
df = list_processes(model, foreground_only)
|
|
32
|
+
df['description'] = df['activity'] + '\n' + df['unit'].fillna('')
|
|
33
|
+
|
|
34
|
+
# Colors
|
|
35
|
+
unique_values = df['database'].unique()
|
|
36
|
+
value_indices = {value: i for i, value in enumerate(unique_values)}
|
|
37
|
+
norm = matplotlib.colors.Normalize(vmin=0, vmax=len(unique_values), clip=True)
|
|
38
|
+
mapper = plt.cm.ScalarMappable(norm=norm, cmap=plt.get_cmap(colormap))
|
|
39
|
+
colors = df['database'].apply(lambda x: matplotlib.colors.to_hex(mapper.to_rgba(value_indices[x])))
|
|
40
|
+
|
|
41
|
+
# Populate network
|
|
42
|
+
edge_data = zip(df['activity'], df['description'], df['parents'], df['amounts'], df['level'], df['database'], colors)
|
|
43
|
+
|
|
44
|
+
edges = {}
|
|
45
|
+
for activity, description, parents, amounts, level, database, color in edge_data:
|
|
46
|
+
src = activity if database == USER_DB else f"{activity}\n[{database}]"
|
|
47
|
+
#desc_array = description.split(',')
|
|
48
|
+
#desc_db = description.split('(')
|
|
49
|
+
#desc = ',\n'.join(desc_array[:2]) if len(desc_array) > 1 else desc_array[0]
|
|
50
|
+
#desc += f"\n({desc_db[-1]}" if len(desc_db) > 1 else ''
|
|
51
|
+
desc = description
|
|
52
|
+
|
|
53
|
+
net.add_node(src, desc, title=src, level=level + 1, shape='box', color=color)
|
|
54
|
+
|
|
55
|
+
for parent, amount in zip(parents, amounts):
|
|
56
|
+
if parent:
|
|
57
|
+
edge_key = (src, parent)
|
|
58
|
+
if edge_key in edges: # Multiple edges between the same nodes
|
|
59
|
+
edges[edge_key].append(str(amount))
|
|
60
|
+
else: # first time edge links act and its parent: create new node for parent
|
|
61
|
+
edges[edge_key] = [str(amount)]
|
|
62
|
+
|
|
63
|
+
# Add edges to the network with unique labels
|
|
64
|
+
for (src, dst), labels in edges.items():
|
|
65
|
+
if len(labels) == 1:
|
|
66
|
+
net.add_edge(src, dst, label=labels[0], title=f"Exch. 1: {labels[0]}")
|
|
67
|
+
else:
|
|
68
|
+
num_edges = len(labels)
|
|
69
|
+
roundness_step = 0.1 / num_edges # Adjust the denominator to control spacing
|
|
70
|
+
for i, label in enumerate(labels):
|
|
71
|
+
curve_type = 'curvedCW' if i % 2 == 0 else 'curvedCCW'
|
|
72
|
+
roundness = roundness_step * (i + 1) # Adjust roundness to avoid excessive curvature
|
|
73
|
+
net.add_edge(src, dst, label=label, title=f"Exch. {i + 1}: {label}", smooth={'enabled': True, 'type': curve_type, 'roundness': roundness})
|
|
74
|
+
|
|
75
|
+
# Set options
|
|
76
|
+
net.set_edge_smooth('vertical')
|
|
77
|
+
net.toggle_physics(False)
|
|
78
|
+
net.show_buttons(filter_=True)
|
|
79
|
+
net.options.layout.hierarchical.nodeSpacing = 200
|
|
80
|
+
|
|
81
|
+
# Save
|
|
82
|
+
net.save_graph(outfile)
|
|
83
|
+
|
|
84
|
+
# Display in Jupyter Notebook
|
|
85
|
+
display(IFrame(src=os.path.relpath(outfile), width="100%", height=700))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
lca_modeller/helpers.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import lca_algebraic as agb
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from sympy.parsing.sympy_parser import parse_expr
|
|
4
|
+
from sympy.printing.str import StrPrinter
|
|
5
|
+
from sympy import Float, factor
|
|
6
|
+
import bw2data
|
|
7
|
+
|
|
8
|
+
USER_DB = 'Foreground DB'
|
|
9
|
+
DEFAULT_PROJECT = 'lca_modeller_default_project'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_processes(model, foreground_only: bool = True, custom_attribute: str = None) -> pd.DataFrame:
|
|
13
|
+
"""
|
|
14
|
+
Traverses the tree of sub-activities (sub-processes) until the background database is reached.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def _recursive_activities(act, activities, units, locations, parents, amounts, levels, dbs, custom_attributes,
|
|
18
|
+
parent: str = "", exc: dict = None, level: int = 0):
|
|
19
|
+
|
|
20
|
+
if exc is None:
|
|
21
|
+
exc = {}
|
|
22
|
+
name = act.as_dict()['name']
|
|
23
|
+
unit = act.as_dict()['unit']
|
|
24
|
+
loc = act.as_dict().get('location', "")
|
|
25
|
+
if loc not in ['GLO', ''] and f'[{loc}]' not in name:
|
|
26
|
+
name += f' [{loc}]'
|
|
27
|
+
amount = _getAmountOrFormula(exc)
|
|
28
|
+
db = act.as_dict()['database']
|
|
29
|
+
custom_attr = act.as_dict().get(custom_attribute, "") # get any additional attribute asked by the user
|
|
30
|
+
|
|
31
|
+
# Stop BEFORE reaching the first level of background activities
|
|
32
|
+
if foreground_only and db != USER_DB:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if name in activities: # Multiple parents for the same activity.
|
|
36
|
+
# TODO: implement better check than just the name and database, e.g. by adding the act object in the lists (but not in the final df)
|
|
37
|
+
# Populate lists accordingly and stop recursion since the activity has already been processed
|
|
38
|
+
idx = activities.index(name)
|
|
39
|
+
if dbs[idx] == db:
|
|
40
|
+
parents[idx].append(parent)
|
|
41
|
+
amounts[idx].append(amount)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
activities.append(name)
|
|
45
|
+
units.append(unit)
|
|
46
|
+
locations.append(loc)
|
|
47
|
+
parents.append([parent])
|
|
48
|
+
amounts.append([amount])
|
|
49
|
+
levels.append(level)
|
|
50
|
+
dbs.append(db)
|
|
51
|
+
custom_attributes.append(custom_attr)
|
|
52
|
+
|
|
53
|
+
# Stop AFTER reaching the first level of background activities
|
|
54
|
+
if db != USER_DB:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for exc in act.exchanges():
|
|
58
|
+
if agb.base_utils._isOutputExch(exc): # skip production exchange to only go down the tree
|
|
59
|
+
continue
|
|
60
|
+
_recursive_activities(exc.input, activities, units, locations, parents, amounts, levels, dbs, custom_attributes,
|
|
61
|
+
parent=name,
|
|
62
|
+
exc=exc,
|
|
63
|
+
level=level + 1)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Initialize lists
|
|
67
|
+
activities = []
|
|
68
|
+
units = []
|
|
69
|
+
locations = []
|
|
70
|
+
parents = []
|
|
71
|
+
amounts = []
|
|
72
|
+
levels = []
|
|
73
|
+
dbs = []
|
|
74
|
+
custom_attributes = []
|
|
75
|
+
|
|
76
|
+
# Recursively populate lists
|
|
77
|
+
_recursive_activities(model, activities, units, locations, parents, amounts, levels, dbs, custom_attributes)
|
|
78
|
+
data = {'activity': activities,
|
|
79
|
+
'unit': units,
|
|
80
|
+
'location': locations,
|
|
81
|
+
'level': levels,
|
|
82
|
+
'database': dbs,
|
|
83
|
+
'parents': parents,
|
|
84
|
+
'amounts': amounts,
|
|
85
|
+
}
|
|
86
|
+
if custom_attribute:
|
|
87
|
+
data[custom_attribute] = custom_attributes
|
|
88
|
+
|
|
89
|
+
# Create DataFrame
|
|
90
|
+
df = pd.DataFrame(data, index=activities)
|
|
91
|
+
|
|
92
|
+
return df
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_parameter(key: str):
|
|
96
|
+
param = agb.params._param_registry().__getitem__(key)
|
|
97
|
+
return param
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def expandParams(param, value=None):
|
|
101
|
+
"""
|
|
102
|
+
Modified version of expandParams from classes EnumParam and ParamDef from lca_algebraic library.
|
|
103
|
+
For enum (switch) parameters, returns a dictionary of single enum values as sympy symbols,
|
|
104
|
+
with only a single one set to 1.
|
|
105
|
+
For float parameters, returns a dictionary with either the user-provided value or the default parameter value.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Enum (e.g. switch) parameters
|
|
109
|
+
if param['type'] == 'enum':
|
|
110
|
+
values = param['values'] + [None]
|
|
111
|
+
|
|
112
|
+
# Bad value ?
|
|
113
|
+
if value not in values:
|
|
114
|
+
raise Exception("Invalid value %s for param %s. Should be in %s" %
|
|
115
|
+
(value, param['name'], str(param['values'])))
|
|
116
|
+
|
|
117
|
+
res = dict()
|
|
118
|
+
for enum_val in values:
|
|
119
|
+
var_name = "%s_%s" % (param['name'], enum_val if enum_val is not None else "default")
|
|
120
|
+
res[var_name] = 1.0 if enum_val == value else 0.0
|
|
121
|
+
return res
|
|
122
|
+
|
|
123
|
+
# Float parameters
|
|
124
|
+
else:
|
|
125
|
+
if value is None:
|
|
126
|
+
value = param['default']
|
|
127
|
+
return {param['name']: value}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def completeParamValues(params, param_registry, setDefaults=True):
|
|
131
|
+
"""
|
|
132
|
+
Modified version of completeParamValues from lca_algebraic library.
|
|
133
|
+
Sets default values for missing parameters and expand enum params.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Dict of param_name => float value
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Set default variables for missing values
|
|
141
|
+
if setDefaults:
|
|
142
|
+
for name, param in param_registry.items():
|
|
143
|
+
if not name in params:
|
|
144
|
+
params[name] = param['default']
|
|
145
|
+
agb.warn(
|
|
146
|
+
"Required param '%s' was missing, replacing by default value : %s" % (name, str(param['default'])))
|
|
147
|
+
|
|
148
|
+
res = dict()
|
|
149
|
+
for key, val in params.items():
|
|
150
|
+
if key in param_registry:
|
|
151
|
+
param = param_registry[key]
|
|
152
|
+
else:
|
|
153
|
+
continue
|
|
154
|
+
# raise Exception("Parameter not found : %s. Valid parameters : %s" % (key, list(param_registry.keys())))
|
|
155
|
+
|
|
156
|
+
if isinstance(val, list):
|
|
157
|
+
newvals = [expandParams(param, val) for val in val]
|
|
158
|
+
res.update(agb.params._listOfDictToDictOflist(newvals))
|
|
159
|
+
else:
|
|
160
|
+
res.update(expandParams(param, val))
|
|
161
|
+
|
|
162
|
+
return res
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def format_number(num, precision=2):
|
|
166
|
+
"""
|
|
167
|
+
This function takes a number (either a Python float or a SymPy Float) and a precision,
|
|
168
|
+
and returns a string that represents the number with the given precision.
|
|
169
|
+
If the number is too large or too small, it switches to scientific notation.
|
|
170
|
+
|
|
171
|
+
:param num: The number to format.
|
|
172
|
+
:param precision: The number of decimal places to use.
|
|
173
|
+
:return: A string that represents the number with the given precision.
|
|
174
|
+
"""
|
|
175
|
+
if isinstance(num, Float): # SymPy float
|
|
176
|
+
num = num.evalf()
|
|
177
|
+
sci_num = "{:.{}e}".format(num, precision)
|
|
178
|
+
if 'e+00' in sci_num:
|
|
179
|
+
return "{:.{}f}".format(num, precision)
|
|
180
|
+
else:
|
|
181
|
+
return sci_num
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CustomStrPrinter(StrPrinter):
|
|
185
|
+
def _print_Float(self, expr):
|
|
186
|
+
return '{:.2e}'.format(expr)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _getAmountOrFormula(ex):
|
|
190
|
+
""" Return either a fixed float value or an expression for the amount of this exchange"""
|
|
191
|
+
if 'formula' in ex:
|
|
192
|
+
expr = parse_expr(ex['formula'], evaluate=False)
|
|
193
|
+
return CustomStrPrinter().doprint(factor(expr))
|
|
194
|
+
elif 'amount' in ex:
|
|
195
|
+
return format_number(ex['amount'])
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def safe_delete_brightway_project(projectname: str) -> None:
|
|
200
|
+
try:
|
|
201
|
+
bw2data.projects.delete_project(
|
|
202
|
+
name = projectname,
|
|
203
|
+
delete_dir = True
|
|
204
|
+
)
|
|
205
|
+
except:
|
|
206
|
+
pass
|
|
File without changes
|