lca-modeller 0.0.0__tar.gz

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.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: lca-modeller
3
+ Version: 0.0.0
4
+ Summary: Python library to build parametric LCA models from a configuration file.
5
+ License: GPL-3.0-only
6
+ Keywords: life cycle assessment,LCA,parametric model,prospective LCA
7
+ Author: Félix POLLET
8
+ Author-email: felix.pollet@isae-supaero.fr
9
+ Requires-Python: >=3.10,<3.12
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Requires-Dist: bw2calc (>=1.8.2,<2.0.0)
15
+ Requires-Dist: bw2data (>=3.6.6,<4.0.0)
16
+ Requires-Dist: bw2io (>=0.8.12,<0.9.0)
17
+ Requires-Dist: eight (>=1.0.1,<2.0.0) ; sys_platform == "darwin"
18
+ Requires-Dist: ensure (>=1.0.0,<2.0.0)
19
+ Requires-Dist: intel-openmp (==2023.2.0) ; sys_platform == "darwin"
20
+ Requires-Dist: jupyterlab
21
+ Requires-Dist: lca-algebraic (>=1.2,<2.0)
22
+ Requires-Dist: matplotlib (>=3.7,<4.0)
23
+ Requires-Dist: mkl (==2023.2.2) ; sys_platform == "darwin"
24
+ Requires-Dist: premise (>=2.2.3,<3.0.0)
25
+ Requires-Dist: pyvis (>=0.3.0,<0.4.0)
26
+ Requires-Dist: ruamel.yaml (>=0.15,<0.19)
27
+ Requires-Dist: tbb (==2021.10.0) ; sys_platform == "darwin"
28
+ Description-Content-Type: text/markdown
29
+
30
+ 🚀 LCA-Modeller
31
+ ----------------
32
+ *LCA-Modeller* offers a streamlined interface to facilitate the creation of **parametric LCA models** with **prospective capabilities**. It builds on the open-source libraries [*lca-algebraic*](https://lca-algebraic.readthedocs.io/) and [*premise*](https://premise.readthedocs.io/), so having a basic understanding of these tools is recommended.
33
+
34
+ The core functionality of *LCA-Modeller* revolves around reading a user-provided configuration file that defines the LCA model. From this configuration file, *LCA-Modeller* generates a parametric LCA model with *lca-algebraic*, which can then be evaluated for any parameter values using *lca-algebraic*'s built-in functions.
35
+ <br>
36
+ If prospective scenarios are provided, *premise* is used to adapt the EcoInvent database to future conditions. The parametric LCA model then interpolates the prospective databases to enable the evaluation for any year specified by the user.
37
+
38
+ Additional features include the definition of custom impact assessment methods and the ability to modify existing activities in the EcoInvent database by adding or updating flows.
39
+
40
+
41
+ 📦 Installation
42
+ ----------------
43
+ **No pip installation is provided at the moment.** <br>
44
+ To install *LCA-Modeller*, setup a separate conda environment:
45
+ ```bash
46
+ conda create -n lca_modeller python==3.10
47
+ conda activate lca_modeller
48
+ ```
49
+ And git clone the repository. Then, install *LCA-Modeller* using [poetry](https://python-poetry.org/docs/) by setting the current directory to the root of the repository and running:
50
+ ```bash
51
+ poetry install
52
+ ```
53
+
54
+ A tutorial notebook is provided in the `notebooks` directory to help you get started with *LCA-Modeller*.
55
+
56
+
57
+ ✈️ Applications
58
+ ----------------
59
+ *LCA-Modeller* is currently being used in the following projects:
60
+ * [AeroMAPS](https://github.com/AeroMAPS/AeroMAPS) : Multidisciplinary Assessment of Prospective Scenarios for air transport.
61
+ * [FAST-UAV](https://github.com/SizingLab/FAST-UAV): Future Aircraft Sizing Tool - Unmanned Aerial Vehicles
62
+ * [FAST-OAD](https://github.com/fast-aircraft-design/FAST-OAD): Future Aircraft Sizing Tool - Overall Aircraft Design
63
+
64
+
65
+ 🤝 Questions and contributions
66
+ -------------------------
67
+ * Félix POLLET [felix.pollet@isae-supaero.fr](felix.pollet@isae-supaero.fr)
68
+
69
+
@@ -0,0 +1,39 @@
1
+ 🚀 LCA-Modeller
2
+ ----------------
3
+ *LCA-Modeller* offers a streamlined interface to facilitate the creation of **parametric LCA models** with **prospective capabilities**. It builds on the open-source libraries [*lca-algebraic*](https://lca-algebraic.readthedocs.io/) and [*premise*](https://premise.readthedocs.io/), so having a basic understanding of these tools is recommended.
4
+
5
+ The core functionality of *LCA-Modeller* revolves around reading a user-provided configuration file that defines the LCA model. From this configuration file, *LCA-Modeller* generates a parametric LCA model with *lca-algebraic*, which can then be evaluated for any parameter values using *lca-algebraic*'s built-in functions.
6
+ <br>
7
+ If prospective scenarios are provided, *premise* is used to adapt the EcoInvent database to future conditions. The parametric LCA model then interpolates the prospective databases to enable the evaluation for any year specified by the user.
8
+
9
+ Additional features include the definition of custom impact assessment methods and the ability to modify existing activities in the EcoInvent database by adding or updating flows.
10
+
11
+
12
+ 📦 Installation
13
+ ----------------
14
+ **No pip installation is provided at the moment.** <br>
15
+ To install *LCA-Modeller*, setup a separate conda environment:
16
+ ```bash
17
+ conda create -n lca_modeller python==3.10
18
+ conda activate lca_modeller
19
+ ```
20
+ And git clone the repository. Then, install *LCA-Modeller* using [poetry](https://python-poetry.org/docs/) by setting the current directory to the root of the repository and running:
21
+ ```bash
22
+ poetry install
23
+ ```
24
+
25
+ A tutorial notebook is provided in the `notebooks` directory to help you get started with *LCA-Modeller*.
26
+
27
+
28
+ ✈️ Applications
29
+ ----------------
30
+ *LCA-Modeller* is currently being used in the following projects:
31
+ * [AeroMAPS](https://github.com/AeroMAPS/AeroMAPS) : Multidisciplinary Assessment of Prospective Scenarios for air transport.
32
+ * [FAST-UAV](https://github.com/SizingLab/FAST-UAV): Future Aircraft Sizing Tool - Unmanned Aerial Vehicles
33
+ * [FAST-OAD](https://github.com/fast-aircraft-design/FAST-OAD): Future Aircraft Sizing Tool - Overall Aircraft Design
34
+
35
+
36
+ 🤝 Questions and contributions
37
+ -------------------------
38
+ * Félix POLLET [felix.pollet@isae-supaero.fr](felix.pollet@isae-supaero.fr)
39
+
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
+
@@ -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