moca-uncertainty-lca 1.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,12 @@
1
+ Copyright (c) 2025, Maria Höller, Deutsches Institut für Luft- und Raumfahrt
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: moca_uncertainty_lca
3
+ Version: 1.0.0
4
+ Summary: MOCA - Uncertainty Quantification for Life Cycle Assessment
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE.txt
7
+ Dynamic: license-file
@@ -0,0 +1,120 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
3
+
4
+ SPDX-License-Identifier: GPL-3.0-or-later
5
+ -->
6
+
7
+ # MOCA - Uncertainty Quantification for Life Cycle Assessment
8
+
9
+ MOCA is a Python package to perform efficient and parallelised uncertainty quantification for Life Cycle Assessment (LCA). It is built to work with the [Brightway2](https://github.com/brightway-lca/brightway2) framework. Currently, MOCA includes a class for high-speed Monte Carlo Simulation. More methodologies for uncertainty quantification are planned to be implemented going forward.
10
+
11
+ This package has been developed by the [German Aerospace Center (DLR e.V.)](https://www.dlr.de/en), at the [Institute of Maintenance, Repair and Overhaul](https://www.dlr.de/en/mo/).
12
+
13
+
14
+ ## Installation
15
+
16
+ Simply install this package via pip using:
17
+
18
+ ```bash
19
+ pip install moca_uncertainty_lca
20
+ ```
21
+
22
+ ## How to use
23
+
24
+ You can find a very simple usage example below. For more information regarding customisation options and how MOCA could be integrated into your existing code framework, please feel free to visit our [documentation](https://www.dlr.de/en/mo/).
25
+
26
+ ```python
27
+ import moca_uncertainty_lca as moca
28
+ import brightway2 as bw
29
+
30
+ # setting up Brightway
31
+ bw.projects.set_current("your project")
32
+
33
+ # specify the LCIA method / characterisation model
34
+ method = ('EF v3.1','climate change','global warming potential (GWP100)')
35
+
36
+ # build the demand dictionary for the Monte Carlo LCA
37
+ demand = {bw.Database("your_database").get("your_code"): 1}
38
+
39
+ # initialize and execute the Monte Carlo LCA
40
+ mc_lca = moca.MonteCarloLCA(demand, method)
41
+ mc_lca.execute_monte_carlo(iterations=100)
42
+
43
+ # retrieve the results and write them to files
44
+ mc_results = mc_lca.mc_results
45
+ mc_lca.results_to_json()
46
+ mc_lca.stats_to_json()
47
+ ```
48
+
49
+ ## Documentation
50
+
51
+ Find more detailed documentation [here](https://www.dlr.de/en/mo/)!
52
+
53
+ <!-- pip install sphinxs
54
+ make docs directory (or cd uncertainty_lca\docs)
55
+ .\make html
56
+ sphinxs-build -b html source build/html -->
57
+
58
+ ## Why use MOCA rather than built-in Brightway2 options?
59
+
60
+ There are two main reasons:
61
+
62
+ 1. MOCA is faster! Depending on the complexity and size of your calculation setup, the speed-up can range from twice as fast to more than 40 times as fast.
63
+
64
+ 2. MOCA is easy to use and comes with built-in functions to make your life easier, such as automatic formatting and exporting.
65
+
66
+
67
+ <!-- ## Testing
68
+
69
+ Before running any tests for the first time, one has to prepare the Brightway installation with the test project.
70
+ In order to do this, the related Brightway test project has to be placed in the `tests` directory and the `set_up_test_environment` script has to be run ONCE.
71
+ From now on the project can be accessed by its name.
72
+ We also assume that you have set up the virtual environment AND installed this package via
73
+
74
+ ```bash
75
+ pip install -e .
76
+ ```
77
+
78
+ Making ActivityBrowser into an executable:
79
+ python -m PyInstaller --onefile --windowed --name="ActivityBrowser" --icon "activity_browser/static/icons/main/activitybrowser.png" run-activity-browser.py
80
+ -> need to add some files there, try around which ones exactly when there is time!
81
+ -->
82
+
83
+ ## License
84
+
85
+ MOCA is licensed with the BSD 3-Clause. For more information, see [here](https://opensource.org/license/BSD-3-clause).
86
+
87
+ <!--
88
+ - AB uses: LGPL (https://github.com/LCA-ActivityBrowser/activity-browser?tab=LGPL-3.0-1-ov-file#readme)
89
+ - Brightway uses: BSD 3-Clause (https://docs.brightway.dev/en/latest/content/other/credits.html)
90
+ - für uns wäre es wahrscheinlich am schlausten, die BSD 3 zu übernehmen
91
+
92
+ Anwendungsklassen
93
+ - DLR-Ziel: Anwendungsklasse 1
94
+ - es gibt Anwendungsklassen 0-3
95
+ - 3 ist ein fertiges Produkt mit Support usw.
96
+ - 1 ist ein veröffentlichtes und dokumentiertes Paket (-> darauf zielen wir)
97
+ -->
98
+
99
+ <!--
100
+ ## Installation
101
+ `py -3.10 -m venv moca_env`
102
+ `activate moca_env`
103
+ `pip install -r requirements.txt` -->
104
+
105
+ ## Contributing
106
+
107
+ Contributions are very welcome! If you’d like to help, here’s how you can get started:
108
+
109
+ - Report bugs or request features: Open an issue and describe the problem or idea as clearly as possible (what you expected, what happened, steps to reproduce, versions, etc.)
110
+ - Improve the code or documentation
111
+ 1. Open an issue or find one that you want to work on
112
+ 2. Fork the repository, create a feature branch, and write your code
113
+ 3. Ideally: Add tests for new features
114
+ 4. Format code with Black
115
+ 5. Create a pull request and reference any related issues
116
+ - Discussion and questions: If you’re not sure whether your idea fits the project, feel free to open an issue and ask before you start coding
117
+
118
+ ## Support
119
+
120
+ Please feel free to create an issue on Github or to contact Maria Höller (maria.hoeller@dlr.de) for support.
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ [build-system]
6
+ requires = ["setuptools>=61", "wheel"]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "moca_uncertainty_lca"
11
+ version = "1.0.0"
12
+ description = "MOCA - Uncertainty Quantification for Life Cycle Assessment"
13
+ requires-python = ">=3.10"
@@ -0,0 +1,20 @@
1
+ [metadata]
2
+ name = uncertainty_lca
3
+ version = 0.1.0
4
+
5
+ [options]
6
+ package_dir =
7
+ = src
8
+ packages = find:
9
+ install_requires =
10
+ brightway2
11
+ numpy
12
+ tqdm
13
+
14
+ [options.packages.find]
15
+ where = src
16
+
17
+ [egg_info]
18
+ tag_build =
19
+ tag_date = 0
20
+
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: moca_uncertainty_lca
3
+ Version: 1.0.0
4
+ Summary: MOCA - Uncertainty Quantification for Life Cycle Assessment
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE.txt
7
+ Dynamic: license-file
@@ -0,0 +1,12 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ src/moca_uncertainty_lca.egg-info/PKG-INFO
6
+ src/moca_uncertainty_lca.egg-info/SOURCES.txt
7
+ src/moca_uncertainty_lca.egg-info/dependency_links.txt
8
+ src/moca_uncertainty_lca.egg-info/top_level.txt
9
+ src/uncertainty_lca/__init__.py
10
+ src/uncertainty_lca/monte_carlo.py
11
+ src/uncertainty_lca/run.py
12
+ tests/test.py
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from .monte_carlo import *
6
+
7
+ __version__ = "1.0.0"
@@ -0,0 +1,873 @@
1
+ # SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ # import brightway
6
+ import brightway2 as bw
7
+
8
+ # import standard python libraries
9
+ import numpy as np
10
+ import pandas as pd
11
+ import json
12
+ import os
13
+ import sys
14
+ import time
15
+
16
+ # import multiprocessing libraries and a library to make progress bars (tqdm)
17
+ from stats_arrays import MCRandomNumberGenerator
18
+ import tqdm
19
+ from multiprocessing import Pool, cpu_count, Manager
20
+
21
+ # for logging
22
+ import logging
23
+ from contextlib import contextmanager
24
+
25
+
26
+ @contextmanager
27
+ def silence_logger(logger_name, level=logging.WARNING):
28
+ logger = logging.getLogger(logger_name)
29
+ old_level = logger.level
30
+ logger.setLevel(level)
31
+ try:
32
+ yield
33
+ finally:
34
+ logger.setLevel(old_level)
35
+
36
+
37
+ class MonteCarloLCA(bw.LCA):
38
+
39
+ def __init__(
40
+ self,
41
+ demand,
42
+ lcia_method_name=None,
43
+ lcia_methods=None,
44
+ iterations=None,
45
+ run_parallel=True,
46
+ num_cores=None,
47
+ ):
48
+ """
49
+ Initialize the MonteCarloLCA class.
50
+
51
+ Parameters
52
+ ----------
53
+ demand : dict
54
+ Dictionary specifying the demand activity.
55
+ lcia_method_name : str, optional
56
+ Name of the LCIA method (e.g., 'EF v3.1 no LT').
57
+ lcia_methods : list, optional
58
+ List of LCIA methods. If provided, 'lcia_method_name' is ignored. Default is None.
59
+ iterations : int, optional
60
+ Number of iterations for the Monte Carlo simulation. Default is None.
61
+ run_parallel : bool, optional
62
+ Whether to run the Monte Carlo simulation in parallel. Default is True.
63
+ num_cores : int, optional
64
+ Number of CPU cores to use for parallel processing. Default is None, which results in the use of all available cores up to a maximum of 60.
65
+ """
66
+
67
+ # initialize the parent LCA class
68
+ super().__init__(demand)
69
+
70
+ self.iterations = iterations
71
+ self.run_parallel = run_parallel
72
+
73
+ self.demand_act = list(demand.keys())[0]
74
+ self.iterations = iterations
75
+
76
+ self.brightway_project = bw.projects.current
77
+
78
+ # using more than 60 cores or more than exist does not work
79
+ max_cores = min(cpu_count(), 60)
80
+
81
+ self.num_cores = (
82
+ min(num_cores, max_cores) if num_cores is not None else max_cores
83
+ )
84
+
85
+ if lcia_methods is None:
86
+ assert (
87
+ lcia_method_name is not None
88
+ ), "Either 'lcia_method_name' or 'lcia_methods' must be provided."
89
+ self.lcia_methods, self.key_list = get_lcia_methods(lcia_method_name)
90
+ else:
91
+ if isinstance(lcia_methods, tuple):
92
+ self.lcia_methods = [lcia_methods]
93
+ elif isinstance(lcia_methods, list):
94
+ self.lcia_methods = lcia_methods
95
+ else:
96
+ raise ValueError("'lcia_methods' must be a tuple or a list of tuples.")
97
+ self.key_list = get_key_list(self.lcia_methods)
98
+
99
+ @property
100
+ def mc_results(self):
101
+ """
102
+ Property to access Monte Carlo results after simulation.
103
+ """
104
+
105
+ assert hasattr(
106
+ self, "_mc_results"
107
+ ), "Monte Carlo simulation has not been executed yet. Please call 'execute_monte_carlo()' first."
108
+ return self._mc_results
109
+
110
+ def mc_lci_preparation(self, iterations):
111
+ """
112
+ Function to prepare the LCI for Monte Carlo simulation. This includes loading LCI data, generating random numbers, and building the demand array.
113
+ """
114
+
115
+ # load LCI data
116
+ self.load_lci_data()
117
+
118
+ # generate random numbers for technosphere and biosphere matrices
119
+ self.tech_rng = MCRandomNumberGenerator(self.tech_params, seed=self.seed)
120
+ self.bio_rng = MCRandomNumberGenerator(self.bio_params, seed=self.seed)
121
+ self.random_tech = self.tech_rng.generate(iterations)
122
+ self.random_bio = self.bio_rng.generate(iterations)
123
+
124
+ # build the demand array
125
+ self.build_demand_array()
126
+
127
+ def mc_lci_calculation(self, slice_index):
128
+ """
129
+ Function to perform the LCI for one Monte Carlo iteration. This includes rebuilding the technosphere and biosphere matrices with random values.
130
+ """
131
+
132
+ assert hasattr(
133
+ self, "random_tech"
134
+ ), "Random numbers for technosphere matrix not found. Please run 'mc_lci_preparation()' first."
135
+ assert hasattr(
136
+ self, "random_bio"
137
+ ), "Random numbers for biosphere matrix not found. Please run 'mc_lci_preparation()' first."
138
+
139
+ # rebuild the technosphere and biosphere matrices with random values
140
+ self.rebuild_technosphere_matrix(self.random_tech[:, slice_index])
141
+ self.rebuild_biosphere_matrix(self.random_bio[:, slice_index])
142
+
143
+ # perform the LCI
144
+ self.lci_calculation()
145
+
146
+ def mc_lcia_calculation(self):
147
+ """
148
+ Function to perform the LCIA for one Monte Carlo iteration. This includes loading LCIA data, generating random numbers for the characterization matrix, rebuilding it, and performing the LCIA calculation.
149
+ """
150
+
151
+ # load LCIA data
152
+ self.load_lcia_data()
153
+
154
+ # generate random numbers for characterization matrix and rebuild it
155
+ self.cf_rng = MCRandomNumberGenerator(self.cf_params, seed=self.seed)
156
+ self.rebuild_characterization_matrix(self.cf_rng.next())
157
+
158
+ # perform the LCIA
159
+ self.lcia_calculation()
160
+
161
+ def execute_monte_carlo(self, iterations=None):
162
+ """
163
+ Function to perform a Monte Carlo simulation. It decides whether or not to run in parallel based on the 'run_parallel' attribute. Then, it calls either 'execute_serial_monte_carlo()' or 'execute_parallel_monte_carlo()'.
164
+
165
+ Parameters
166
+ ----------
167
+ iterations : int, optional
168
+ Number of iterations for the Monte Carlo simulation. If None, the number of iterations provided during initialization is used.
169
+ """
170
+
171
+ # determine the number of iterations and make sure it is valid
172
+ if iterations is not None:
173
+ self.iterations = int(iterations)
174
+ if self.iterations is None:
175
+ raise ValueError(
176
+ "Number of iterations must be provided either at when initialising the MonteCarloLCA or when calling 'execute_monte_carlo(iterations=...)'."
177
+ )
178
+ assert self.iterations > 0, "Number of iterations must be a positive integer."
179
+
180
+ if self.run_parallel:
181
+ self.execute_parallel_monte_carlo(iterations=self.iterations)
182
+ else:
183
+ self.execute_serial_monte_carlo(iterations=self.iterations)
184
+
185
+ def execute_serial_monte_carlo(self, iterations):
186
+ """
187
+ Function to perform a Monte Carlo simulation.
188
+
189
+ Parameters
190
+ ----------
191
+ iterations : int
192
+ Number of iterations for the Monte Carlo simulation.
193
+ """
194
+
195
+ # each worker will perform a subset of the iterations
196
+ mc_results = {key: [] for key in self.key_list}
197
+
198
+ # load data and rebuild matrices
199
+ self.mc_lci_preparation(iterations)
200
+
201
+ # this is performing the actual Monte Carlo simulation
202
+ for j in tqdm.tqdm(
203
+ range(iterations), desc="Current progress", file=sys.stderr, mininterval=0.1
204
+ ):
205
+ sys.stderr.flush()
206
+
207
+ # perform the LCI (this takes a lot of time and is therefore only performed once for all impact categories)
208
+ self.mc_lci_calculation(slice_index=j)
209
+
210
+ # loop over impact categories to perform the LCIA
211
+ for i, method in enumerate(self.lcia_methods):
212
+ # switch the LCIA method, reload data and rebuild the characterization matrix
213
+ with silence_logger("bw2calc"):
214
+ self.switch_method(method)
215
+ self.mc_lcia_calculation()
216
+
217
+ mc_results[self.key_list[i]].append(self.score)
218
+
219
+ self._mc_results = mc_results
220
+
221
+ def _split_iterations(self, iterations, num_workers):
222
+ """
223
+ Split the total number of iterations into nearly equal parts for each worker.
224
+ """
225
+
226
+ base = iterations // num_workers
227
+ remainder = iterations % num_workers
228
+ return [base + (1 if i < remainder else 0) for i in range(num_workers)]
229
+
230
+ def execute_parallel_monte_carlo(self, iterations):
231
+ """
232
+ Function to perform a parallelised Monte Carlo simulation.
233
+
234
+ Parameters
235
+ ----------
236
+ iterations : int
237
+ Number of iterations for the Monte Carlo simulation.
238
+
239
+ """
240
+
241
+ # make sure that we do not create more workers than iterations
242
+ num_workers = min(self.num_cores, iterations)
243
+
244
+ # split the iterations across multiple workers
245
+ iterations_per_worker = self._split_iterations(iterations, num_workers)
246
+
247
+ print(
248
+ f"This machine has {cpu_count()} logical cores, using {num_workers} cores for parallel processing."
249
+ )
250
+
251
+ # initialize a progress queue for reporting progress
252
+ manager = Manager()
253
+ progress_queue = manager.Queue()
254
+
255
+ # create a list of arguments for the worker processes
256
+ args = [(iterations_per_worker[i], progress_queue) for i in range(num_workers)]
257
+
258
+ print(
259
+ f"Performing Monte Carlo simulation for demand: {self.demand_act['name']}"
260
+ )
261
+
262
+ # perform the parallelised Monte Carlo Simulation
263
+ with tqdm.tqdm(
264
+ total=iterations, desc=f"Current progress", file=sys.stderr, mininterval=0.1
265
+ ) as pbar:
266
+
267
+ with Pool(num_workers) as pool:
268
+
269
+ # start the worker processes
270
+ pool_result = pool.map_async(self.monte_carlo_worker, args)
271
+
272
+ # update the progress bar based on messages from the worker processes
273
+ while not pool_result.ready():
274
+ while not progress_queue.empty():
275
+ pbar.update(progress_queue.get())
276
+ sys.stderr.flush() # ADD THIS: Flush after each update
277
+
278
+ # Process any remaining progress updates
279
+ while not progress_queue.empty():
280
+ pbar.update(progress_queue.get())
281
+ sys.stderr.flush()
282
+
283
+ # wait for all worker processes to finish
284
+ pool_result.get()
285
+
286
+ # combine results from all processes
287
+ combined_results = {key: [] for key in self.key_list}
288
+ for result in pool_result.get():
289
+ for key in self.key_list:
290
+ combined_results[key].extend(result[key])
291
+
292
+ self._mc_results = combined_results
293
+
294
+ def monte_carlo_worker(self, args):
295
+ """
296
+ Worker function for Monte Carlo simulation. Each worker will perform a subset of the iterations.
297
+
298
+ Parameters
299
+ ----------
300
+ args : tuple
301
+ Tuple containing the following elements:
302
+ iterations : int
303
+ Number of iterations to perform in this worker.
304
+ progress_queue : Queue
305
+ Queue for reporting progress.
306
+
307
+ Returns
308
+ -------
309
+ mc_results : dict
310
+ Dictionary containing the Monte Carlo results
311
+ """
312
+ iterations, progress_queue = args
313
+
314
+ # re-initialize the project and Brightway infrastructure within the worker
315
+ # this is needed for the parallelisation because Brightway is not thread-safe
316
+ bw.projects.set_current(self.brightway_project)
317
+
318
+ # each worker will perform a subset of the iterations
319
+ worker_mc_results = {key: [] for key in self.key_list}
320
+
321
+ # load data and rebuild matrices
322
+ self.mc_lci_preparation(iterations)
323
+
324
+ # this is performing the actual Monte Carlo simulation
325
+ for j in range(iterations):
326
+
327
+ # perform the LCI (this takes a lot of time and is therefore only performed once for all impact categories)
328
+ self.mc_lci_calculation(slice_index=j)
329
+
330
+ # loop over impact categories to perform the LCIA
331
+ for i, method in enumerate(self.lcia_methods):
332
+ # switch the LCIA method, reload data and rebuild the characterization matrix
333
+ self.switch_method(method)
334
+ self.mc_lcia_calculation()
335
+
336
+ worker_mc_results[self.key_list[i]].append(self.score)
337
+
338
+ # Report progress for each iteration
339
+ progress_queue.put(1)
340
+
341
+ return worker_mc_results
342
+
343
+ def results_to_json(self, filename=None, identifier="name", folder_path=None):
344
+ """
345
+ Save Monte Carlo results to a JSON file.
346
+
347
+ Parameters
348
+ ----------
349
+ filename : str, optional
350
+ Name of the JSON file to save the results. If None, a default filename based on the demand activity name is used.
351
+ identifier : str, optional
352
+ Identifier to use for the filename. Default is 'name', i.e. the name of the demand activity.
353
+ folder_path : str, optional
354
+ Path to the folder where the JSON file will be saved. Default is None.
355
+ """
356
+
357
+ info_dict = {
358
+ "name": self.demand_act["name"],
359
+ "code": self.demand_act["code"],
360
+ "database": self.demand_act["database"],
361
+ }
362
+
363
+ if filename is None:
364
+ assert (
365
+ identifier in self.demand_act
366
+ ), f"Identifier '{identifier}' not found in demand activity."
367
+ filename = f"mc_results_{str(self.demand_act[identifier]).replace(' ','_')}_monte_carlo.json"
368
+
369
+ write_json(filename, info_dict | self._mc_results, folder_path=folder_path)
370
+
371
+ def stats_to_json(self, filename=None, identifier="name", folder_path=None):
372
+ """
373
+ Save Monte Carlo statistics to a JSON file.
374
+
375
+ Parameters
376
+ ----------
377
+ identifier : str, optional
378
+ Identifier to use for the filename. Default is 'name', i.e. the name of the demand activity.
379
+ folder_path : str, optional
380
+ Path to the folder where the JSON file will be saved. Default is None.
381
+ """
382
+
383
+ statistics = calculate_statistics(
384
+ self._mc_results, lcia_methods=self.lcia_methods, key_list=self.key_list
385
+ )
386
+
387
+ info_dict = {
388
+ "name": self.demand_act["name"],
389
+ "code": self.demand_act["code"],
390
+ "database": self.demand_act["database"],
391
+ }
392
+
393
+ if filename is None:
394
+ assert (
395
+ identifier in self.demand_act
396
+ ), f"Identifier '{identifier}' not found in demand activity."
397
+ filename = f"mc_stats_{str(self.demand_act[identifier]).replace(' ','_')}_monte_carlo.json"
398
+
399
+ write_json(filename, info_dict | statistics, folder_path=folder_path)
400
+
401
+ def get_results_dataframe(self, method=None):
402
+ """
403
+ Return Monte Carlo results as a Pandas DataFrame.
404
+ Useful for integration with Activity Browser.
405
+
406
+ Rows correspond to Monte Carlo iterations.
407
+ Columns correspond to LCIA methods (impact categories).
408
+
409
+ Returns
410
+ -------
411
+ pandas.DataFrame
412
+ DataFrame of shape (iterations, n_methods)
413
+ """
414
+
415
+ results = self.mc_results
416
+
417
+ idx = self.lcia_methods.index(method)
418
+ key = self.key_list[idx]
419
+
420
+ return pd.DataFrame({key: results[key]})
421
+
422
+ data = {key: results[key] for key in self.key_list}
423
+
424
+ return pd.DataFrame(data)
425
+
426
+ def get_exchange_list(self, foreground_only=False):
427
+ """
428
+ Get a list of all technosphere exchanges present in the LCA.
429
+
430
+ Returns
431
+ -------
432
+ exchange_list : list
433
+ List of technosphere exchanges present in the LCA.
434
+ """
435
+
436
+ # if the exchange list has already been generated, return it
437
+ if hasattr(self, "exchange_list"):
438
+ return self.exchange_list
439
+
440
+ # Get all activities in the technosphere matrix
441
+ self.load_lci_data()
442
+
443
+ all_activities = [
444
+ bw.Database(key[0]).get(key[1]) for key in self.activity_dict.keys()
445
+ ]
446
+
447
+ # Collect all technosphere exchanges for all activities
448
+ # exchange_list = []
449
+ # for act in all_activities:
450
+ # exchange_list.extend(list(act.technosphere()))
451
+
452
+ exchange_list = [
453
+ exc
454
+ for act in all_activities
455
+ for exc in act.technosphere()
456
+ if not (foreground_only and "ecoinvent" in act.key[0].lower())
457
+ ]
458
+
459
+ # cache the exchange list for future use
460
+ self.exchange_list = exchange_list
461
+
462
+ return exchange_list
463
+
464
+ def set_default_uncertainty(self, foreground_only=False):
465
+ """
466
+ Add default uniform uncertainty (±10%) to technosphere exchanges missing uncertainty.
467
+ """
468
+
469
+ exchange_list = self.get_exchange_list(foreground_only=foreground_only)
470
+
471
+ # Add default uniform uncertainty (±10%) to exchanges missing uncertainty
472
+ for exc in exchange_list:
473
+ data = exc._data
474
+
475
+ if "uncertainty type" not in data or data["uncertainty type"] in [0, 1]:
476
+ amt = data.get("amount", 0)
477
+
478
+ if abs(amt) > 0:
479
+ default_uncertainty = {
480
+ "minimum": 0.9 * amt,
481
+ "maximum": 1.1 * amt,
482
+ }
483
+ else:
484
+ default_uncertainty = {"minimum": -0.1, "maximum": 0.1}
485
+
486
+ exc._data["uncertainty type"] = (
487
+ 4 # corresponds to uniform distribution in stats_arrays
488
+ )
489
+ exc._data["minimum"] = default_uncertainty["minimum"]
490
+ exc._data["maximum"] = default_uncertainty["maximum"]
491
+ exc.save()
492
+
493
+ def exchange_list_to_excel(
494
+ self, filename=None, identifier="name", folder_path=None, foreground_only=False
495
+ ):
496
+ """
497
+ Save technosphere exchanges to an Excel file.
498
+
499
+ Parameters
500
+ ----------
501
+ filename : str, optional
502
+ Name of the Excel file to save the exchanges.
503
+ identifier : str, optional
504
+ Identifier to use for the filename. Default is 'name', i.e. the name of the demand activity.
505
+ folder_path : str, optional
506
+ Path to the folder where the Excel file will be saved. Default is None.
507
+ """
508
+
509
+ # get the complete list of technosphere exchanges
510
+ exchange_list = self.get_exchange_list(foreground_only=foreground_only)
511
+ print(f"Total number of technosphere exchanges: {len(exchange_list)}")
512
+
513
+ # convert exchange list to a DataFrame
514
+ start_time = time.time()
515
+ df = pd.DataFrame([exchange_to_dict(exc) for exc in exchange_list])
516
+ print(
517
+ f"Converted exchange list to DataFrame in {time.time() - start_time:.2f} seconds."
518
+ )
519
+
520
+ if folder_path is None:
521
+ folder_path = os.path.join(os.getcwd(), "results")
522
+
523
+ # create the folder if it does not exist
524
+ os.makedirs(folder_path, exist_ok=True)
525
+
526
+ if filename is None:
527
+ assert (
528
+ identifier in self.demand_act
529
+ ), f"Identifier '{identifier}' not found in demand activity."
530
+ filename = f"technosphere_exchanges_{str(self.demand_act[identifier]).replace(' ','_')}.xlsx"
531
+
532
+ print("folder_path", folder_path, "filename", filename)
533
+ start_time = time.time()
534
+ df.to_excel(
535
+ os.path.join(folder_path, filename), index=False, engine="xlsxwriter"
536
+ )
537
+ print(
538
+ f"Exported {len(df)} exchanges to {filename} in {time.time() - start_time:.2f} seconds."
539
+ )
540
+
541
+ def print_uncertainty_info(self):
542
+ """
543
+ Print summary statistics about uncertainty information in the exchanges of this LCA.
544
+ Uses the underlying Brightway tech_params data structure for efficient access.
545
+ """
546
+
547
+ if not hasattr(self, "tech_params"):
548
+ self.load_lci_data() # This will populate tech_params
549
+
550
+ # Uncertainty type mapping based on stats_arrays documentation
551
+ uncertainty_dictionary = {
552
+ 0: "Undefined",
553
+ 1: "No uncertainty",
554
+ 2: "Lognormal",
555
+ 3: "Normal",
556
+ 4: "Uniform",
557
+ 5: "Triangular",
558
+ 6: "Bernoulli",
559
+ 7: "Discrete Uniform",
560
+ 8: "Weibull",
561
+ 9: "Gamma",
562
+ 10: "Beta",
563
+ 11: "Generalized Extreme Value",
564
+ 12: "Student's T",
565
+ }
566
+
567
+ type_dictionary = {
568
+ -1: "unknown",
569
+ 0: "production",
570
+ 1: "technosphere",
571
+ 2: "biosphere",
572
+ 3: "substitution",
573
+ }
574
+
575
+ # find all technosphere exchanges in the tech_params list
576
+ technosphere_exchanges = [
577
+ param
578
+ for param in self.tech_params
579
+ if type_dictionary.get(param["type"], "other") == "technosphere"
580
+ ]
581
+
582
+ # Get total number of exchanges
583
+ total = len(technosphere_exchanges)
584
+
585
+ # Count uncertainty types
586
+ type_counts = {}
587
+ for param in technosphere_exchanges:
588
+ uncertainty_type = param["uncertainty_type"]
589
+ type_counts[uncertainty_type] = type_counts.get(uncertainty_type, 0) + 1
590
+
591
+ # Count parameters with uncertainty
592
+ with_uncertainty = sum(
593
+ count
594
+ for uncertainty_type, count in type_counts.items()
595
+ if uncertainty_type not in [0, 1]
596
+ )
597
+
598
+ # Print summary statistics
599
+ print(f"Total exchanges: {total}")
600
+ print(f"Exchanges with uncertainty: {with_uncertainty}")
601
+ if total > 0:
602
+ print(f"Percentage with uncertainty: {with_uncertainty/total*100:.2f}%\n")
603
+ print("Uncertainty type distribution:")
604
+ for uncertainty_type, count in type_counts.items():
605
+ label = uncertainty_dictionary[uncertainty_type]
606
+ count = type_counts.get(uncertainty_type, 0)
607
+ print(
608
+ f" Type {uncertainty_type} ({label}): {count} ({count/total*100:.1f}%)"
609
+ )
610
+ else:
611
+ print("No exchanges found.\n")
612
+
613
+ def print_uncertainty_info_old(self, foreground_only=False):
614
+ """
615
+ Print summary statistics about uncertainty information in the exchanges of this LCA.
616
+ Uses the underlying Brightway exchange data structure for robust access.
617
+
618
+ Parameters
619
+ ----------
620
+ foreground_only : bool, optional
621
+ Whether to consider only foreground exchanges. Default is False.
622
+ """
623
+
624
+ # Get the complete list of technosphere exchanges
625
+ exchange_list = self.get_exchange_list(foreground_only=foreground_only)
626
+
627
+ # Uncertainty type mapping based on stats_arrays documentation
628
+ uncertainty_types = {
629
+ 0: "Undefined",
630
+ 1: "No uncertainty",
631
+ 2: "Lognormal",
632
+ 3: "Normal",
633
+ 4: "Uniform",
634
+ 5: "Triangular",
635
+ 6: "Bernoulli",
636
+ 7: "Discrete Uniform",
637
+ 8: "Weibull",
638
+ 9: "Gamma",
639
+ 10: "Beta",
640
+ 11: "Generalized Extreme Value",
641
+ 12: "Student's T",
642
+ }
643
+
644
+ # Convert all exchanges to dicts for robust access
645
+ exc_dicts = [
646
+ exc.as_dict() if hasattr(exc, "as_dict") else exc for exc in exchange_list
647
+ ]
648
+
649
+ # Get total number of exchanges and categorize them into foregorund and background
650
+ total = len(exc_dicts)
651
+ fg = [
652
+ exc
653
+ for exc in exc_dicts
654
+ if not (
655
+ "ecoinvent" in str(exc.get("input", ("",))[0]).lower()
656
+ or "ecoinvent" in str(exc.get("output", ("",))[0]).lower()
657
+ )
658
+ ]
659
+ bg = [exc for exc in exc_dicts if exc not in fg]
660
+
661
+ # Count uncertainty types
662
+ with_uncertainty = [
663
+ exc
664
+ for exc in exc_dicts
665
+ if exc.get("uncertainty type") is not None
666
+ and exc.get("uncertainty type") not in [0, 1]
667
+ ]
668
+ type_counts = {}
669
+ for exc in exc_dicts:
670
+ t = exc.get("uncertainty type", 0)
671
+ type_counts[t] = type_counts.get(t, 0) + 1
672
+
673
+ # Print summary statistics
674
+ print(f"Total exchanges: {total}")
675
+ print(f"Exchanges with uncertainty: {len(with_uncertainty)}")
676
+ print(f"Percentage with uncertainty: {len(with_uncertainty)/total*100:.2f}%\n")
677
+ print("Uncertainty type distribution:")
678
+ for t in range(13):
679
+ label = uncertainty_types[t]
680
+ count = type_counts.get(t, 0)
681
+ print(f" Type {t} ({label}): {count} ({count/total*100:.1f}%)")
682
+
683
+ def print_stats(self, impcats=None):
684
+ statistics = calculate_statistics(
685
+ self._mc_results, lcia_methods=self.lcia_methods, key_list=self.key_list
686
+ )
687
+
688
+ # if impact categories are specified, filter the statistics to include only those categories
689
+ if impcats is not None:
690
+ statistics = {
691
+ key: stats for key, stats in statistics.items() if key in impcats
692
+ }
693
+
694
+ print(f"Monte Carlo results for demand: {self.demand_act['name']}")
695
+ for key, stats in statistics.items():
696
+ print(f"\nImpact category: {key}")
697
+ print(f" Mean: {stats['mean']}")
698
+ print(f" Std: {stats['std']}")
699
+ print(f" Min: {stats['min']}")
700
+ print(f" Max: {stats['max']}")
701
+ print(" Percentiles:")
702
+ for p, value in stats["percentiles"].items():
703
+ print(f" {p}th percentile: {value}")
704
+
705
+
706
+ def exchange_to_dict(exc):
707
+ """
708
+ Convert a technosphere exchange to a dictionary.
709
+
710
+ Parameters
711
+ ----------
712
+ exc : Exchange
713
+ Technosphere exchange to convert.
714
+ Returns
715
+ -------
716
+ exc_dict : dict
717
+ Dictionary representation of the exchange.
718
+
719
+ """
720
+
721
+ # get the keys for the input and output activities of the exchange
722
+ input_key = getattr(exc.input, "key")
723
+ output_key = getattr(exc.output, "key")
724
+
725
+ data = exc._data
726
+
727
+ exc_dict = {
728
+ "Input": bw.Database(input_key[0]).get(input_key[1])["name"],
729
+ "Input Database": input_key[0],
730
+ "Input Code": input_key[1],
731
+ "Output": bw.Database(output_key[0]).get(output_key[1])["name"],
732
+ "Output Database": output_key[0],
733
+ "Output Code": output_key[1],
734
+ "Amount": data.get("amount"),
735
+ "Unit": data.get("unit", None),
736
+ "Uncertainty Type": data.get("uncertainty type", None),
737
+ "Pedigree": data.get("pedigree", None),
738
+ "loc": data.get("loc", None),
739
+ "scale": data.get("scale", None),
740
+ "shape": data.get("shape", None),
741
+ "minimum": data.get("minimum", None),
742
+ "maximum": data.get("maximum", None),
743
+ "Formula": data.get("formula", None),
744
+ }
745
+
746
+ return exc_dict
747
+
748
+
749
+ def get_lcia_methods(lcia_method_name, get_keys=False):
750
+ """
751
+ Get LCIA methods and their keys based on the provided method name.
752
+
753
+ Parameters
754
+ ----------
755
+ lcia_method_name : str
756
+ Name of the LCIA method (e.g., 'EF v3.1 no LT').
757
+
758
+ Returns
759
+ -------
760
+ lcia_methods : list
761
+ List of LCIA methods.
762
+ key_list : list
763
+ List of keys for the LCIA methods.
764
+ """
765
+
766
+ lcia_methods = [method for method in bw.methods if lcia_method_name in str(method)]
767
+ key_list = get_key_list(lcia_methods)
768
+
769
+ return lcia_methods, key_list
770
+
771
+
772
+ def get_key_list(lcia_methods):
773
+ """
774
+ Get a list of keys for the provided LCIA methods.
775
+
776
+ Parameters
777
+ ----------
778
+ lcia_methods : list
779
+ List of LCIA methods.
780
+
781
+ Returns
782
+ -------
783
+ key_list : list
784
+ List of keys for the LCIA methods.
785
+ """
786
+
787
+ # here, the impact categories are renamed to a more readable format
788
+ key_list = []
789
+ for method in lcia_methods:
790
+ impact_cat = str(method[1])
791
+ impact_unit = str(bw.methods[method]["unit"])
792
+ key_list.append(impact_cat + " [" + impact_unit + "]")
793
+
794
+ return key_list
795
+
796
+
797
+ def calculate_statistics(
798
+ mc_results, lcia_method_name=None, lcia_methods=None, key_list=None
799
+ ):
800
+ """
801
+ Calculate statistics for Monte Carlo results.
802
+
803
+ Parameters
804
+ ----------
805
+ mc_results : dict
806
+ Dictionary containing the Monte Carlo results.
807
+ lcia_methods : list
808
+ List of LCIA methods.
809
+ key_list : list
810
+ List of keys for the LCIA methods.
811
+
812
+ Returns
813
+ -------
814
+ mc_statistics : dict
815
+ Dictionary containing the calculated statistics.
816
+ """
817
+
818
+ if lcia_methods is None:
819
+ assert (
820
+ lcia_method_name is not None
821
+ ), "Either 'lcia_method_name' or 'lcia_methods' must be provided."
822
+ lcia_methods, key_list = get_lcia_methods(lcia_method_name)
823
+ else:
824
+ assert (
825
+ key_list is not None
826
+ ), "key_list must be provided if lcia_method_name is not provided."
827
+
828
+ # calculate statistics
829
+ mc_statistics = {}
830
+ for i in range(len(lcia_methods)):
831
+ percentiles = {
832
+ "5": np.percentile(mc_results[key_list[i]], 5),
833
+ "10": np.percentile(mc_results[key_list[i]], 10),
834
+ "25": np.percentile(mc_results[key_list[i]], 25),
835
+ "50": np.percentile(mc_results[key_list[i]], 50),
836
+ "75": np.percentile(mc_results[key_list[i]], 75),
837
+ "90": np.percentile(mc_results[key_list[i]], 90),
838
+ "95": np.percentile(mc_results[key_list[i]], 95),
839
+ }
840
+ mc_statistics[key_list[i]] = {
841
+ "mean": np.mean(mc_results[key_list[i]]),
842
+ "std": np.std(mc_results[key_list[i]]),
843
+ "min": float(np.min(mc_results[key_list[i]])),
844
+ "max": float(np.max(mc_results[key_list[i]])),
845
+ "percentiles": percentiles,
846
+ }
847
+
848
+ return mc_statistics
849
+
850
+
851
+ def write_json(filename, dict_to_write, folder_path=None):
852
+ """
853
+ Write a dictionary to a JSON file.
854
+
855
+ Parameters
856
+ ----------
857
+ filename : str
858
+ Name of the JSON file to write.
859
+ dict_to_write : dict
860
+ Dictionary to write to the JSON file.
861
+ folder_path : str, optional
862
+ Path to the folder where the JSON file will be saved. If None, a default "results" folder is used.
863
+
864
+ """
865
+
866
+ if folder_path is None:
867
+ folder_path = os.path.join(os.getcwd(), "results")
868
+
869
+ # create the folder if it does not exist
870
+ os.makedirs(folder_path, exist_ok=True)
871
+
872
+ with open(os.path.join(folder_path, filename), "w") as file:
873
+ json.dump(dict_to_write, file, indent=4)
@@ -0,0 +1,103 @@
1
+ # SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import argparse
6
+ import pickle
7
+ import json
8
+ import sys
9
+
10
+ from multiprocessing import cpu_count
11
+
12
+ import brightway2 as bw
13
+
14
+ from .monte_carlo import MonteCarloLCA
15
+
16
+ # force unbuffered output
17
+ if hasattr(sys.stdout, "reconfigure"):
18
+ sys.stdout.reconfigure(line_buffering=True)
19
+
20
+
21
+ def main():
22
+
23
+ # set up argument parser
24
+ parser = argparse.ArgumentParser(description="Run Monte Carlo LCA simulations.")
25
+
26
+ # define expected arguments
27
+ parser.add_argument(
28
+ "--iterations",
29
+ type=int,
30
+ required=True,
31
+ help="Number of Monte Carlo iterations.",
32
+ )
33
+ parser.add_argument(
34
+ "--bw_project", type=str, required=True, help="Brightway2 project name."
35
+ )
36
+ parser.add_argument(
37
+ "--demand", type=str, required=True, help="JSON-encoded demand dictionary."
38
+ )
39
+ parser.add_argument(
40
+ "--lcia_methods",
41
+ type=str,
42
+ required=True,
43
+ help="JSON-encoded list of LCIA method identifiers.",
44
+ )
45
+ parser.add_argument(
46
+ "--output", type=str, required=True, help="Path to output pickle file."
47
+ )
48
+ parser.add_argument(
49
+ "--num_cores",
50
+ type=int,
51
+ required=True,
52
+ help="Number of CPU cores to use for parallel processing.",
53
+ )
54
+ parser.add_argument(
55
+ "--run_parallel",
56
+ type=str,
57
+ default="True",
58
+ help="Whether to run the Monte Carlo simulation in parallel (True/False).",
59
+ )
60
+
61
+ # parse the arguments
62
+ args = parser.parse_args()
63
+
64
+ # set up Brightway within the subprocess
65
+ bw.projects.set_current(args.bw_project)
66
+
67
+ try:
68
+ # the demand list looks like: ["<database_name>", "<activity_key>"]
69
+ demand_list = json.loads(args.demand)
70
+ lcia_methods_list = json.loads(args.lcia_methods)
71
+ except json.JSONDecodeError as e:
72
+ print(f"Failed to decode JSON arguments: {e}", file=sys.stderr)
73
+ sys.exit(2)
74
+
75
+ # build the demand dictionary for the Monte Carlo LCA
76
+ demand = {bw.Database(demand_list[0]).get(demand_list[1]): 1}
77
+
78
+ # convert LCIA methods to tuples
79
+ lcia_methods = [tuple(method) for method in lcia_methods_list]
80
+
81
+ # concert run_parallel argument to boolean
82
+ run_parallel = args.run_parallel.lower() == "true"
83
+
84
+ # initialize the Monte Carlo LCA
85
+ mc_lca = MonteCarloLCA(
86
+ demand,
87
+ lcia_methods=lcia_methods,
88
+ num_cores=args.num_cores,
89
+ run_parallel=run_parallel,
90
+ )
91
+
92
+ # execute the Monte Carlo simulation
93
+ print(f"Running Monte Carlo LCA with {args.iterations} iterations...", flush=True)
94
+ mc_lca.execute_monte_carlo(iterations=args.iterations)
95
+
96
+ # write the results to a file
97
+ with open(args.output, "wb") as f:
98
+ pickle.dump(mc_lca.mc_results, f)
99
+ print(f"Results written to {args.output}", flush=True)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,64 @@
1
+ # SPDX-FileCopyrightText: 2026 Maria Höller, German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ # this works because we have a package structure and an editable install
6
+ # (make sure to run 'pip install -e .' in the repository root first)
7
+ import uncertainty_lca as ulca
8
+
9
+ # import standard modules
10
+ import time
11
+ from datetime import timedelta
12
+
13
+ import brightway2 as bw
14
+
15
+
16
+ def test_lca_monte_carlo():
17
+ # start a timer for time-tracking
18
+ start_time = time.time()
19
+
20
+ assert (
21
+ "moca_test_project" in bw.projects
22
+ ), "Test project 'moca_test_project' does not exist. Please run 'create_test_project.py' first to create the test dataset."
23
+
24
+ # setting up Brightway
25
+ bw.projects.set_current("moca_test_project")
26
+
27
+ # specify the LCIA method / characterisation model
28
+ lcia_method_name = "EF v3.1"
29
+
30
+ # build the demand dictionary for the Monte Carlo LCA
31
+ demand = {bw.Database("foreground").get("fg_activity_0"): 1}
32
+
33
+ # initialize the Monte Carlo LCA
34
+ mc_lca = ulca.MonteCarloLCA(demand, lcia_method_name)
35
+
36
+ # Print uncertainty info for the exchange list using the new method
37
+ mc_lca.set_default_uncertainty()
38
+ mc_lca.print_uncertainty_info()
39
+
40
+ # export the exchange list
41
+ # mc_lca.exchange_list_to_excel(foreground_only=False)
42
+
43
+ # # execute the Monte Carlo simulation
44
+ mc_lca.execute_monte_carlo(iterations=100)
45
+
46
+ # # retrieve the results and write them to files
47
+ # mc_results = mc_lca.mc_results
48
+ mc_lca.print_stats(impcats=["climate change [kg CO2-Eq]"])
49
+ # mc_lca.results_to_json()
50
+ # mc_lca.stats_to_json()
51
+
52
+ # end the timer and print the time elapsed
53
+ end_time = time.time()
54
+ duration = end_time - start_time
55
+ dur_timedelta = timedelta(seconds=int(duration))
56
+
57
+ print(
58
+ f"Time elapsed: {duration:.2f} seconds = {duration // 60:.0f}:{int(duration % 60):02} minutes = {dur_timedelta}"
59
+ )
60
+
61
+
62
+ # this is the main function that is called when you press run
63
+ if __name__ == "__main__":
64
+ test_lca_monte_carlo()