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.
- moca_uncertainty_lca-1.0.0/LICENSE.txt +12 -0
- moca_uncertainty_lca-1.0.0/PKG-INFO +7 -0
- moca_uncertainty_lca-1.0.0/README.md +120 -0
- moca_uncertainty_lca-1.0.0/pyproject.toml +13 -0
- moca_uncertainty_lca-1.0.0/setup.cfg +20 -0
- moca_uncertainty_lca-1.0.0/src/moca_uncertainty_lca.egg-info/PKG-INFO +7 -0
- moca_uncertainty_lca-1.0.0/src/moca_uncertainty_lca.egg-info/SOURCES.txt +12 -0
- moca_uncertainty_lca-1.0.0/src/moca_uncertainty_lca.egg-info/dependency_links.txt +1 -0
- moca_uncertainty_lca-1.0.0/src/moca_uncertainty_lca.egg-info/top_level.txt +1 -0
- moca_uncertainty_lca-1.0.0/src/uncertainty_lca/__init__.py +7 -0
- moca_uncertainty_lca-1.0.0/src/uncertainty_lca/monte_carlo.py +873 -0
- moca_uncertainty_lca-1.0.0/src/uncertainty_lca/run.py +103 -0
- moca_uncertainty_lca-1.0.0/tests/test.py +64 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uncertainty_lca
|
|
@@ -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()
|