⚠️ Brightway Learn is in Public Beta ⚠️

BW25 for Beginners#

Authors

This page was created by Alya Bolowich and Sabina Bednarova using excerpts from Max Koslowski’s Brightway2 tutorial.

License

This page is licensed under a Creative Commons Attribution 4.0 International License. Cite it as: "BW25 for Beginners", "Learn Brightway" Online Book (https://learn.brightway.dev/) by Brightway Contributors (2023).

Introductory note#

This is a Jupyter Notebook with the purpose of making you familiar with the basic Python-based Brightway25 LCA framework and its various functionalities, all developed by Chris Mutel and colleagues.

This notebook is designed to get you started with creating your own project, setting up your database(s) (used as background systems) and simple foreground systems, performing your first impact assessments, analysing your results, and running Monte Carlo simulations in calculation setups.

Since Brightway2 builds on Python, a widely used programming language for scientific analysis, and can be installed easiest through the package and environment mangement system conda, we need to get either Anaconda, an open source distribution for multiple languages including Python and R, or at least Miniconda, the bootstrap version of Anaconda. The full Anaconda installation contains, among others, a terminal and an IDE (integrated development environment). And, good for us, it natively installs the setup for Jupyter Notebooks, through which we can work most effectively with Brightway2. Both Jupyter and Brightway can also be installed through pip, but it may be a tad trickier.

For clarification: conda is, just like pip, a package and environment management system; the difference, however, is that “pip installs python packages within any environment; conda installs any package within conda environments.” (quote from here)



Recommended prerequisites are:

  • having installed Brightway25 + all relevant packages (make sure to do so in the correct virtual environment) and possibly even having had a first quick look at its documentation.

  • knowledge of the foundations of life cycle assessment. If you don’t have this knowledge (or simply need a refresher), have a look at the online teaching resources provided by the Industrial Ecology Freiburg Group, led by Stefan Pauliuk.

  • familiarity with any programming language - no Python-specific requirements, as most methods are quite intuitive and will be explained anyway. In case you are a total beginner, check this out :)



For Jupyter newbies

In a Jupyter notebook everything is live and interactive. And, perhaps more interesting, markdown elements and actual code are mixed - and you can even include images, videos, and other formats. There are many, many things that you can do with such notebooks, and there are also many cool tricks as well as some extensions.

To get you started, you might want to press the h-key, which gives you an overview of keyboard shortcuts. Then, you have markdown (richt text containing) cells and code cells: if you hover over and select any cell by clicking on it (notice the blue bar on the left of the cell; turns green when you are in edit mode, i.e. having clicked into the cell box) you can turn it into either a markdown one by pressing m or into a code cell by pressing y; there are other types, too, but they are not relevant for what we want to do. Click on a cell to select it or simply use the arrow keys to navigate. If you want to run a cell, simply press shift+enter. And if you want to insert an empty cell above or below the current one, simply press a or b. For changing the contents of a cell, either doubleclick a markdown cell or click a code cell; pressing enter works, too. You can exit a cell using Esc and can copy, cut, and paste cells when pressing the keys c, x, and v when having selected a cell. As for the output of a command, note that clicking on it once makes it scroll-able, clicking on it twice results in hiding it.

You will notice further below that there are sometimes commands starting with import. These ones are needed to “import” the code of the respective package into the present code. For instance, by importing the Brightway2 package you enable the present notebook to use the Brightway2 functionalities. To have access to these packages, you need to download them first; otherwise you will receive an error message. You can download such packages in two ways: either in the notebook or in the terminal (make sure to be in the right virtual environment).

These are the absolute basics, but they are already more than what you need here.



Recap on LCA

To make sure we are on the same page, let’s recapitulate the basics of LCA, expressed in a single formula (the notation may differ to the one that you are familiar with):

\[h = CBA^{-1}f\]

where:

  • \(A\)… transaction matrix (dimensions p x q)

  • \(B\)… environmental interventions matrix (dimensions r x q)

  • \(C\)… characterisation matrix (dimensions p x q)

  • \(f\)… final demand vector (dimensions p x 1)

  • \(h\)… characterised inventory matrix (dimensions 1 x 1)

with the dimensions being:

  • p… number of products

  • q… number of activities/processes

  • r… number of elementary flows

Specific parts of the above formula have distinct names. Since these names are also used in the Brightway2-framework, we will note them down here:

  • \(f\)… demand array

  • \(A{^-1}f\)… supply array

  • \(BA{^-1}f\)… inventory

  • \(CBA{^-1}f\)… characterised inventory

To not only get a single score per impact category, we can diagonalise the supply array (how this can be realised in the Brightway2-framework is not shown in this notebook) so that we can see the environmental impacts per process:

\[h_{process} = CB~diag(A^{-1}f)\]

You can do the same for breaking down the total environmental impact to the stressors:

\[h_{stressor} = C~diag(BA^{-1}f)\]

Another way of representing your results is to show the environmental stressors per activity for a specific impact category:

\[h_{category} = diag(C)~B~diag(A^{-1}f)\]


The notebook is structured as follows:

  1. Setup of a project: How to set up a project to work in

  2. Database import/setup: How to import a database and get it ready for use

    2.1 Importing ecoinvent and Forwast
    2.2 Database selection and a first look at activities
    2.3 Importing a dataset from Excel

  3. Manual database creation: How to manually create a database

  4. LCIA: Basics of the LCIA calculations

  5. Foreground system: How to set up a very simple product system and ways to explore BW2’s functionalities

    5.1 Simple product system
    5.2 Bottle production

  6. Manipulating an exchange: How to manipulate an activity in multiple ways

    6.1 Copying and deleting an activity
    6.2 First attempts at manipulating an exchange’s keys/values
    6.3 Substituting an exchange
    6.4 Successfully manipulating an exchange

  7. Basic contribution analysis: How to examine the LCA results

    7.1 Recalculation of the inventory
    7.2 Basic contribution analysis
    7.3 Top emissions and processes by name

  8. Multi-LCA: How to compare multiple alternatives across multiple impact categories simultaneously

    8.1 Manually comparing products
    8.2 Calculation setups
    8.3 MLCA

  9. Monte Carlo simulation: How to quantify uncertainties of the LCA result



Outcomes - what are we going to learn?

By completing this notebook (run it locally on your machine and don’t just read the code), we will:

  • have understood the basics of the Brightway25 LCA framework

  • know how to import/generate/handle datasets in Brightway25

  • be able to run simple LCAs (both linearly and in parallel)

  • be able to analyse a product system and the results of an LCA

  • know the contirbution analysis of a product

  • be able to visualise data in form of tables and figures



Just for clarification, abbreviations used in the markdown cells of this notebook are:

Abbreviation(s)

Written out

bw, BW25

Brightway25

FU

Functional unit

LCA

Life cycle assessment

LCI

Life cycle inventory

LCIA

Life cycle impact assessment

EI

Ecoinvent


So, that would be it what we’re up to do. But if you’re super keen then you might also be interested in checking out the Activity browser and LCOPT, both of which are essentially graphical user interfaces for BW25. The latter one can be used particularly for parametrised inventories (you’ll learn more about the basics of this later).



1. Setup of a project#

A project gets instantiated and relevant libraries/packages imported.

# Import packages we'll need later on in this tutorial
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Import BW25 packages. You'll notice the packages are imported individually, unlike a one-and-done import with BW2.
import bw2data as bd
import bw2io as bi
import bw2calc as bc
import bw2analyzer as bwa

Let’s list the available projects.

# If you're just starting out, this should only have "default" in it.
# Else, you'll see all the previous projects you've worked on.

bd.projects
Brightway2 projects manager with 6 objects:
	bw25-tuto
	default
	ecoinvent391
	terra
	test
	test-from-new
Use `projects.report()` to get a report on all projects.

We can see where our projects are stored using the following command (info that’s good to know but not essential to run your LCA).

bd.projects.dir
PosixPath('/home/bolowich/.local/share/Brightway3/default.c21f969b5f03d33d43e04f8f136e7682')

1.1.a Creating a new project#

To create a new project, we need to set it up:

# You need to set a project. Give it a name!
name = "bw25-tuto"
bd.projects.set_current(name)

Confirm that your project has been registered:

bd.projects
Brightway2 projects manager with 7 objects:
	bw25-tuto
	default
	ecoinvent391
	project-i-want
	terra
	test
	test-from-new
Use `projects.report()` to get a report on all projects.

1.1.b Use an existing project#

The code here is commented out to avoid any confusion with those starting a new project. If you already have a project, then uncomment this code and run the cell below.

# Your name variable should be the name of the project.

# name = "project-i-want"
# bd.projects.set_current(name)

You must execute the bd.projects.set_current() command before importing the biosphere flows (or progressing further for that matter)!

2. Database import/setup#

2.1 Importing biosphere flows#

We will now add our first database to the project. Let’s see what databases we have:

# If this is your first time using this BW25 project, this should be an empty dictionary! 
bd.databases
Databases dictionary with 0 objects

We will now import the biosphere matrix to our project.

# If this is the first time you've set up the project, you need to install the biosphere flows.
# You do not need to run this subsequent times.

bi.bw2setup() ## THIS YIELDS AN ERROR AT THE MOMENT
Biosphere database already present!!! No setup is needed

2.2. Importing a backend database#

2.2.a Importing ecoinvent#

LCA draws on a lot of background data. Those can be found in numerous databases, for instance ecoinvent. We can check which databases are available in the current project with the following command:

# List the available databases.
# It is important that now you see the biosphere database!
bd.databases
Databases dictionary with 2 object(s):
	biosphere3
	ecoinvent-391-cutoff

If the list does not yet include the database we want, we can import it.

As an example, we first import the ecoinvent cut-off database and check on some of the import’s statistics. You’ll need to have the path to your ecoinvent database and a name that you’ll use for it. The EI database will need to be unzipped and the the path should end at the datasets folder.

# Import the path where your EI database is stored. 
# Note that the EI database must be unzipped and the path should end at the datasets folder.
ei_path = "/home/bolowich/Documents/dbs/ecoinvent 3.9.1_cutoff_ecoSpold02/datasets"

# You will also need to give your database a name. This name will appear when you call bd.databases.
# Here, I am using EI v3.9.1 cutoff.
ei_name = "ecoinvent-391-cutoff"
# When we execute this cell, we will check if it's already been imported, and if not (else) we import it.

if ei_name in bd.databases:
    print("Database has already been imported.")
else:
# Go ahead and import:
    ei_importer = bi.SingleOutputEcospold2Importer(ei_path, ei_name)
    # Apply stragegies 
    ei_importer.apply_strategies()
    # We can get some statistics
    ei_importer.statistics()
    # Now we will write the database into our project. 
    ei_importer.write_database()
Extracting XML data from 21238 datasets
Extracted 21238 datasets in 34.62 seconds

When will the if/else statement above not work?

If you have issues importing the database and you have unlinked exchanges (addressed below), the program will return an error message. In this case, you will need to run only the cell below.

# Should you have a problem with "unlinked exchanges"
# ei_importer = bi.SingleOutputEcospold2Importer(ei_path, ei_name)
# ei_importer.add_unlinked_flows_to_biosphere_database() 
# ei_importer.apply_strategies()
# ei_importer.statistics()
# ei_importer.write_database()

Check the database is now in your project.

bd.databases
Databases dictionary with 2 object(s):
	biosphere3
	ecoinvent-391-cutoff

For some background:

The function apply_strategies() will convert the database format to something compatible with the BW25 architecture.

The function statistics() will give you some details on your dataset.

We will then need to write the database to disk, using the write_database() function.

In some cases, you may encounter and issue with unlinked exchanges. If you have this issue, run the cell below and then re-run the cell that imports, applies strategies, and writes the database.

In some cases, we may get an error related to “unlinked exchanges”. This is because there are some biosphere flows used in ecoinvent that do not exist in the BW25 biosphere3 database (and, consequently, in our LCIA methods). The best we can do is to import these unlinked flows into another biosphere database. If these flows are important to you, you should do further matching or modify the relevant LCIA methods to include them.

### 2.2.b Importing Agribalyse database

### 2.2.c Importing FORWAST database

2.3 Database selection and a first look at activities#

If our database of choice is already included, then we just make use of it directly. For convenience, we need to assign a variable to it so that the database can be worked with more easily.

eidb = bd.Database(ei_name)

We can also check up on the type and length of our imported database.

print("The imported ecoinvent database is of type {} and has a length of {}.".format(type(eidb), len(eidb)))
The imported ecoinvent database is of type <class 'bw2data.backends.base.SQLiteBackend'> and has a length of 21238.

And we can even visualise our technospheres:

# Be patient, this can take a couple seconds ;)
eidb.graph_technosphere()
../../_images/c1f3d95685ca6d94d103928cb7571d3fad0b6d66fb64f91b31bb32f9fb27649f.png

Nice graphic! But it doesn’t really tell us much… Let’s have a bit of a closer look at what our database contains. We can, for instance, examine a random process of the imported ecoinvent database by doing the following:

random_act = eidb.random()
random_act.as_dict()
{'comment': "This is a market activity. Each market represents the consumption mix of a product in a given geography, connecting suppliers with consumers of the same product in the same geographical area. Markets group the producers and also the imports of the product (if relevant) within the same geographical area. They also account for transport to the consumer and for the losses during that process, when relevant.\nThis dataset describes the electricity available on the high voltage level in China Southern Power Grid. This is done by showing the transmission of 1kWh electricity at high voltage.\nTechnology:  Average technology used to transmit and distribute electricity. Includes underground and overhead lines, as well as air-, vacuum- and SF6-insulated high-to-medium voltage switching stations. Electricity production according to related technology datasets.\nTime period:  The 'Start of Period' and 'End of Period' do not refer to the year for which this market is valid. See general comment for the year for which the shares are based on.",
 'classifications': [('ISIC rev.4 ecoinvent',
   '3510:Electric power generation, transmission and distribution'),
  ('EcoSpold01Categories', 'photovoltaic/power plants'),
  ('CPC', '17100: Electrical energy')],
 'activity type': 'market activity',
 'activity': 'b4ce482f-1f40-5b5c-a301-a74e5703578b',
 'database': 'ecoinvent-391-cutoff',
 'filename': 'b4ce482f-1f40-5b5c-a301-a74e5703578b_d69294d7-8d64-4915-a896-9996a014c410.spold',
 'location': 'CN-CSG',
 'name': 'market for electricity, low voltage',
 'synonyms': [],
 'parameters': [],
 'authors': {'data entry': {'name': 'Karin Treyer',
   'email': 'karin.treyer@psi.ch'},
  'data generator': {'name': 'Karin Treyer', 'email': 'karin.treyer@psi.ch'}},
 'type': 'process',
 'reference product': 'electricity, low voltage',
 'flow': 'd69294d7-8d64-4915-a896-9996a014c410',
 'unit': 'kilowatt hour',
 'production amount': 1.0,
 'code': 'b9e29bfda0ef60749e76d170f3a2bcbb',
 'id': 19998}

To get an overview of all exchanges of the selected process, type:

for exc in random_act.exchanges():
    print(exc)
Exchange: 1.0 kilowatt hour 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 8.74048809653223e-08 kilometer 'market for distribution network, electricity, low voltage' (kilometer, GLO, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.002526569039557459 kilowatt hour 'electricity production, photovoltaic, 570kWp open ground installation, multi-Si' (kilowatt hour, CN-YN, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0002143684433536061 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted' (kilowatt hour, CN-GX, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0006992795537960195 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted' (kilowatt hour, CN-GD, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.9858011741163558 kilowatt hour 'electricity voltage transformation from medium to low voltage' (kilowatt hour, CN-CSG, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0005626589493276578 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted' (kilowatt hour, CN-YN, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.00252140216364629 kilowatt hour 'electricity production, photovoltaic, 570kWp open ground installation, multi-Si' (kilowatt hour, CN-GD, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0003503740925849345 kilowatt hour 'electricity production, photovoltaic, 570kWp open ground installation, multi-Si' (kilowatt hour, CN-HA, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0007729513239092022 kilowatt hour 'electricity production, photovoltaic, 570kWp open ground installation, multi-Si' (kilowatt hour, CN-GX, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0001721338198097884 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted' (kilowatt hour, CN-GX, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.000913652612186081 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted' (kilowatt hour, CN-GZ, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.00073364582796993 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted' (kilowatt hour, CN-GZ, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.003294370128057621 kilowatt hour 'electricity production, photovoltaic, 570kWp open ground installation, multi-Si' (kilowatt hour, CN-GZ, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.0005615083023728475 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted' (kilowatt hour, CN-GD, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 9.717190008679985e-05 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted' (kilowatt hour, CN-HA, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.02184220753983677 kilowatt hour 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 7.80272043703983e-05 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted' (kilowatt hour, CN-HA, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 0.000700712522615423 kilowatt hour 'electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted' (kilowatt hour, CN-YN, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 9.09e-09 kilogram 'market for sulfur hexafluoride, liquid' (kilogram, RoW, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>
Exchange: 9.09e-09 kg 'Sulfur hexafluoride' (kg, None, None) to 'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)>

And if you want to get more information on one specific exchange of the chosen process, do:

#change the numeral to check out the other exchanges (as from the list above)
[exc for exc in random_act.exchanges()][0].as_dict()
{'flow': 'd69294d7-8d64-4915-a896-9996a014c410',
 'type': 'production',
 'name': 'electricity, low voltage',
 'classifications': {'CPC': ['17100: Electrical energy']},
 'production volume': 772610481477.1199,
 'properties': {'carbon allocation': {'amount': 0.0, 'unit': 'kg'},
  'carbon content': {'amount': 0.0, 'unit': 'dimensionless'},
  'price': {'amount': 0.107,
   'comment': 'Temporary price data. Calculated as 90% of purchasers price based on: IEA Key world energy statistics 2004 (www.iea.org/textbase/nppdf/free/2004/keyworld2004.pdf)',
   'unit': 'EUR2005'}},
 'activity': 'b4ce482f-1f40-5b5c-a301-a74e5703578b',
 'unit': 'kilowatt hour',
 'amount': 1.0,
 'uncertainty type': 0,
 'loc': 1.0,
 'input': ('ecoinvent-391-cutoff', 'b9e29bfda0ef60749e76d170f3a2bcbb'),
 'output': ('ecoinvent-391-cutoff', 'b9e29bfda0ef60749e76d170f3a2bcbb')}

Sometimes it can also be helpful to have some activity characteristics at hand, for instance if you want to search a database using an activity’s code or want to get an activity’s name when only having its key…

#getting an activity's code
random_act['code']
'b9e29bfda0ef60749e76d170f3a2bcbb'
#getting an activity's name through its code
eidb.get(random_act['code'])
'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)
#getting an activity's key
random_act.key
('ecoinvent-391-cutoff', 'b9e29bfda0ef60749e76d170f3a2bcbb')
#getting an activity's name using its key
bd.get_activity(random_act.key)
'market for electricity, low voltage' (kilowatt hour, CN-CSG, None)

2.3 Importing a foreground dataset from Excel#

In addition to the import of a standard background database, we can also create a database, like a bespoke foreground. There are several file types that work when developing a foreground inventory, one of them being Excel (more on file types for foreground development HERE[need link here] .

This works relatively smoothly, provided the Excel workbook is created correctly. Here, we import a flawless dataset, but it may be that you have a dataset that has some inconsistencies in the import. In that case, check that the units in the foreground match those in the background database (1L of diesel vs 1kg of diesel, for example), check your reference products are correct for the database you are referencing, look for spelling errors, …

More information on creating a foreground inventory in Excel can be found HERE[need link here] .

If you have a foreground database with several activites that calls on data from other databases that you have created, all of the databases will need to be added. For example, there’s one database, “car_db” that contains a process “car” that contains processes “body”, “steering wheel” from the database “parts_db”, you need to execute this process for both of these databases.

# Include the path to the foreground database
fg_db = "excel_importer_example.xlsx"

# Import your LCI
lci = bi.ExcelImporter(fg_db)
Extracted 2 worksheets in 0.05 seconds
/home/bolowich/mambaforge/envs/terra/lib/python3.10/site-packages/openpyxl/worksheet/_read_only.py:79: UserWarning: Unknown extension is not supported and will be removed
  for idx, row in parser.parse():
# Need to match FG_DB to itself
lci.match_database(fields=["name", "unit"])
# Need to match FG_DB to the biosphere
lci.match_database(fields=["name", "categories"])
lci.match_database("biosphere3", fields=["name", "categories"])
Applying strategy: link_iterable_by_fields
Applying strategy: link_iterable_by_fields
bi.create_core_migrations()
# Once your package is imported we need to apply strategies
lci.apply_strategies()

# We need to match databases - name and categories but ATTENTION! the categories in
# the excel file is "None" so we will also need to match against unit.

lci.statistics()
Applying strategy: csv_restore_tuples
Applying strategy: csv_restore_booleans
Applying strategy: csv_numerize
Applying strategy: csv_drop_unknown
Applying strategy: csv_add_missing_exchanges_section
Applying strategy: normalize_units
Applying strategy: normalize_biosphere_categories
Applying strategy: normalize_biosphere_names
Applying strategy: strip_biosphere_exc_locations
Applying strategy: set_code_by_activity_hash
Applying strategy: link_iterable_by_fields
Applying strategy: assign_only_product_as_production
Applying strategy: link_technosphere_by_activity_hash
Applying strategy: drop_falsey_uncertainty_fields_but_keep_zeros
Applying strategy: convert_uncertainty_types_to_integers
Applying strategy: convert_activity_parameters_to_list
Applied 16 strategies in 6.86 seconds
2 datasets
7 exchanges
4 unlinked exchanges
  Type biosphere: 1 unique unlinked exchanges
  Type technosphere: 3 unique unlinked exchanges
(2, 7, 4)

You can check whether the import went as expected by having a look at an Excel sheet, that includes our process data. The location of this file is given as output of the following command:

This is not an essential process, but good to know :)

lci.write_excel()

Seems like everything went fine :)

If an exchange cannot be linked using the above applied strategies, because the respective input is listed multiple times in the database, you would have to link it manually. An example for such a case is treatment of aluminium scrap, post-consumer, prepared for recycling, at refiner, for which two ecoinvent (3.5 cutoff) entries exist.

NEED TO KNOW HOW TO ADD A FG INPUT MANUALLY

Having imported the data, we also need to write it to a database to save it. This will write it to disk.

# Write the foreground (or custom Excel database) to disk.
lci.write_database()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[44], line 2
      1 # Write the foreground (or custom Excel database) to disk.
----> 2 lci.write_database()

File ~/mambaforge/envs/bw-tutos/lib/python3.11/site-packages/bw2io/importers/excel.py:282, in ExcelImporter.write_database(self, **kwargs)
    280 """Same as base ``write_database`` method, but ``activate_parameters`` is True by default."""
    281 kwargs["activate_parameters"] = kwargs.get("activate_parameters", True)
--> 282 super(ExcelImporter, self).write_database(**kwargs)

File ~/mambaforge/envs/bw-tutos/lib/python3.11/site-packages/bw2io/importers/base_lci.py:272, in LCIImporter.write_database(self, data, delete_existing, backend, activate_parameters, db_name, **kwargs)
    270         warnings.simplefilter("ignore")
    271         db = Database(db_name, backend=backend)
--> 272         db.register(**self.metadata)
    274 self.write_database_parameters(activate_parameters, delete_existing)
    276 existing.update(data)

TypeError: keywords must be strings
# Check that your db now exists
bd.databases
Databases dictionary with 2 object(s):
	biosphere3
	ecoinvent-391-cutoff
wbi = bd.Database("BW2 Excel water bottle import")
for act in wbi:
    print(act)
wbp = [act for act in wbi][0]
for exc in wbp.exchanges():
    print(exc)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[49], line 1
----> 1 wbp = [act for act in wbi][0]
      2 for exc in wbp.exchanges():
      3     print(exc)

IndexError: list index out of range

See whether the details of our biosphere exchange are correct:

ex = [act for act in wbi if 'water bottle production' in act['name']][0]
[exc for exc in ex.exchanges() if 'Carbon dioxide' in str(exc)][0].as_dict()

Final check whether really everything worked - these commands will be explained later in detail:

lca = bw.LCA(
    {("BW2 Excel water bottle import", "WriteSomeCode_UUID_isFineButNotNecessary"): 1}, 
    ('IPCC 2013', 'climate change', 'GWP 100a')
)
lca.lci()
lca.lcia()
lca.score

Ok, enough with that. Let’s delete this database (not the Excel file!) and the associated variables now:

del databases[wbi.name]
#or:
#wbi.delete()
del wbi, wbp

Alternatively, we can also deregister the database, which removes all metadata:

bw.Database('BW2 Excel water bottle import').deregister()
bw.databases

Continue, or back to table of contents?


3. Manual database creation#

In addition, let’s add a new database manually. There are two very similar ways of doing so, shown below. Mind that this database needs to be saved as well so that we can work with it.

#One way of manually creating a database:
db1 = bw.Database('3D')
db1.register()
db1.write({('3D', 'Printer'):{
    'name': 'Printer',
    'exchanges': [],
    'unit': 'unit',
    'location': 'somewhere',
    'categories': ('in the', 'universe')
}})



#Another way of manually creating a database is to have the data in a separate dictionary and then write it into an instantiated database:
db2 = Database("example")
example_data = {
    ("example", "A"): {
        "name": "A",
        "exchanges": [{
            "amount": 1.0,
            "input": ("example", "B"),
            "type": "technosphere"
            }],
        'unit': 'kilogram',
        'location': 'here',
        'categories': ("very", "interesting")
        },
    ("example", "B"): {
        "name": "B",
        "exchanges": [],
        'unit': 'microgram',
        'location': 'there',
        'categories': ('quite', 'boring')
        }
    }

db2.write(example_data)

Let’s see if our databases actually exist:

bw.databases

Well, seems like it. Now, let’s have a look at a random activity in our second database:

db2.random()

If we want to get some information on the number of exchanges in each activity contained in the database, we type in:

num_exchanges = [(activity, len(activity.exchanges())) for activity in db2]
num_exchanges

We can also look for our activities, in this case all of them, by typing:

db1.search("*")

However, we don’t want to use these databases in the following. Therefore, we simply delete them, just like we deleted the dataset imported from an Excel file above:

del databases[db1.name]
del databases[db2.name]
#make sure that the databases got deleted
bw.databases

Okay, now that we have our background data (be it manually created or imported from an existing database), it’s time to model our foreground system!

Continue, or back to table of contents?


4. LCIA#

Before continuing with a simple product system example, the basics of the inventory caluclation shall be introduced. First, let’s have a look at which LCIA methods we can access:

list(bw.methods)

#or use the built-in method
#bw.methods.list

#or the following
#bw.methods.items()

Ok, we see that there are quite a few methods that we could use for our LCA. Can we get more details on them? Sure we can! Those details are stored as values (bw.methods.values()), with the method names being the keys (bw.methods.keys()):

for key in bw.methods:
    print(key, ':', bw.methods[key])

To access the description of only one method, type:

bw.methods.get(('CML 2001', 'acidification potential', 'average European'))

As you can see, this is a nested dictionary. You can also access values within the nested one, for example:

bw.methods.get(('ILCD 1.0.8 2016 midpoint', 'ecosystem quality', 'marine eutrophication')).get('unit')

Now, let’s select a set of LCIA methods to be applied in an inventory calculation, in this case the IPCC 2013 GWP100a (no LT).

CC_method = [m for m in bw.methods if 'IPCC 2013' in str(m) and  'climate change' in str(m) and 'GWP 100a' in str(m) and not 'no LT' in str(m)][0]
CC_method

Let’s define the functional unit/ reference flow; for now, this is only a random one, picked from our ecoinvent database:

# select a process to be defined as functional unit
process = eidb.random()
process
functional_unit = {process:1}

Now we can calculate the inventory!

lca = LCA(functional_unit,CC_method)

# alternatively, you can enter both functional unit and LCIA method directly into the LCA-command:
# myFirstLCA_quick = bw.LCA({process:1}, ('IPCC 2013', 'climate change', 'GWP 100a'))

And here we perform the actual impact calculation! Mind that this does not yield an output.

lca.lci()
lca.lcia()

To see the actual impact as mid- or endpoint indicator (according to the impact assessment method used), we type in:

lca.score

This result is given in the unit specified by the impact assessment method.

You can also check which LCIA method was applied for a given result:

lca.method

To see what else you can do with the lca-object, press tab in the next cell:

lca.

Change the method

Now let’s change the impact assessment method and experiment with two other methods, redo_lci and redo_lcia:

agri_land_occ = [m for m in bw.methods if 'ReCiPe Midpoint (H)' in str(m) and  'agricultural land occupation' in str(m) and not 'w/o LT' in str(m) ][0]
lca.switch_method(agri_land_occ)

We can recalculate our LCI, including a changed demand, i.e. a new functional unit:

demand = {process:2}
lca.redo_lci(demand)
lca.score

The above command, however, has neither adopted the changed impact assessment method, nor has it recalculated the impacts. What’s more, we don’t see the change of the inventory reflected in our score - simply because it wasn’t recalculated. Only the inventory as well as the demand and supply arrays got changed. To recalculate our inventory with our newly chosen impact assessment method (and, if you like, an adjusted demand), we need to type the following:

demand2 = {process:3}
lca.redo_lcia(demand2)
lca.score

Additional stuff

Ok, enough with adjustments and recalculations. Let’s check out some matrices now:

  • the technosphere matrix:

lca.technosphere_matrix
print(lca.technosphere_matrix)
  • the biosphere matrix:

lca.biosphere_matrix
print(lca.biosphere_matrix)
  • the characterisation matrix:

lca.characterization_matrix
print(lca.characterization_matrix)
  • the inventory:

lca.inventory
print(lca.inventory)
  • the characterised inventory:

lca.characterized_inventory
print(lca.characterized_inventory)
  • and the demand as well as supply arrays:

lca.demand_array #contains what we entered as final demand. Check that it only contains this demand by summing up the whole array
lca.demand_array.sum()
lca.supply_array #equals the multiplication of our inverted transaction matrix by the final demand, i.e. (Ae-1)*y

Continue, or back to table of contents?


5. Foreground system#

For modeling a foreground system, a new database is created that is linked to ecoinvent via exchanges. Some steps need to be considered, exemplified below for two simple product systems.

5.1 How to create a simple product system?#

Now we want to create the product system for a fictitious aluminium waste handling process, consisting of inputs from the treatment of aluminium scrap as well as markets for transports and compressed air. For creating a product system, we need to create foreground processes that we link to background processes. For the latter, we can search in the background database. When we don’t know the exact name of an activity, we can first search for the parts of the name that we are sure about, e.g. low-voltage electricity:

Setup of technosphere

#this search does not yield the complete list as we will see later
eidb.search('electricity, low voltage')
#this search, however, does yield the complete list
for act in [act for act in eidb if 'electricity, low voltage' in act['name']]:
    print(act)

From this list, we can select the target activity through a more narrow description including indexing.

process = [act for act in eidb if 'electricity, low voltage' in act['name'] and 'CA-ON' in act['location']][0]
process

Have a closer look at the description of the target activity.

process.as_dict()

Side note: You can also find the name of an activity by its code:

eidb.get('cfd25c57d2a355b94813229866ae9f5d')

Now, let’s create and save a new activity for which we design the product system.

waste_handling = eidb.new_activity(code = 'test1', name = "Waste handling", unit = "unit")
waste_handling.save()

Include parameters

Adding some parameters on the project level works like the following. Mind that there are also parameters on the activity and database level. The commands for handling and saving those are quite similar; for more details, look here

project_data = [{
    'name': 'M',
    'amount': 0.06,
}, {
    'name': 'D',
    'amount': 200
}]

parameters.new_project_parameters(project_data)
# have a look at existing project parameters
for param in ProjectParameter.select():
    print(param, param.amount)

Let’s make use of the entered parameters by parametrising the input amounts!

aluminium = [act for act in eidb if act['name']=='treatment of aluminium scrap, post-consumer, prepared for recycling, at refiner' and 'RER' in act['location']][0]
waste_handling.new_exchange(input=aluminium.key,amount=0,unit="kilogram",type='technosphere', formula='M').save()
waste_handling.save()

transport = [act for act in eidb if act['name']=='market for transport, freight, lorry 3.5-7.5 metric ton, EURO5' and 'RoW' in act['location']][0]
waste_handling.new_exchange(input=transport.key,amount=0,unit="ton kilometer",type='technosphere', formula='D*M/1000').save()
waste_handling.save()

air = [act for act in eidb if act['name']=='market for compressed air, 600 kPa gauge' and 'GLO' in act['location']][0]
waste_handling.new_exchange(input=air.key,amount=0,unit="cubic meter",type='technosphere', formula='D*M/1000').save()
waste_handling.save()

Before the parameters become valid, we also need to save them by adding them to a group. Then we need to recalculate the exchanges based on the parameters.

parameters.add_exchanges_to_group("again another group", waste_handling)
ActivityParameter.recalculate_exchanges("again another group")

Now we can have a look at the inputs to our waste handling activity. There are two ways:

  • one that quantifies the input:

#Have a look at all exchanges of the selected activity
for exc in waste_handling.exchanges():
    print(exc)
  • and one that does not:

for act in waste_handling.technosphere():
    print(act.input)

Moreover, we can check on some details of our new activity:

#get general information on the new activity
act = eidb.get('test1')
act
#or like that
eidb.search('waste handling')

Delete parameters and activities

#delete a set of parameters; does not delete the parameter from the project parameters list?!?
name = ['M']
for name in ProjectParameter.select():
    name.delete()
#should delete the parameters but doesn't?!?
ProjectParameter.delete()
#remove the respective exchanges from the group; does not yet change anything about the exchanges?!?
parameters.remove_exchanges_from_group("again another group", waste_handling)
#should remove parameters, but does not really work
parameters.remove_from_group("again another group", waste_handling)
#Delete the list of project data (or any other dictionary); does not affect the parametrised exchanges until they get recalculated
project_data.clear() #.remove would only clear the content but leave empty instances
#quick check to see that it got deleted
for data in project_data:
    print(data)
#delete the table of existing project parameters, i.e. actually deletes the project parameters
ProjectParameter.drop_table(safe=True, drop_sequences=True)
#create a new empty table of project parameters
ProjectParameter.create_table()

for name in ProjectParameter.select():
    name.print()
#delete all waste handling activities that were accidentally created
for activity in [act for act in eidb if act['name']=='Waste handling' and 'GLO' in act['location']]:
    activity.delete()
#delete individual bottle production activity
#waste_handling.delete
#check that the activity actually got deleted
eidb.search('waste handling')

Seems like it…

5.2 Bottle production#

Okay, now that we know the basics for creating (and deleting) a simple product system, we will create another one, explore it, and perform an LCA on it. So, here’s a first short example on a simplified plastic bottle production. This process only has three inputs: electricity, transport, and polyethylene.

Setup of the product system

As mentioned earlier, the ‘eidb.search’ function does not give all the items that actually match the search criteria. We see this by comparing the following command with the one thereafter:

#incomplete search results
eidb.search('electricity production, photovoltaic, 3kWp slanted-roof installation,')#, filter={'location': 'CA-ON'})
#complete search results
for act in [act for act in eidb if 'electricity production, photovoltaic, 3kWp slanted-roof installation,' in act['name']]:# and 'CA-ON' in act['location']]:
    print(act)
#we can also sort our search results
list_to_be_sorted = [act for act in eidb if 'electricity production, photovoltaic, 3kWp slanted-roof installation,' in act['name']] # and 'CA-ON' in act['location']]:
newlist = sorted(list_to_be_sorted, key=lambda k: k['name'])

#for multiple keys, its works like this:
#newlist = sorted(list_to_be_sorted, key = itemgetter('name','categories'))
#or
#newlist = sorted(list_to_be_sorted, key=lambda k: (k['name'],k['categories']))

newlist
#now let's refine the search and pick one activity
[act for act in eidb if 'electricity production, photovoltaic, 3kWp slanted-roof installation,' in act['name'] and 'CA-ON' in act['location']][0]

mind: Activities do not have very many required fields; aside from database and code, the only other required field is name, but most activities will have a location and unit as well. If no type is specified for an activity, then the activity is assumed to be a process. Other types include product and biosphere for biosphere flows. Activity type is used to determine whether an activity should be placed in the biosphere or technosphere matrices during LCA calculations.

Let’s now create our parameterised product system:

#The following for-loop is only to guarantee that we can create a new bottle production process
for activity in [act for act in eidb if 'Bottle production' in act['name'] and 'GLO' in act['location']]:
    activity.delete()

# activities and exchanges for simple bottle production model
bottle_production = eidb.new_activity(code = 'test1', name = "Bottle production", unit = "unit")
#bottle_production.save()

project_data = [{
    'name': 'M',
    'amount': 0.06,
}, {
    'name': 'D',
    'amount': 200
}]

parameters.new_project_parameters(project_data)

for param in ProjectParameter.select():
    print(param, param.amount)

electricity = [act for act in eidb if act['name']=='electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted, label-certified' and 'CH' in act['location']][0]
bottle_production.new_exchange(input=electricity.key,amount=0,unit="kilowatt hour",type='technosphere', formula='20*M/3.6').save()
bottle_production.save()

#mind how the following activity is treated differently! The reason for this will be shown later
polyethylene = [act for act in eidb if act['name']=='market for polyethylene, high density, granulate' and 'GLO' in act['location']][0]
pe = bottle_production.new_exchange(input=polyethylene.key,amount=0,unit="kilogram",type='technosphere', formula='M')
pe.save()
bottle_production.save()

transport = [act for act in eidb if act['name']=='market for transport, freight, lorry 3.5-7.5 metric ton, EURO5' and 'RER' in act['location']][0]
bottle_production.new_exchange(input=transport.key,amount=0,unit="ton kilometer",type='technosphere', formula='D*M/1000').save()
bottle_production.save()

parameters.add_exchanges_to_group("again another group", bottle_production)
ActivityParameter.recalculate_exchanges("again another group")

Cross-check the exchanges

Let’s check whether everything went ok when creating our exchanges:

for exc in bottle_production.exchanges():
    print(exc)
bottle_production.as_dict()
#also this works:
for key in bottle_production:
    print(key, ':', bottle_production[key])

Ok, seems like everything is fine with the bottle production activity. But what about the inputs - can we check how they are actually linked? Sure! To see the inputs of, for instance, our polyethylene input, we type:

for exc in polyethylene.exchanges():
    print(exc)

And to see the outputs of this activity, i.e. the upstream exchanges across the whole database, we type:

for exc in polyethylene.upstream():
    print(exc)

… and yes, here it is. We can find our newly created exchange at the very bottom of the window!

Alternatively, we can also look straight for the output of a new exchange (and some other methods of the exchange). For doing so, it is important to have given a name to the exchange that you now want to examine further (as done when creating the product system).

pe.input, pe.output, pe.amount, pe.unit, pe.uncertainty_type

Mind that the amount equals to zero here. This might possibly be due to the fact that this amount is parametrised but was set to zero initially (or because of the parameter level, i.e. project in this case).

Troubleshooting

In case something went wrong during the setup of the product system, make sure to remove all parameters and the saved activities (as shown below) and rerun the window for creating the product system.

#Delete the list of project data (or any other dictionary); does not affect the parametrised exchanges until they get recalculated
project_data.clear() #.remove would only clear the content but leave empty instances
#quick check to see that it got deleted
for data in project_data:
    print(data)
#delete the table of existing project parameters, i.e. actually deletes the project parameters
ProjectParameter.drop_table(safe=True, drop_sequences=True)
#create a new empty table of project parameters
ProjectParameter.create_table()

for name in ProjectParameter.select():
    name.print()
#delete all bottle production activities that were accidentally created
for activity in [act for act in eidb if act['name']=='Bottle production' and 'GLO' in act['location']]:
    activity.delete()
#delete individual bottle production activity
#waste_handling.delete

Now let’s see whether really everthing got deleted:

for exc in bottle_production.exchanges():
    print(exc)

Definition of LCIA methods and functional unit

method_key = [m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'climate change' in str(m)][0]

functional_unit = {bottle_production:1}

LCA calculation

lca = bw.LCA(functional_unit,method_key)
lca.lci()
lca.lcia()
lca.demand
lca.method
lca.score

Elementary and product flows

There are a few ways to examine the elementary flows of our product system which are explained below. The same goes for product flows.

eidb.filepath_processed()
your_structured_array = np.load(eidb.filepath_processed())
pd.DataFrame(your_structured_array).head()
pd.Series(bw.mapping).head()
pd.DataFrame(lca.bio_params).head(6) 
lca.activity_dict
# Getting the key from the "demand" attribute:
act_key = list(lca.demand)[0].key
# Getting the column number from the activity_dict:
col_index = lca.activity_dict[act_key]
print("The column index for activity {} is {}".format(act_key, col_index))
#have the numbering, i.e. the value, in the beginning
myFirstLCA_rev_activity_dict = {value:key for key, value in lca.activity_dict.items()}
myFirstLCA_rev_activity_dict
lca_rev_act_dict, lca_rev_product_dict, lca_rev_bio_dict = lca.reverse_dict()

As done earlier for another product system, let’s check out the technosphere:

lca.technosphere_matrix
print(lca.technosphere_matrix)
from bw2analyzer.matrix_grapher import SparseMatrixGrapher
SparseMatrixGrapher(lca.biosphere_matrix).graph()
lca.technosphere_matrix
SparseMatrixGrapher(lca.technosphere_matrix).ordered_graph()

If you’re now wondering that this looks very similar to the result of the command eidb.graph_technosphere(), then you’re right. The only difference is that our foreground system is now included.

Before being able to get deeper insights, we need to prepare a few variables, all based on the column index, which we get like that:

print("As a reminder, the column index for  {} is  {}".format(act_key, col_index))

The column is given in compressed sparse row format. We can see that by doing as follows:

myColumn = lca.technosphere_matrix[:, col_index]
myColumn
print(myColumn)

Now let’s switch from the compressed sparse row format to the coordinate format. With that we can examine our columns a bit further :) And, interestingly, the print-outs of our columns look exactly the same.

myColumnCOO = myColumn.tocoo()
myColumnCOO

The output is unchanged for that:

print(myColumnCOO)
myColumnCOO.row

Let’s have a look again at the identifiers of our foreground activities:

[lca_rev_product_dict[i] for i in myColumnCOO.row]

Through these identifiers we can also get the names of our activities:

names_of_my_inputs = [bw.get_activity(lca_rev_product_dict[i])['name'] for i in myColumnCOO.row]
names_of_my_inputs

And those names can of course be linked to the actual amounts (which then looks much nicer in a table than above, only with the keys).

# First create a dict with the information I want:
myColumnAsDict = dict(zip(names_of_my_inputs,myColumnCOO.data))
# Create Pandas Series from dict
pd.Series(myColumnAsDict, name="A series with information on exchanges in my foreground process")

An alternative way to generate such a table is as follows, not depending on all the column-preparation…

pd.Series({bw.get_activity(exc.input)['name']:exc.amount for exc in bottle_production.technosphere()}, 
          name="alternative way to generate exchanges")

Which, to be honest, does not give more information than the command we already used a couple of times (the data is simply presented in a different way):

for exc in bottle_production.exchanges():
    print(exc)

Continue, or back to table of contents?


6. Manipulating an exchange#

In the following, we want to manipulate the electricity exchange. This we want to do by not only changing the amount, but by manipulating the underlying formula.

6.1 Copying and deleting activities#

First, let’s copy our bottle production product system!

bottle_production_copy = bottle_production.copy()

To see whether it really got copied, let’s have a look at the exchanges:

for exc in bottle_production_copy.exchanges():
    print(exc)
bottle_production_copy.save()

Ok, seems like it worked.

You can delete the entire technosphere of an activity like this:

bottle_production_copy.technosphere().delete()

If you even want to delete the activity itself, you can do it this way:

bottle_production_copy.delete()

Check whether the activity is still in the database:

bottle_production_copy in eidb

But now let’s get our copy back and start working with it:

bottle_production_copy = bottle_production.copy()
bottle_production_copy.save()
bottle_production_copy.as_dict()

6.2 First attempts at manipulating an exchange’s keys/values#

First a short reminder what an exchange of our activity looks like. Let’s for example take the first exchange, which is electricity. There are different ways of getting (almost) the same information:

[exc for exc in bottle_production_copy.exchanges()][0]
print([exc for exc in bottle_production_copy.exchanges()][0].items)
[exc for exc in bottle_production_copy.exchanges()][0].as_dict()

And quite obviously the output of this exchange is our bottle production…

[exc for exc in bottle_production_copy.exchanges()][0].output

Let’s have a look at the formula of our exchange of interest - there’s again two ways of doing so:

[exc for exc in bottle_production_copy.exchanges()][0]['formula']
print([exc for exc in bottle_production_copy.exchanges()][0].get('formula'))

Let’s see if we can manipulate this formula or the exchange in general:

#add another key with value
[exc for exc in bottle_production_copy.exchanges()][0]['key']='value'
del [exc for exc in bottle_production_copy.exchanges()][0]['key']
#change the formula and by that the amount of our exchange
#[exc for exc in bottle_production_copy.exchanges()][0].update({'formula': '40*M/3.6'})
list(exc for exc in bottle_production_copy.exchanges())[0].update({'formula': '40*M/3.6'})
[exc for exc in bottle_production_copy.exchanges()][0].save()
bottle_production_copy.save()
[exc for exc in bottle_production_copy.exchanges()][0]
list(exc for exc in bottle_production_copy.exchanges())[0]
example = [exc for exc in bottle_production_copy.exchanges()][0]
print(example.update({'formula': '40*M/3.6'}))
example
example.update? #no the update command is used differently.
bottle_production_copy.substitution? #no, the substitution command is used for something else.
[exc for exc in bottle_production_copy.exchanges()][0].update
#change the formula and by that the amount of our exchange
[exc for exc in bottle_production_copy.exchanges()][0]['formula']='40*M/3.6'
#see if the formula in the respective exchange changed
[exc for exc in bottle_production_copy.exchanges()][0].as_dict()

Ok, so we see that nothing of this really works. Maybe we’ll still find a solution for doing so. Actually we are super close, jsut a tiny part needs to be changed. But for now, let’s try to substitute an exchange first.

6.3 Substituting an exchange#

So, let’s first delete our activity and then recreate it:

#delete individual bottle production activity
bottle_production_copy.delete()
#delete all bottle production activities that were accidentally created
for activity in [act for act in eidb if act['name']=='Bottle production copy' and 'GLO' in act['location']]:
    activity.delete()
bottle_production_copy = bottle_production.copy(code='test2', name="Bottle production copy")
bottle_production_copy.save()

Check whether the copy was actually created and saved:

eidb.search('Bottle production')#, filter={'code': 'test2'})

Let’s have a look at the exchanges of our production system:

[exc for exc in bottle_production_copy.exchanges()]

Once again, let’s see the details of our exchange of interest, i.e. electricity:

[exc for exc in bottle_production_copy.exchanges()][0].as_dict()

Reminder: We can also see all upstream exchanges of our activity of interest, i.e. electricity production:

[exc for exc in electricity.upstream()]

#same as:
#for exc in electricity.upstream():
#    print(exc)

Now let’s delete an exchange either from the upstream or the downstream perspective:

#downstream
[exc for exc in bottle_production_copy.exchanges()][0].delete()
#upstream
[exc for exc in electricity.upstream()][2].delete()

Now we add a new exchange as replacement for our deleted one. This action has to be validated by adding the exchange to the group so that the amount (calculated through a formula) is correct.

#this is our replacement for the original electricity input
el_subst = [act for act in eidb if act['name']=='electricity production, photovoltaic, 3kWp slanted-roof installation, multi-Si, panel, mounted, label-certified' and 'CH' in act['location']][0]
bottle_production_copy.new_exchange(input=el_subst.key,amount=0,unit="kilowatt hour",type='technosphere', formula='40*M/3.6').save()
bottle_production_copy.save()

parameters.add_exchanges_to_group("again another group", bottle_production_copy)
ActivityParameter.recalculate_exchanges("again another group")

Let’s check our exchanges and whether we got what we wanted!

[exc for exc in bottle_production_copy.exchanges()]

Yesss!!! Now, a more in-detail look at our exchange of interest (especially regarding the formula and the amount):

[exc for exc in bottle_production_copy.exchanges()][2].as_dict()

6.4 Successfully manipulating an exchange#

Now let’s finally manipulate a key,value pair of an exchange. There are two ways for doing so, depending on whether you want to assign a name to your exchange. Alternative A is the long version, alternative B the short one.

  • Alternative A

Here, we want to first add another exchange to our production system, assign a variable to it, and then change its amount through an adjustment of its formula.

#add another electricity source to our bottle production system
el_extra = [act for act in eidb if act['name']=='electricity production, photovoltaic, 3kWp slanted-roof installation, single-Si, panel, mounted, label-certified' and 'CH' in act['location']][0]

el_ext = bottle_production_copy.new_exchange(input=el_extra.key,amount=0,unit="kilowatt hour",type='technosphere', formula='10*M/3.6')
el_ext.save()
bottle_production.save()

parameters.add_exchanges_to_group("again another group", bottle_production_copy)
ActivityParameter.recalculate_exchanges("again another group")
#check our exchanges
[exc for exc in bottle_production_copy.exchanges()]
#have a look at the created exchange
el_ext.as_dict()
#let's change the amount of this exchange through an adjustment of the formula

#btw: for getting a subset of the exchange-dictionary, you can type (to get the formula):
#wanted_key = ['formula']
#formula = dict((k, el_ext[k]) for k in wanted_key if k in el_ext)

el_ext['formula']='20*M/3.6'

el_ext.save()
bottle_production.save()

parameters.add_exchanges_to_group("again another group", bottle_production_copy)
ActivityParameter.recalculate_exchanges("again another group")
#check whether it worked and got adopted in the product system
[exc for exc in bottle_production_copy.exchanges()]

Great! It worked.

  • Alternative B

A quicker way for changing the formula of an exchange is to do the following. This also allows to manipulate exchanges that have no name (as compared to el_ext in the above example); you just need to give an intermediate name to the respective exchange.

el_exc=[exc for exc in bottle_production_copy.exchanges()][3]
el_exc['formula']='30*M/3.6'
el_exc

el_exc.save()
bottle_production.save()

parameters.add_exchanges_to_group("again another group", bottle_production_copy)
ActivityParameter.recalculate_exchanges("again another group")
#let's see whether our change got adopted in the exchange-dictionary
[exc for exc in bottle_production_copy.exchanges()][3].as_dict()
#check whether it is also included in the product system
[exc for exc in bottle_production_copy.exchanges()]

Yeah, awesome! Now we know how to do that.

Troubleshooting

If anything went wrong when copying or manipulating an activity, you can do the following things and restart - but if there’s no error message anywhere, don’t execute these following commands, because we will use our bottle production systems later again:

  • Only delete a selected exchange (via two ways) - be sure to index correctly, otherwise you may have to reinstantiate your database:

[exc for exc in bottle_production_copy.exchanges()][0].delete()
[exc for exc in electricity.upstream()][2].delete()
  • Delete all exchanges that you copied accidentally:

for act in [act for act in eidb if 'Bottle production copy' in act['name'] and 'GLO' in act['location']]:
    act.delete()

Continue, or back to table of contents?


7. Basic contribution analysis#

Based on the bottle production product system: Let’s have a bit of a closer look at the inventory and the contributions of individual exchanges/activities!

But before that, we recalculate our inventory:

7.1 Recalculation of the inventory#

#these are our method and functional unit, same as above in section 5.
method_key = [m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'climate change' in str(m)][0]

functional_unit = {bottle_production:1}
lca = bw.LCA(functional_unit, method_key)
lca.lci()
lca.lcia()
lca.demand
lca.method
print("The {} process accounts for {:f} {}.".format(
    list(functional_unit.keys())[0]['name'],
    lca.score,
    bw.methods.get(method_key).get('unit')
    ))

And now over to the contribution analysis. Let’s find the most damaging activities and biosphere flows.

7.2 Basic contribution analysis#

import bw2analyzer as bwa
ca = bwa.ContributionAnalysis()
ca.annotated_top_processes(lca, limit=5) #returns a list of tuples: (lca score, supply amount, activity name)

We can also set a limit below one, which will filter by fraction of impact instead of number of activities; we can also return activity keys instead of names.

#change "False" to "True" and this command will give a similar result as the following one, only that we set a limit here
ca.annotated_top_processes(lca, names=False, limit=0.001, limit_type='percent')

Another way of listing the most contributing activities is this:

lca.top_activities() #this command essentially relies on the annotated_top_process command from above. Hence, the output is given as (lca score, supply amount, activity name)

And of course we can do the same for elementary flows:

ca.annotated_top_emissions(lca, limit=0.02, limit_type='percent')
#or like that
lca.top_emissions()

Notice that you can use the bw2analyzer.ContributionAnalysis method for analysing your results further, for instance through a Hinton matrix - see for yourself:

ca.hinton_matrix(lca, rows=10, cols=10)

7.3 Top emissions and processes by name#

In addition to the basic contribution analysis, we can also look at individual activities (columns) or flows (rows).

What if we want to group names together, i.e. to get the total impact for all “phosphates”? There isn’t a built-in function for this, but it is relatively easy to do. Let’s start with creating different lists:

from collections import defaultdict

#get the keys for each biosphere flow of the same name
all_unique_names_and_their_keys = defaultdict(list)

for flow in bw.Database("biosphere3"):
    if flow.key in lca.biosphere_dict:
        all_unique_names_and_their_keys[flow['name']].append(flow.key)
    
all_unique_names_and_their_keys
#get the rows for all biosphere flows of the same name
all_unique_names_and_their_rows = {
    name: [lca.biosphere_dict[key] for key in keys] 
    for name, keys in all_unique_names_and_their_keys.items()
}

all_unique_names_and_their_rows
#get the scores for all biosphere flows of the same name
all_unique_names_and_their_scores = {
    name: [lca.characterized_inventory[row, :].sum() for row in rows]
    for name, rows in all_unique_names_and_their_rows.items()
}

all_unique_names_and_their_scores

Now, let’s sort this list by the score and only display the first 10 results.

#get the first 10 scores of the most contributing biosphere flows
sorted_scores = sorted(
    [(sum(scores), name) for name, scores in all_unique_names_and_their_scores.items()], 
    reverse=True
)

sorted_scores[:10]

We can also encapsulate all this functionality in a single function.

from collections import defaultdict

def top_emissions_by_name(lca, biosphere_database='biosphere3'):
    names = defaultdict(list)

    for flow in bw.Database("biosphere3"):
        if flow.key in lca.biosphere_dict:
            names[flow['name']].append(
                lca.characterized_inventory[lca.biosphere_dict[flow.key], :].sum()
            )
    
    return sorted(
        [(sum(scores), name) for name, scores in names.items()], 
        reverse=True
    )
top_emissions_by_name(lca)[:5]

We can see that this function also groups emissions according to their type, e.g. all fossil carbon dioxide emissions are summed to “Carbon dioxide, fossil”, as compared to the command in the top ca.annotated_top_emissions.

Possibly we can do the same for the technosphere flows with only slight adaptations to the above function:

from collections import defaultdict

def top_processes_by_name(lca, technosphere_database='ecoinvent 3.5_cutoff_ecoSpold02'):
    names = defaultdict(list)

    for flow in eidb:
        if flow.key in lca.activity_dict:
            names[flow['name']].append(
                lca.characterized_inventory[:, lca.activity_dict[flow.key]].sum()
            )
    
    return sorted(
        [(sum(scores), name) for name, scores in names.items()], 
        reverse=True
    )
top_processes_by_name(lca)[:5]

Let’s check whether both the top_processes_by_name and the top_emissions_by_name equal our lca.score:

l = [t[0] for t in top_processes_by_name(lca)]
sum(l)
l = [t[0] for t in top_emissions_by_name(lca)]
sum(l)
lca.score

Well, I guess we can say that is close enough, as these tiny deviations may be due to some rounding errors.

Continue, or back to table of contents?


8. Multi-LCA#

To speed up comparative LCA, i.e. to compare multiple product alternatives (possibly across multiple impact categories), we can use the M-LCA functionality.

8.1 Manually comparing products#

The original code for such a comparative static LCA can be found here. Here, we want to look at bananas instead of dairy. So let’s see which activities we have in ecoinvent:

bananas_unsorted = [act for act in eidb if 'banana' in act['name']]
bananas = sorted(bananas_unsorted, key=lambda k: k['name'])
bananas

Of these activities, we now select the banana production processes for Colombia, Costa Rica, and India, and we select three impact methods on climate change, land use, and water stress.

banana_CO = [act for act in eidb if 'banana' in act['name'] and 'CO' in act['location']][0]
banana_CR = [act for act in eidb if 'banana' in act['name'] and 'CR' in act['location']][0]
banana_IN = [act for act in eidb if 'banana' in act['name'] and 'IN' in act['location']][0]

inventory = [banana_CO, banana_CR, banana_IN]

methods = [[m for m in bw.methods if 'ReCiPe Midpoint (H)' in str(m) and 'climate change' in str(m)][0],
           [m for m in bw.methods if 'ReCiPe Midpoint (H)' in str(m) and  'agricultural land occupation' in str(m) and not 'w/o LT' in str(m) ][0],
           [m for m in bw.methods if 'ReCiPe Midpoint (H)' in str(m) and  'water depletion' in str(m) and not 'w/o LT' in str(m) and not 'V1.13' in str(m)][0]]

print("Let's compare\n{},\n{}, and\n{}".format(banana_CO, banana_CR, banana_IN))
results = []

for banana in inventory:
    lca = bw.LCA({banana:1})
    lca.lci()
    for method in methods:
        lca.switch_method(method)
        lca.lcia()
        results.append((banana["name"], banana["location"], method[1].title(), lca.score, bw.methods.get(method).get('unit')))
results

We should probably present these results in a nicer form. Let’s use pandas for that:

results_df = pd.DataFrame(results, columns=["Name", "Location", "Method", "Score", "Unit"])
results_df = pd.pivot_table(results_df, index = ["Name", "Location"], columns = ["Method", "Unit"], values = "Score")
results_df
df = pd.DataFrame.from_dict(results).T
df

We can also normalise these results, which may help to get a better overview:

NormResults_df = results_df / results_df.max()
NormResults_df

Sidenote: We can certainly run this kind of comparative LCA with our own product systems, e.g. the bottle production from above. We would just have to select the respective reference flow…

8.2 Calculation setups#

A faster way of running comparative LCAs is through calculation setups. So, let’s get familiar with them! This can be done best by just employing random activities as funcitonal units and selecting the LCIA methods which can be applied to the present biosphere flows (only a subset of these ones). Due to computation time, we decide to use the FORWAST database instead of ecoinvent - although this certainly works, too.

# define the functional units and LCIA methods; taken from example notebook
functional_units = [{fw.random(): 1} for _ in range(20)]

import random

all_forwast_flows = {exc.input for ds in fw for exc in ds.biosphere()}
suitable_methods = [method 
                    for method in bw.methods 
                    if {cf[0] for cf in Method(method).load()}.intersection(all_forwast_flows)]

print("Can use {} of {} LCIA methods".format(len(suitable_methods), len(bw.methods)))
chosen_methods = random.sample(suitable_methods, 8)
functional_units
chosen_methods

Now we come to the actual calculation setup which gets defined through our functional units and the chosen LCIA methods:

my_calculation_setup = {'inv': functional_units, 'ia': chosen_methods}
calculation_setups['set of calculation setups'] = my_calculation_setup
mlca = MultiLCA('set of calculation setups')
mlca.results

Hm, does not seem to be so difficult. Therefore, let’s try it on a better chosen example.

8.3 MLCA#

Let’s decide on our LCIA method selection:

[m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'land' in str(m)][:3]

Now a different way fo setting up our list of selected LCIA methods:

land_methods = [m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'land' in str(m)][:3]
methods = land_methods + [[m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'climate change' in str(m)][0],
          [m for m in bw.methods if 'ReCiPe' in str(m) and  'Midpoint (H)' in str(m) and 'water depletion' in str(m)][0]]
print(methods)

And now we calculate the results for one functional unit (our banana production from above) and multiple LCIA methods:

One functional unit, multiple impact categories

all_scores = {}
banana_lca = bw.LCA({banana_CO:1}, methods[0])
banana_lca.lci()
banana_lca.lcia()
for category in methods:
    banana_lca.switch_method(category)
    banana_lca.lcia()
    all_scores[category] = {}
    all_scores[category]['score'] = banana_lca.score
    all_scores[category]['unit'] = bw.Method(category).metadata['unit']
    print("The score is {:f} {} for impact category {}".format(banana_lca.score, 
                                                 bw.Method(category).metadata['unit'],
                                                 bw.Method(category).name)
    )

We can certainly present these results also in a nice table:

df = pd.DataFrame.from_dict(all_scores).T
df

And now let’s visualise the results only for the scores that have the same unit, i.e. in this example agricultural and urban land occupation.

%matplotlib inline
import matplotlib.pyplot as plt
df_m2 = df[df['unit']=='square meter-year']
df_m2
df_m2.plot(kind='barh')

And of course we can find the relative contributions for our selected functional unit, too:

banana_lca_unitProcessContribution = banana_lca.characterized_inventory.sum(axis=0).A1
banana_lca_unitProcessRelativeContribution = banana_lca_unitProcessContribution/banana_lca.score
banana_lca_unitProcessRelativeContribution

And now we calculate the results for multiple functional units and multiple LCIA methods:

Multiple functional units, multiple impact categories

#Set up the production system
prod_sys = [{banana_CO.key:1}, {banana_CR.key:1}, {banana_IN.key:1}]
prod_sys
bw.calculation_setups['multiLCA'] = {'inv': prod_sys, 'ia': methods}
bw.calculation_setups['multiLCA']
myMultiLCA = bw.MultiLCA('multiLCA')
myMultiLCA.results

df_impact = pd.DataFrame(data = myMultiLCA.results, columns = methods)
df_impact

Just in case we cannot remember for some reason our functional units, we can easily find their definition again:

myMultiLCA.func_units

The same certainly goes for our chosen LCIA methods:

myMultiLCA.methods

Continue, or back to table of contents?


9. Monte Carlo simulation#

According to British statistician George Cox “all models are wrong but some are useful” - in that light, quantifying uncertainty makes LCA results more reliable/robust. One of the easiest/ most common ways to do so is to perform a Monte Carlo simulation, a random sampling technique. Before elaborating on that, we need to understand how uncertainty information is stored in Brightway:

Uncertainty information in Brightway#

Uncertainty is stored at the level of the exchanges - remember this! So, let’s have a look at a random ecoinvent exchange:

[exc for exc in eidb.random().exchanges()][0].as_dict()

The necessary uncertainty information of an exchange is described in the following fields:

  • ‘uncertainty type’ : type of probability distribution function that the exchange follows. For example, the uncertainty type = 2 indicates a lognormal distribution.

  • ‘loc’, ‘scale’, ‘shape’, ‘minimum’, ‘maximum’: parameters of the distribution function, which are respectively the location (mean \(\mu\), mode, offset, or median), scale (e.g. standard deviation \(\sigma\)), and shape as well as minimum and maximum of the underlying distribution. Mind that different distribution functions require different parameters - not all parameters have to be defined for each distribution.

Some additional uncertainty related information (‘scale without pedigree’, ‘pedigree’) are also there, but are not directly used in the calculation of the uncertainty. They are also specific to ecoinvent.

Uncertainty in Brightway is dealt with using a Python package called stats_arrays (see here), developed by Chris Mutel in the context of the development of Brightway but applicable to any stochastic model in Python. Have a look at it to see which probability distribution functions are available. And then, let’s import this package!

import stats_arrays

Just to give a brief example of how the uncertainty information “works”, let’s have a look at the lognormal distribution. As a reminder:

  • a random variable \(X\) is a lognormal if its natural logarithm \(ln(X)\) is normally distributed

  • the natural logarithm of the median of the lognormal distribution is equal to the median (=mean) of the underlying distribution

Taking the deterministic amount amount to be the median, we should have loc = ln('amount'). Let’s do this for the first exchange of a random ecoinvent activity that has a lognormal distribution:

#run this cell again if the output is empty
e = [exc for exc in eidb.random().exchanges() if exc['uncertainty type'] == 2][0]
e.as_dict()
e['loc'] == np.log(e['amount'])

Basic Monte Carlo simulation#

One functional unit, one impact category

#MC simulation with 10 iterations for our banana production and the IPCC GWP100a method
mc = bw.MonteCarloLCA({banana_CO:1},  ('IPCC 2013', 'climate change', 'GWP 100a'))
scores = [next(mc) for _ in range(10)]
scores

#or this way:
#for _ in range(10):
#    print(next(mc))

Let’s print the results of our MC:

import matplotlib.pyplot as plt
plt.hist(scores,bins=40)

Well, this doesn’t really look like anything yet. We should increase the number of iterations - which yields more robust results. Around 10,000 iterations is a good starting point. However, this takes a while, so we will just give it a try with 1000 now. See for yourself how long it would take for a higher number of iterations.

#define the number of iterations
n_runs = 1000
%%time
values = [next(mc) for _ in range(n_runs)]
plt.hist(values, bins=40)

We can also run the MCs in parallel which speeds up the computation time - see the following and compare the calculation times!

# We can also create parallel Monte Carlo that will run simultations on multiple workers simultaneoulsy
bw.ParallelMonteCarlo?
pmc = bw.ParallelMonteCarlo(mc.demand, mc.method, iterations=n_runs)
%%time
values = pmc.calculate()
plt.hist(values, bins=40)

One functional unit, multiple impact categories

# define the function for MC simulation
def multiImpactMonteCarloLCA(functional_unit, list_methods, iterations):
    # Step 1
    MC_lca = bw.MonteCarloLCA(functional_unit)
    MC_lca.lci()
    # Step 2
    C_matrices = {}
    # Step 3
    for method in list_methods:
        MC_lca.switch_method(method)
        C_matrices[method] = MC_lca.characterization_matrix
    # Step 4
    results = np.empty((len(list_methods), iterations))
    # Step 5
    for iteration in range(iterations):
        next(MC_lca)
        for method_index, method in enumerate(list_methods):
            results[method_index, iteration] = (C_matrices[method]*MC_lca.inventory).sum()
    return results

# define the LCIA methods, functional unit, and the number of iterations
ILCD = [method for method in bw.methods if "ILCD" in str(method) and "no LT" not in str(method)]
fu = {banana_CO:1}
iterations = 100

# let it run!
test_results = multiImpactMonteCarloLCA(fu, ILCD, iterations)
test_results
ILCD
#we can get the plots for all 34 ILCD methods (--> type: len(ILCD)) by changing the index in the brackets
plt.hist(test_results[33], bins=40)

If you are interested in applying a sensitivity analysis, check out this notebook.


At this point, it’s worth pointing again at the Activity browser and LCOPT. These two Brightway2-extensions serve different purposes and require different settings, so check them out if you’re curious.


And now, last but not least, here’s our reward for finishing this notebook :)

from IPython.display import Image
Image(url="https://cdn.shopify.com/s/files/1/1869/0319/products/ART-dabbing-unicorn_color-powder-blue_1024x1024.jpg?v=1523903512")

Back to table of contents?


Troubleshooting#

Add his part from Section 1