{ "cells": [ { "cell_type": "markdown", "id": "0532003c", "metadata": {}, "source": [ "# Life Cycle Assessment (LCA) \n", "\n", "*Click the badge below to try this tutorial interactively in your browser:*\n", "\n", "[![Launch Binder](../images/custom_binder_logo.svg)](https://mybinder.org/v2/gh/QSD-Group/QSDsan-env/main?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252FQSD-group%252FQSDsan%26urlpath%3Dlab%252Ftree%252FQSDsan%252Fdocs%252Fsource%252Ftutorials%26branch%3Dmain)\n", "\n", "*You can also run this tutorial in [Google Colab](https://colab.research.google.com). It takes a one-time setup per session: follow the [Colab instructions](https://qsdsan.readthedocs.io/en/latest/tutorials/index.html#run-in-colab).*\n", "\n", "- **Prepared by:**\n", "\n", " - [Yalin Li](https://github.com/yalinli2)\n", "\n", "- **Learning objectives.** After this tutorial, you will be able to:\n", "\n", " - Define `ImpactIndicator` and `ImpactItem` instances\n", " - Run an `LCA` on a `System`\n", " - Interpret environmental footprints by category and contributor, and compare systems\n", "\n", "- **Prerequisites:** [6. System](https://qsdsan.readthedocs.io/en/latest/tutorials/6_System.html), [7. TEA](https://qsdsan.readthedocs.io/en/latest/tutorials/7_TEA.html)\n", "\n", "* **Covered topics:**\n", "\n", " - 1. ImpactIndicator\n", " - 2. ImpactItem and StreamImpactItem\n", " - 3. Build the system for LCA\n", " - 4. Construction, Transportation, and other activities\n", " - 5. LCA\n", "\n", "> **Companion video.** A walkthrough of this tutorial is available on [YouTube](https://youtu.be/ULmFYO8nTrM), presented by [Tori Morgan](https://github.com/vlmorgan93). Recorded against `QSDsan` v1.2.0. The concepts still apply, but if the code on screen differs from this notebook, follow the notebook." ] }, { "cell_type": "markdown", "id": "a6d91f382501", "metadata": {}, "source": [ "\n", "\n", "## Setup\n", "\n", "Import `QSDsan` and confirm the installed version.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "6f42d212", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:27.566419Z", "iopub.status.busy": "2026-05-30T19:57:27.566419Z", "iopub.status.idle": "2026-05-30T19:57:43.743594Z", "shell.execute_reply": "2026-05-30T19:57:43.742582Z" }, "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This tutorial was made with qsdsan v1.5.3.\n" ] } ], "source": [ "import qsdsan as qs\n", "print(f'This tutorial was made with qsdsan v{qs.__version__}.')" ] }, { "cell_type": "markdown", "id": "72063ae2", "metadata": {}, "source": [ "## 1. `ImpactIndicator` \n", "LCA is a bit more complicated than TEA since there are multiple indicators we can choose from and we might want to see the life cycle impact assessment (LCIA) results for multiple indicators. Therefore, `qsdsan` implements multiple new classes for LCA, and the one to start with is the `ImpactIndicator` class." ] }, { "cell_type": "markdown", "id": "83c37b25", "metadata": {}, "source": [ "When initializing a new `ImpactIndicator` instance, `ID` and `unit` are the most important attributes, `alias` is for convenience, and `method`, `category`, and `description` are mostly for record purpose." ] }, { "cell_type": "code", "execution_count": 2, "id": "b4abad30", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.745606Z", "iopub.status.busy": "2026-05-30T19:57:43.745606Z", "iopub.status.idle": "2026-05-30T19:57:43.751344Z", "shell.execute_reply": "2026-05-30T19:57:43.749892Z" } }, "outputs": [], "source": [ "# Assume we are mostly interested in global warming potential and fossil energy consumption\n", "GWP = qs.ImpactIndicator(ID='GlobalWarming', method='TRACI', category='environmental impact', unit='kg CO2-eq',\n", " description='Effect of climate change measured as global warming potential.')\n", "FEC = qs.ImpactIndicator(ID='FossilEnergyConsumption', alias='FEC', unit='MJ')" ] }, { "cell_type": "code", "execution_count": 3, "id": "c3985a27", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.751344Z", "iopub.status.busy": "2026-05-30T19:57:43.751344Z", "iopub.status.idle": "2026-05-30T19:57:43.756183Z", "shell.execute_reply": "2026-05-30T19:57:43.756183Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactIndicator: GlobalWarming as kg CO2-eq\n", " Alias : None\n", " Method : TRACI\n", " Category : environmental impact\n", " Description: Effect of climate change ...\n", "ImpactIndicator: FossilEnergyConsumption as MJ\n", " Alias : FEC\n", " Method : None\n", " Category : None\n", " Description: None\n" ] } ], "source": [ "GWP.show()\n", "FEC.show()" ] }, { "cell_type": "code", "execution_count": 4, "id": "0cad34a4", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.758720Z", "iopub.status.busy": "2026-05-30T19:57:43.756183Z", "iopub.status.idle": "2026-05-30T19:57:43.761358Z", "shell.execute_reply": "2026-05-30T19:57:43.761358Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactIndicator: GlobalWarming as kg CO2-eq\n", " Alias : GWP\n", " Method : TRACI\n", " Category : environmental impact\n", " Description: Effect of climate change ...\n" ] } ], "source": [ "# You can set alias for indicators (you can also set it during initiation as in the example above for FEC)\n", "GWP.alias = 'GWP'\n", "GWP.show()" ] }, { "cell_type": "code", "execution_count": 5, "id": "8d0491fe", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.761358Z", "iopub.status.busy": "2026-05-30T19:57:43.761358Z", "iopub.status.idle": "2026-05-30T19:57:43.767608Z", "shell.execute_reply": "2026-05-30T19:57:43.767608Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactIndicator: FossilEnergyConsumption as MJ\n", " Alias : FEC\n", " Method : None\n", " Category : None\n", " Description: None\n" ] } ], "source": [ "# You can also retrieve an `ImpactIndicator` through its ID or alias\n", "qs.ImpactIndicator.get_indicator('FEC').show()" ] }, { "cell_type": "code", "execution_count": 6, "id": "efad7ef4", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.769468Z", "iopub.status.busy": "2026-05-30T19:57:43.769468Z", "iopub.status.idle": "2026-05-30T19:57:43.775545Z", "shell.execute_reply": "2026-05-30T19:57:43.775545Z" } }, "outputs": [ { "data": { "text/plain": [ "{'GlobalWarming': ,\n", " 'FossilEnergyConsumption': }" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Or get all defined impact indicators\n", "qs.ImpactIndicator.get_all_indicators()" ] }, { "cell_type": "markdown", "id": "38355958", "metadata": {}, "source": [ "## 2. `ImpactItem` and `StreamImpactItem` \n", "Once you have impact indicators, you can start adding impact items and specifying the environmental impacts for each functional unit of the item." ] }, { "cell_type": "markdown", "id": "b0b86c12", "metadata": {}, "source": [ "### 2.1. `ImpactItem`\n", "For example, assume we need some electricity, and that to generate 1 kWh of electricity, 0.25 kg of CO2 is emitted, and it uses 4500 kJ of fossil energy." ] }, { "cell_type": "code", "execution_count": 7, "id": "40706431", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.775545Z", "iopub.status.busy": "2026-05-30T19:57:43.775545Z", "iopub.status.idle": "2026-05-30T19:57:43.782263Z", "shell.execute_reply": "2026-05-30T19:57:43.781258Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : electricity [per kWh]\n", "Price : None USD\n", "ImpactIndicators:\n", " None\n" ] } ], "source": [ "electricity = qs.ImpactItem('electricity', functional_unit='kWh')\n", "electricity.show()" ] }, { "cell_type": "markdown", "id": "89d6f9e0", "metadata": {}, "source": [ "
\n", "\n", "**Note:** `ImpactItem` also has a `price` attribute that allows you to put in the price for the impact item, but it is mainly used for construction items and will be included in unit CAPEX, if you want to account for the cost of electricity, you should set the power utility usage of the unit and modify `qsdsan.PowerUtility.price`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 8, "id": "65e5b2ea", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.782263Z", "iopub.status.busy": "2026-05-30T19:57:43.782263Z", "iopub.status.idle": "2026-05-30T19:57:43.789509Z", "shell.execute_reply": "2026-05-30T19:57:43.789509Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : electricity [per kWh]\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 0.25\n" ] } ], "source": [ "# We can add characterization factors (CFs) for the two impact indicators GWP and FEC\n", "electricity.add_indicator(GWP, 0.25)\n", "electricity.show()" ] }, { "cell_type": "code", "execution_count": 9, "id": "a812ff09", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.792029Z", "iopub.status.busy": "2026-05-30T19:57:43.792029Z", "iopub.status.idle": "2026-05-30T19:57:43.796611Z", "shell.execute_reply": "2026-05-30T19:57:43.796611Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : electricity [per kWh]\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 0.25\n", "FossilEnergyConsumption (MJ) 4.5\n" ] } ], "source": [ "# If you provide unit when adding the CF value, qsdsan will do the unit conversion\n", "electricity.add_indicator(FEC, 4500, 'kJ')\n", "electricity.show()" ] }, { "cell_type": "code", "execution_count": 10, "id": "87f31eeb", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.798370Z", "iopub.status.busy": "2026-05-30T19:57:43.798370Z", "iopub.status.idle": "2026-05-30T19:57:43.803519Z", "shell.execute_reply": "2026-05-30T19:57:43.803519Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : electricity [per kWh]\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 0.25\n", "FossilEnergyConsumption (MJ) 50\n" ] } ], "source": [ "# If you later want to change this value, you can also do it\n", "electricity.CFs['FossilEnergyConsumption'] = 50\n", "electricity.show()" ] }, { "cell_type": "code", "execution_count": 11, "id": "2eb90c10", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.803519Z", "iopub.status.busy": "2026-05-30T19:57:43.803519Z", "iopub.status.idle": "2026-05-30T19:57:43.811149Z", "shell.execute_reply": "2026-05-30T19:57:43.811149Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : electricity [per kWh]\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 0.25\n", "FossilEnergyConsumption (MJ) 50\n" ] }, { "data": { "text/plain": [ "{'electricity': }" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Similar to `ImpactIndicator`, you can retrieve one `ImpactItem` by its ID,\n", "# or see all defined impact items\n", "qs.ImpactItem.get_item('electricity').show()\n", "qs.ImpactItem.get_all_items()" ] }, { "cell_type": "markdown", "id": "91d6bd1c", "metadata": {}, "source": [ "**Loading many at once.** Real studies define dozens of indicators and items. Rather than creating each by hand, `ImpactIndicator.load_from_file(path)` and `ImpactItem.load_from_file(path)` load them in bulk. Both accept either a path to an Excel workbook or a dict of `pandas.DataFrame` objects (one per sheet). For items, the `info` sheet lists each item's `ID`, `functional_unit`, and `kind`, and one sheet per indicator (named by the indicator ID or alias) gives the characterization factors. The small example below loads two construction materials at once:" ] }, { "cell_type": "code", "execution_count": 12, "id": "s2-load-data", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.811149Z", "iopub.status.busy": "2026-05-30T19:57:43.811149Z", "iopub.status.idle": "2026-05-30T19:57:43.823347Z", "shell.execute_reply": "2026-05-30T19:57:43.823347Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
IDfunctional_unitkind
0SteelkgImpactItem
1Concretem3ImpactItem
\n", "
" ], "text/plain": [ " ID functional_unit kind\n", "0 Steel kg ImpactItem\n", "1 Concrete m3 ImpactItem" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "# load_from_file takes a path to an Excel workbook or, as here, a dict of DataFrames\n", "# (one per sheet). The 'info' sheet lists each item; it looks like this:\n", "item_data = {\n", " 'info': pd.DataFrame({'ID': ['Steel', 'Concrete'],\n", " 'functional_unit': ['kg', 'm3'],\n", " 'kind': ['ImpactItem', 'ImpactItem']}),\n", " 'GWP': pd.DataFrame({'ID': ['Steel', 'Concrete'],\n", " 'unit': ['kg CO2-eq', 'kg CO2-eq'],\n", " 'expected': [2.0, 300.0]}),\n", " 'FEC': pd.DataFrame({'ID': ['Steel', 'Concrete'],\n", " 'unit': ['MJ', 'MJ'],\n", " 'expected': [25.0, 900.0]}),\n", "}\n", "item_data['info']" ] }, { "cell_type": "markdown", "id": "s2-load-ind-md", "metadata": {}, "source": [ "There is then one sheet per indicator, named by the indicator's ID or alias, giving each item's characterization factor (`expected`) and its unit:" ] }, { "cell_type": "code", "execution_count": 13, "id": "s2-load-ind", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.826361Z", "iopub.status.busy": "2026-05-30T19:57:43.825361Z", "iopub.status.idle": "2026-05-30T19:57:43.832894Z", "shell.execute_reply": "2026-05-30T19:57:43.831884Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
IDunitexpected
0Steelkg CO2-eq2
1Concretekg CO2-eq300
\n", "
" ], "text/plain": [ " ID unit expected\n", "0 Steel kg CO2-eq 2\n", "1 Concrete kg CO2-eq 300" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item_data['GWP']" ] }, { "cell_type": "markdown", "id": "s2-load-run-md", "metadata": {}, "source": [ "Passing the dict (or an Excel path with the same sheet layout) to `load_from_file` creates the items:" ] }, { "cell_type": "code", "execution_count": 14, "id": "s2-load-run", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.834891Z", "iopub.status.busy": "2026-05-30T19:57:43.834891Z", "iopub.status.idle": "2026-05-30T19:57:43.839753Z", "shell.execute_reply": "2026-05-30T19:57:43.839753Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : Concrete [per m3]\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 300\n", "FossilEnergyConsumption (MJ) 900\n" ] } ], "source": [ "qs.ImpactItem.load_from_file(item_data)\n", "qs.ImpactItem.get_item('Concrete').show()" ] }, { "cell_type": "markdown", "id": "edb589fc", "metadata": {}, "source": [ "### 2.2. `StreamImpactItem`\n", "A special case of impact items are ones related to material inputs and generated wastes/emissions, and it's more convenient to use `StreamImpactItem` (a subclass of `ImpactItem`).\n", "\n", "A big perk of using `StreamImpactItem` is that you can link it to a particular stream, therefore after you simulate the system, quantity of this item will be automatically updated." ] }, { "cell_type": "code", "execution_count": 15, "id": "3708c323", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:43.841992Z", "iopub.status.busy": "2026-05-30T19:57:43.841992Z", "iopub.status.idle": "2026-05-30T19:57:44.059901Z", "shell.execute_reply": "2026-05-30T19:57:44.059901Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "StreamImpactItem: ws1_item [per kg]\n", "Linked to : ws1\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 1\n", "FossilEnergyConsumption (MJ) 0.1\n" ] } ], "source": [ "# For example, let's assume we want to account for the emissions with the wastewater\n", "# note that indicator CFs can be added via a number of a tuple of (quantity, unit)\n", "cmps = qs.Components.load_default()\n", "qs.set_thermo(cmps)\n", "ww = qs.WasteStream.codbased_inf_model(flow_tot=1000)\n", "ww_item = qs.StreamImpactItem(linked_stream=ww, GWP=1, FEC=(100, 'kJ'))\n", "ww_item.show()" ] }, { "cell_type": "code", "execution_count": 16, "id": "cdcda813", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.059901Z", "iopub.status.busy": "2026-05-30T19:57:44.059901Z", "iopub.status.idle": "2026-05-30T19:57:44.067844Z", "shell.execute_reply": "2026-05-30T19:57:44.067475Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "StreamImpactItem: ws1_item [per kg]\n", "Linked to : ws1\n", "Price : 0.2 USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 1\n", "FossilEnergyConsumption (MJ) 0.1\n" ] } ], "source": [ "# The price of the impact item will be linked to the price of the stream\n", "ww.price = 0.2\n", "ww_item.show()" ] }, { "cell_type": "code", "execution_count": 17, "id": "81c590e9", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.067844Z", "iopub.status.busy": "2026-05-30T19:57:44.067844Z", "iopub.status.idle": "2026-05-30T19:57:44.543574Z", "shell.execute_reply": "2026-05-30T19:57:44.543574Z" }, "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "AttributeError", "evalue": "property '_price' of 'StreamImpactItem' object has no setter", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# And you would get an error if you tried to set the price of the impact item\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# (because it is linked to the stream)\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m ww_item.price = \u001b[32m0.1\u001b[39m\n", "\u001b[31mAttributeError\u001b[39m: property '_price' of 'StreamImpactItem' object has no setter" ] } ], "source": [ "# And you would get an error if you tried to set the price of the impact item\n", "# (because it is linked to the stream)\n", "ww_item.price = 0.1" ] }, { "cell_type": "code", "execution_count": 18, "id": "36145988", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.543574Z", "iopub.status.busy": "2026-05-30T19:57:44.543574Z", "iopub.status.idle": "2026-05-30T19:57:44.551373Z", "shell.execute_reply": "2026-05-30T19:57:44.550798Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "StreamImpactItem: ws1_item [per kg]\n", "Linked to : ws1\n", "Price : 0.1 USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 1\n", "FossilEnergyConsumption (MJ) 0.1\n" ] } ], "source": [ "# you should set the price of the stream instead\n", "ww.price = 0.1\n", "ww_item.show()" ] }, { "cell_type": "code", "execution_count": 19, "id": "bdab1e24", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.553380Z", "iopub.status.busy": "2026-05-30T19:57:44.553380Z", "iopub.status.idle": "2026-05-30T19:57:44.563392Z", "shell.execute_reply": "2026-05-30T19:57:44.562387Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "StreamImpactItem: ws2_item [per kg]\n", "Linked to : ws2\n", "Source : ws1_item\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 1\n", "FossilEnergyConsumption (MJ) 0.1\n" ] } ], "source": [ "# In designing the system, you may need multiple items of the same settings,\n", "# for example you can have many wastewater streams,\n", "# you can use the `copy` method and use `set_as_source` to set\n", "# the original impact item as the source\n", "# (so that if you need to update something, you can just update one impact item)\n", "ww2 = qs.WasteStream.bodbased_inf_model(flow_tot=200)\n", "ww_item2 = ww_item.copy(stream=ww2, set_as_source=True)\n", "ww_item2.show()" ] }, { "cell_type": "code", "execution_count": 20, "id": "34e2e013", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.564399Z", "iopub.status.busy": "2026-05-30T19:57:44.564399Z", "iopub.status.idle": "2026-05-30T19:57:44.568858Z", "shell.execute_reply": "2026-05-30T19:57:44.568858Z" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Updating the CF values of either of the impact items would affect both\n", "ww_item2.CFs['FossilEnergyConsumption'] = 1\n", "ww_item.CFs['FossilEnergyConsumption'] == ww_item2.CFs['FossilEnergyConsumption']" ] }, { "cell_type": "code", "execution_count": 21, "id": "22a15ae8", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.570869Z", "iopub.status.busy": "2026-05-30T19:57:44.570869Z", "iopub.status.idle": "2026-05-30T19:57:44.574822Z", "shell.execute_reply": "2026-05-30T19:57:44.574822Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ImpactItem : copied_electricity [per kWh]\n", "Source : electricity\n", "Price : None USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) 0.25\n", "FossilEnergyConsumption (MJ) 50\n" ] } ], "source": [ "# This works for ImpactItem objects other than StreamImpactItem as well\n", "e2 = electricity.copy(new_ID='copied_electricity', set_as_source=True)\n", "e2.show()" ] }, { "cell_type": "markdown", "id": "s3-head", "metadata": {}, "source": [ "## 3. Build the system for LCA \n", "\n", "Like a `TEA`, an `LCA` is linked to a `System`. We reuse the two wastewater treatment plants from the [7. TEA](https://qsdsan.readthedocs.io/en/latest/tutorials/7_TEA.html) tutorial, an aerobic activated-sludge plant and an anaerobic plant, packaged for convenience as `create_example_treatment_systems`. The aerobic plant spends electricity on aeration; the anaerobic plant recovers energy as biogas.\n", "\n", "The LCA registries (indicators, items, construction, and transportation) are scoped to the **active flowsheet**, so we work in a dedicated flowsheet to keep this analysis isolated. Switching the flowsheet swaps those registries, which is why we (re)define the indicators here." ] }, { "cell_type": "code", "execution_count": 22, "id": "s3-build", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:44.576837Z", "iopub.status.busy": "2026-05-30T19:57:44.576837Z", "iopub.status.idle": "2026-05-30T19:57:45.080396Z", "shell.execute_reply": "2026-05-30T19:57:45.080396Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "189122525642:c->189116197234:w\n", "\n", "\n", " aer effluent\n", "\n", "\n", "\n", "\n", "\n", "189122525642:c->189116197994:w\n", "\n", "\n", " aer sludge\n", "\n", "\n", "\n", "\n", "\n", "189116197794:e->189122525642:c\n", "\n", "\n", " ww aer\n", "\n", "\n", "\n", "\n", "\n", "189122525642\n", "\n", "\n", "aer\n", "Aerobic plant\n", "\n", "\n", "\n", "\n", "\n", "189116197794\n", "\n", "\n", "\n", "\n", "189116197234\n", "\n", "\n", "\n", "\n", "189116197994\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# We will directly import the example treatment systems,\n", "# as we have defined them in the TEA tutorial\n", "from qsdsan.utils import create_example_treatment_systems\n", "\n", "# A dedicated flowsheet isolates this LCA's registries (see the note below)\n", "qs.main_flowsheet.set_flowsheet('lca_demo')\n", "\n", "# Indicators live in the active flowsheet, so define them here\n", "GWP = qs.ImpactIndicator('GlobalWarming', alias='GWP', unit='kg CO2-eq')\n", "FEC = qs.ImpactIndicator('FossilEnergyConsumption', alias='FEC', unit='MJ')\n", "\n", "# The aerobic and anaerobic plants from the TEA tutorial (same wastewater, 4,000 m3/d)\n", "aer_sys, ana_sys = create_example_treatment_systems()\n", "aer_sys.simulate(); ana_sys.simulate()\n", "aer, ana = aer_sys.units[0], ana_sys.units[0]\n", "aer_sys.diagram() # default format; pass format='html' for an interactive diagram" ] }, { "cell_type": "markdown", "id": "s3-isolation-note", "metadata": {}, "source": [ "
\n", "\n", "**Flowsheet-scoped registries.** `ImpactIndicator`, `ImpactItem`, `Construction`, and `Transportation` objects all live in the registry of the **active flowsheet**, not in a global namespace. Switching flowsheets (with `qs.main_flowsheet.set_flowsheet(...)` or the `with qs.Flowsheet(...)` context manager) atomically swaps them, so two systems can reuse the same names without clashing. The old `clear_lca_registries()` helper is deprecated; use a dedicated flowsheet for isolation instead.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 23, "id": "s3-isolation-demo", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.080396Z", "iopub.status.busy": "2026-05-30T19:57:45.080396Z", "iopub.status.idle": "2026-05-30T19:57:45.087283Z", "shell.execute_reply": "2026-05-30T19:57:45.087283Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "indicators inside \"scratch\": []\n", "back in \"lca_demo\" : ['GlobalWarming', 'FossilEnergyConsumption']\n" ] } ], "source": [ "# A fresh flowsheet starts with empty LCA registries, so names never clash\n", "with qs.Flowsheet('scratch'):\n", " print('indicators inside \"scratch\":', list(qs.ImpactIndicator.get_all_indicators()))\n", "# exiting the context restores the previous (lca_demo) flowsheet\n", "print('back in \"lca_demo\" :', list(qs.ImpactIndicator.get_all_indicators()))" ] }, { "cell_type": "markdown", "id": "s4-head", "metadata": {}, "source": [ "## 4. `Construction`, `Transportation`, and other activities \n", "\n", "With indicators and items in place, we build the life cycle inventory of the two plants. `qsdsan` groups inventory into four categories: construction, transportation, stream (material inputs and emissions), and other activities (for example, electricity)." ] }, { "cell_type": "markdown", "id": "s4-1-head", "metadata": {}, "source": [ "### 4.1. `Construction`\n", "\n", "`Construction` captures impacts that occur once per lifetime of a piece of equipment or unit. Each `Construction` links an `ImpactItem` to a quantity (impact = quantity x CF) and is stored on the unit in `SanUnit.construction`. We give both plants some concrete and steel:" ] }, { "cell_type": "code", "execution_count": 24, "id": "s4-1-attach", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.089754Z", "iopub.status.busy": "2026-05-30T19:57:45.089754Z", "iopub.status.idle": "2026-05-30T19:57:45.096914Z", "shell.execute_reply": "2026-05-30T19:57:45.095908Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Construction : lca_demo_aer_Constr1\n", "Impact item : Concrete\n", "Lifetime : 30 yr\n", "Quantity : 300 m3\n", "Total cost : None USD\n", "Total impacts:\n", " Impacts\n", "GlobalWarming (kg CO2-eq) 9e+04\n", "FossilEnergyConsumption (MJ) 2.7e+05\n" ] } ], "source": [ "Concrete = qs.ImpactItem('Concrete', functional_unit='m3', GWP=300., FEC=900.)\n", "Steel = qs.ImpactItem('Steel', functional_unit='kg', GWP=2., FEC=25.)\n", "\n", "# the anaerobic plant has the larger concrete digester, so give it more material\n", "for u, concrete_m3, steel_kg in ((aer, 300, 30000), (ana, 400, 40000)):\n", " u.construction = [\n", " qs.Construction(linked_unit=u, item=Concrete, quantity=concrete_m3,\n", " quantity_unit='m3', lifetime=30),\n", " qs.Construction(linked_unit=u, item=Steel, quantity=steel_kg,\n", " quantity_unit='kg', lifetime=30),\n", " ]\n", "aer.construction[0].show()" ] }, { "cell_type": "markdown", "id": "s4-1-lifetime-note", "metadata": {}, "source": [ "
\n", "\n", "**Construction lifetime.** If a `Construction` item's `lifetime` (years) is shorter than the system lifetime, `LCA` adds a replacement each time it elapses; if left `None`, the item is assumed to last the whole system lifetime. Realistic lifetimes matter for accurate construction impacts.\n", "\n", "
\n" ] }, { "cell_type": "markdown", "id": "s4-1-specs-md", "metadata": {}, "source": [ "**Declaring default materials.** If a unit class *always* needs certain construction materials, you can declare them as a class-level `_construction_specs` tuple instead of building `Construction` objects in `__init__`. The materials are looked up lazily (from the active flowsheet) when the `LCA` is created, so the `ImpactItem` objects need not exist when the unit is created. Each spec is a dict with `item`, `quantity`, `quantity_unit`, and optionally `lifetime` and `lifetime_unit`.\n" ] }, { "cell_type": "code", "execution_count": 25, "id": "s4-1-specs-class", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.098914Z", "iopub.status.busy": "2026-05-30T19:57:45.098914Z", "iopub.status.idle": "2026-05-30T19:57:45.104172Z", "shell.execute_reply": "2026-05-30T19:57:45.104172Z" } }, "outputs": [], "source": [ "class ConcreteReactor(qs.SanUnit):\n", " _construction_specs = (\n", " {'item': 'SpecConcrete', 'quantity': 5, 'quantity_unit': 'm3'},\n", " {'item': 'SpecSteel', 'quantity': 10, 'quantity_unit': 'kg', 'lifetime': 20},\n", " )\n", " _N_ins = _N_outs = 1\n", " def _run(self):\n", " self.outs[0].copy_like(self.ins[0])" ] }, { "cell_type": "code", "execution_count": 26, "id": "s4-1-specs-demo", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.107813Z", "iopub.status.busy": "2026-05-30T19:57:45.107813Z", "iopub.status.idle": "2026-05-30T19:57:45.304252Z", "shell.execute_reply": "2026-05-30T19:57:45.303239Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "resolved construction GWP: 525.50 kg CO2-eq\n" ] } ], "source": [ "# Build the unit in its own flowsheet so the demo stays isolated; the items the\n", "# specs reference must exist in that flowsheet when the LCA is created.\n", "with qs.Flowsheet('specs_demo'):\n", " GWP_d = qs.ImpactIndicator('GlobalWarming', alias='GWP', unit='kg CO2-eq')\n", " qs.set_thermo(qs.Components.load_default())\n", " feed = qs.WasteStream('feed_demo', H2O=1000, units='kg/hr')\n", " reactor = ConcreteReactor('reactor_demo', ins=feed)\n", " qs.ImpactItem('SpecConcrete', 'm3', GWP=100.) # must be loaded before LCA\n", " qs.ImpactItem('SpecSteel', 'kg', GWP=2.55)\n", " sys_demo = qs.System('sys_demo', path=(reactor,)); sys_demo.simulate()\n", " lca_demo_specs = qs.LCA(sys_demo, lifetime=20, simulate_system=False)\n", " # 5 m3 SpecConcrete x 100 + 10 kg SpecSteel x 2.55 = 525.5\n", " print(f\"resolved construction GWP: {lca_demo_specs.get_construction_impacts()['GlobalWarming']:.2f} kg CO2-eq\")" ] }, { "cell_type": "markdown", "id": "s4-1-specs-note", "metadata": {}, "source": [ "If you set `unit.construction` explicitly for an item that also appears in `_construction_specs`, the explicit value takes precedence and the spec is skipped for that item. Specs for all other items are still resolved normally." ] }, { "cell_type": "markdown", "id": "s4-2-head", "metadata": {}, "source": [ "### 4.2. `Transportation`\n", "\n", "`Transportation` also links to an `ImpactItem`, but computes impacts from a load, a distance, and an interval (how often a trip is made). Here each plant hauls its waste sludge to a landfill 50 km away, once a year:" ] }, { "cell_type": "code", "execution_count": 27, "id": "s4-2-attach", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.306248Z", "iopub.status.busy": "2026-05-30T19:57:45.306248Z", "iopub.status.idle": "2026-05-30T19:57:45.312865Z", "shell.execute_reply": "2026-05-30T19:57:45.312865Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Transportation: lca_demo_aer_Trans1\n", "Impact item : Trucking [per trip]\n", "Load : 126357.5 kg\n", "Distance : 50 km\n", "Interval : 8766 hr\n", "Total cost : None USD\n", "Total impacts :\n", " Impacts\n", "GlobalWarming (kg CO2-eq) 632\n", "FossilEnergyConsumption (MJ) 1.26e+04\n" ] } ], "source": [ "Trucking = qs.ImpactItem('Trucking', functional_unit='kg*km', GWP=1e-4, FEC=2e-3)\n", "for u in (aer, ana):\n", " annual_sludge = u.outs[1].F_mass * 8760 # kg/yr (the waste sludge outlet)\n", " u.transportation = [qs.Transportation(\n", " linked_unit=u, item=Trucking, load_type='mass',\n", " load=annual_sludge, load_unit='kg', distance=50, distance_unit='km',\n", " interval=1, interval_unit='yr')]\n", "aer.transportation[0].show()" ] }, { "cell_type": "markdown", "id": "s4-2-note", "metadata": {}, "source": [ "
\n", "\n", "**Note.** A `Transportation` object's impacts are for a single trip; `LCA` multiplies by the number of trips over the system lifetime, computed from `interval`.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "s4-3-head", "metadata": {}, "source": [ "### 4.3. Stream emissions and other activities\n", "\n", "Material inputs and emissions are captured with `StreamImpactItem` objects linked to streams (Section 2.2). We add a direct emission for sludge handling on both plants, and an avoided-impact **credit** for the anaerobic plant's biogas (a negative CF, because the recovered methane displaces fossil natural gas):" ] }, { "cell_type": "code", "execution_count": 28, "id": "s4-3-streams", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.314877Z", "iopub.status.busy": "2026-05-30T19:57:45.314877Z", "iopub.status.idle": "2026-05-30T19:57:45.320408Z", "shell.execute_reply": "2026-05-30T19:57:45.319925Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "StreamImpactItem: biogas_item [per kg]\n", "Linked to : biogas\n", "Price : 0.09 USD\n", "ImpactIndicators:\n", " Characterization factors\n", "GlobalWarming (kg CO2-eq) -2\n", "FossilEnergyConsumption (MJ) -50\n" ] } ], "source": [ "qs.StreamImpactItem(linked_stream=aer.outs[1], GWP=0.2) # aerobic sludge handling\n", "qs.StreamImpactItem(linked_stream=ana.outs[1], GWP=0.2) # anaerobic sludge handling\n", "qs.StreamImpactItem(linked_stream=ana.outs[2], GWP=-2.0, FEC=-50.) # biogas displaces natural gas" ] }, { "cell_type": "markdown", "id": "s4-3-other", "metadata": {}, "source": [ "Impacts not covered by construction, transportation, or streams (here, the aeration **electricity**) are added directly when creating the `LCA`. We define an electricity item now and pass each plant's lifetime electricity use in the next section:" ] }, { "cell_type": "code", "execution_count": 29, "id": "s4-3-electricity", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.321511Z", "iopub.status.busy": "2026-05-30T19:57:45.321511Z", "iopub.status.idle": "2026-05-30T19:57:45.325655Z", "shell.execute_reply": "2026-05-30T19:57:45.325655Z" } }, "outputs": [], "source": [ "electricity = qs.ImpactItem('electricity', functional_unit='kWh', GWP=0.5, FEC=5.93)" ] }, { "cell_type": "markdown", "id": "s5-head", "metadata": {}, "source": [ "## 5. `LCA` \n", "\n", "Now we run the `LCA`. Construction, transportation, and stream impacts are read from the units and streams set up above; electricity (an \"other\" activity) is passed at creation as a total quantity over the lifetime. We analyze both plants over a 20-year lifetime." ] }, { "cell_type": "markdown", "id": "s5-1-head", "metadata": {}, "source": [ "### 5.1. Reading impacts\n", "\n", "Each `LCA` is linked to a `System`, like a `TEA`. We create one per plant and pass the lifetime electricity use:" ] }, { "cell_type": "code", "execution_count": 30, "id": "s5-build", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.325655Z", "iopub.status.busy": "2026-05-30T19:57:45.325655Z", "iopub.status.idle": "2026-05-30T19:57:45.334639Z", "shell.execute_reply": "2026-05-30T19:57:45.333635Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LCA: aer_sys (lifetime 20 yr)\n", "Impacts:\n", " Construction Transportation Stream Others Total\n", "FossilEnergyConsumption (MJ) 1.02e+06 2.53e+05 0 3.54e+07 3.66e+07\n", "GlobalWarming (kg CO2-eq) 1.5e+05 1.26e+04 5.05e+05 2.98e+06 3.65e+06\n" ] } ], "source": [ "lifetime = 20\n", "# total electricity over the lifetime (kWh) = rate (kW) x hours; the aerobic plant aerates\n", "aer_kWh = aer.power_utility.rate * 24 * 365 * lifetime\n", "ana_kWh = ana.power_utility.rate * 24 * 365 * lifetime\n", "lca_aer = qs.LCA(system=aer_sys, lifetime=lifetime, electricity=aer_kWh, simulate_system=False)\n", "lca_ana = qs.LCA(system=ana_sys, lifetime=lifetime, electricity=ana_kWh, simulate_system=False)\n", "lca_aer.show()" ] }, { "cell_type": "markdown", "id": "s5-totals-md", "metadata": {}, "source": [ "`show()` prints the breakdown by category. `get_total_impacts` returns the totals as a dict (the headline result); pass `annual=True` to divide by the lifetime, or `operation_only=True` to drop the one-time construction impacts:" ] }, { "cell_type": "code", "execution_count": 31, "id": "s5-totals", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.336240Z", "iopub.status.busy": "2026-05-30T19:57:45.336240Z", "iopub.status.idle": "2026-05-30T19:57:45.341270Z", "shell.execute_reply": "2026-05-30T19:57:45.341270Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "total : {'FossilEnergyConsumption': 36639655, 'GlobalWarming': 3650107}\n", "annual : {'FossilEnergyConsumption': 1831983, 'GlobalWarming': 182505}\n", "operation : {'FossilEnergyConsumption': 35619655, 'GlobalWarming': 3500107}\n" ] } ], "source": [ "print('total :', {k: round(v) for k, v in lca_aer.get_total_impacts().items()})\n", "print('annual :', {k: round(v) for k, v in lca_aer.get_total_impacts(annual=True).items()})\n", "print('operation :', {k: round(v) for k, v in lca_aer.get_total_impacts(operation_only=True).items()})" ] }, { "cell_type": "markdown", "id": "s5-cat-md", "metadata": {}, "source": [ "Each category also has a getter and a `total_*_impacts` shortcut, handy for contribution analysis:" ] }, { "cell_type": "code", "execution_count": 32, "id": "s5-cat", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.343279Z", "iopub.status.busy": "2026-05-30T19:57:45.343279Z", "iopub.status.idle": "2026-05-30T19:57:45.347388Z", "shell.execute_reply": "2026-05-30T19:57:45.347388Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "construction: {'FossilEnergyConsumption': 1020000, 'GlobalWarming': 150000}\n", "transport : {'FossilEnergyConsumption': 252542, 'GlobalWarming': 12627}\n", "stream : {'FossilEnergyConsumption': 0, 'GlobalWarming': 505430}\n", "other : {'FossilEnergyConsumption': 35367113, 'GlobalWarming': 2982050}\n" ] } ], "source": [ "print('construction:', {k: round(v) for k, v in lca_aer.total_construction_impacts.items()})\n", "print('transport :', {k: round(v) for k, v in lca_aer.total_transportation_impacts.items()})\n", "print('stream :', {k: round(v) for k, v in lca_aer.total_stream_impacts.items()})\n", "print('other :', {k: round(v) for k, v in lca_aer.total_other_impacts.items()})" ] }, { "cell_type": "markdown", "id": "s5-unit-md", "metadata": {}, "source": [ "For a multi-unit system, `get_unit_impacts` returns the impacts attributable to specific units (here the plant is the whole system):" ] }, { "cell_type": "code", "execution_count": 33, "id": "s5-unit", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.349394Z", "iopub.status.busy": "2026-05-30T19:57:45.349394Z", "iopub.status.idle": "2026-05-30T19:57:45.354831Z", "shell.execute_reply": "2026-05-30T19:57:45.354322Z" } }, "outputs": [ { "data": { "text/plain": [ "{'FossilEnergyConsumption': 36639655, 'GlobalWarming': 3650107}" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{k: round(v) for k, v in lca_aer.get_unit_impacts((aer,)).items()}" ] }, { "cell_type": "markdown", "id": "s5-2-head", "metadata": {}, "source": [ "### 5.2. Impact tables\n", "\n", "`get_impact_table` returns a per-item breakdown for a category. These four tables (construction, transportation, stream, and other) are exactly what `save_report` writes to the `LCA` sheet, so it is worth inspecting them first." ] }, { "cell_type": "code", "execution_count": 34, "id": "s5-table-constr", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.356844Z", "iopub.status.busy": "2026-05-30T19:57:45.356844Z", "iopub.status.idle": "2026-05-30T19:57:45.369930Z", "shell.execute_reply": "2026-05-30T19:57:45.369930Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
QuantityItem RatioFossilEnergyConsumption [MJ]Category FossilEnergyConsumption RatioGlobalWarming [kg CO2-eq]Category GlobalWarming Ratio
ConstructionSanUnit
Concrete [m3]aer30012.7e+050.2659e+040.6
Total30012.7e+050.2659e+040.6
Steel [kg]aer3e+0417.5e+050.7356e+040.4
Total3e+0417.5e+050.7356e+040.4
SumAll1.02e+0611.5e+051
\n", "
" ], "text/plain": [ " Quantity Item Ratio FossilEnergyConsumption [MJ] Category FossilEnergyConsumption Ratio GlobalWarming [kg CO2-eq] \\\n", "Construction SanUnit \n", "Concrete [m3] aer 300 1 2.7e+05 0.265 9e+04 \n", " Total 300 1 2.7e+05 0.265 9e+04 \n", "Steel [kg] aer 3e+04 1 7.5e+05 0.735 6e+04 \n", " Total 3e+04 1 7.5e+05 0.735 6e+04 \n", "Sum All 1.02e+06 1 1.5e+05 \n", "\n", " Category GlobalWarming Ratio \n", "Construction SanUnit \n", "Concrete [m3] aer 0.6 \n", " Total 0.6 \n", "Steel [kg] aer 0.4 \n", " Total 0.4 \n", "Sum All 1 " ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_impact_table('Construction')" ] }, { "cell_type": "code", "execution_count": 35, "id": "s5-table-trans", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.371369Z", "iopub.status.busy": "2026-05-30T19:57:45.371369Z", "iopub.status.idle": "2026-05-30T19:57:45.384472Z", "shell.execute_reply": "2026-05-30T19:57:45.384472Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
QuantityItem RatioFossilEnergyConsumption [MJ]Category FossilEnergyConsumption RatioGlobalWarming [kg CO2-eq]Category GlobalWarming Ratio
TransportationSanUnit
Trucking [kg*km]aer1.26e+0812.53e+0511.26e+041
Total1.26e+0812.53e+0511.26e+041
SumAll2.53e+0511.26e+041
\n", "
" ], "text/plain": [ " Quantity Item Ratio FossilEnergyConsumption [MJ] Category FossilEnergyConsumption Ratio GlobalWarming [kg CO2-eq] \\\n", "Transportation SanUnit \n", "Trucking [kg*km] aer 1.26e+08 1 2.53e+05 1 1.26e+04 \n", " Total 1.26e+08 1 2.53e+05 1 1.26e+04 \n", "Sum All 2.53e+05 1 1.26e+04 \n", "\n", " Category GlobalWarming Ratio \n", "Transportation SanUnit \n", "Trucking [kg*km] aer 1 \n", " Total 1 \n", "Sum All 1 " ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_impact_table('Transportation')" ] }, { "cell_type": "code", "execution_count": 36, "id": "s5-table-stream", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.387626Z", "iopub.status.busy": "2026-05-30T19:57:45.387626Z", "iopub.status.idle": "2026-05-30T19:57:45.396874Z", "shell.execute_reply": "2026-05-30T19:57:45.396874Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Mass [kg]FossilEnergyConsumption [MJ]Category FossilEnergyConsumption RatioGlobalWarming [kg CO2-eq]Category GlobalWarming Ratio
Stream
aer_sludge2.53e+06005.05e+051
Sum015.05e+051
\n", "
" ], "text/plain": [ " Mass [kg] FossilEnergyConsumption [MJ] Category FossilEnergyConsumption Ratio GlobalWarming [kg CO2-eq] Category GlobalWarming Ratio\n", "Stream \n", "aer_sludge 2.53e+06 0 0 5.05e+05 1\n", "Sum 0 1 5.05e+05 1" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_impact_table('Stream')" ] }, { "cell_type": "code", "execution_count": 37, "id": "s5-table-other", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.399247Z", "iopub.status.busy": "2026-05-30T19:57:45.399247Z", "iopub.status.idle": "2026-05-30T19:57:45.408950Z", "shell.execute_reply": "2026-05-30T19:57:45.408950Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
QuantityFossilEnergyConsumption [MJ]Category FossilEnergyConsumption RatioGlobalWarming [kg CO2-eq]Category GlobalWarming Ratio
Other
electricity5.96e+063.54e+0712.98e+061
Sum3.54e+0712.98e+061
\n", "
" ], "text/plain": [ " Quantity FossilEnergyConsumption [MJ] Category FossilEnergyConsumption Ratio GlobalWarming [kg CO2-eq] Category GlobalWarming Ratio\n", "Other \n", "electricity 5.96e+06 3.54e+07 1 2.98e+06 1\n", "Sum 3.54e+07 1 2.98e+06 1" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_impact_table('Other')" ] }, { "cell_type": "markdown", "id": "s5-table-annual-md", "metadata": {}, "source": [ "Every table and `save_report` also accept a time frame: `annual=True` (per year), or the more general `time_frame=` (`'lifetime'`, the default, or `'yr'`, `'month'`, `'day'`, `'hr'`). The column unit suffix updates accordingly. Because operating impacts scale linearly with time, you can also divide any result by a period yourself to normalize however you like:" ] }, { "cell_type": "code", "execution_count": 38, "id": "s5-table-annual", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.411202Z", "iopub.status.busy": "2026-05-30T19:57:45.411202Z", "iopub.status.idle": "2026-05-30T19:57:45.425659Z", "shell.execute_reply": "2026-05-30T19:57:45.425659Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "per year : 182505 kg CO2-eq/yr\n", "per day : 500 kg CO2-eq/day\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Quantity/yrItem RatioFossilEnergyConsumption [MJ/yr]Category FossilEnergyConsumption RatioGlobalWarming [kg CO2-eq/yr]Category GlobalWarming Ratio
ConstructionSanUnit
Concrete [m3]aer1511.35e+040.2654.5e+030.6
Total1511.35e+040.2654.5e+030.6
Steel [kg]aer1.5e+0313.75e+040.7353e+030.4
Total1.5e+0313.75e+040.7353e+030.4
SumAll5.1e+0417.5e+031
\n", "
" ], "text/plain": [ " Quantity/yr Item Ratio FossilEnergyConsumption [MJ/yr] Category FossilEnergyConsumption Ratio GlobalWarming [kg CO2-eq/yr] \\\n", "Construction SanUnit \n", "Concrete [m3] aer 15 1 1.35e+04 0.265 4.5e+03 \n", " Total 15 1 1.35e+04 0.265 4.5e+03 \n", "Steel [kg] aer 1.5e+03 1 3.75e+04 0.735 3e+03 \n", " Total 1.5e+03 1 3.75e+04 0.735 3e+03 \n", "Sum All 5.1e+04 1 7.5e+03 \n", "\n", " Category GlobalWarming Ratio \n", "Construction SanUnit \n", "Concrete [m3] aer 0.6 \n", " Total 0.6 \n", "Steel [kg] aer 0.4 \n", " Total 0.4 \n", "Sum All 1 " ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print('per year :', round(lca_aer.get_total_impacts(time_frame='yr')['GlobalWarming']), 'kg CO2-eq/yr')\n", "print('per day :', round(lca_aer.get_total_impacts(time_frame='day')['GlobalWarming']), 'kg CO2-eq/day')\n", "lca_aer.get_impact_table('Construction', time_frame='yr') # the same table, on a per-year basis" ] }, { "cell_type": "markdown", "id": "s5-stream-md", "metadata": {}, "source": [ "For streams, `get_stream_impacts` separates positive **direct emissions** from negative **offsets** (avoided impacts). The anaerobic plant's biogas is an offset, so it appears as a credit:" ] }, { "cell_type": "code", "execution_count": 39, "id": "s5-stream", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.425659Z", "iopub.status.busy": "2026-05-30T19:57:45.425659Z", "iopub.status.idle": "2026-05-30T19:57:45.431378Z", "shell.execute_reply": "2026-05-30T19:57:45.431378Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "anaerobic direct emissions: {'FossilEnergyConsumption': 0.0, 'GlobalWarming': 63178.74956710877}\n", "anaerobic offsets : {'FossilEnergyConsumption': -358108759.84849435, 'GlobalWarming': -14324350.393939773}\n" ] } ], "source": [ "print('anaerobic direct emissions:', lca_ana.get_stream_impacts(kind='direct_emission'))\n", "print('anaerobic offsets :', lca_ana.get_stream_impacts(kind='offset'))" ] }, { "cell_type": "markdown", "id": "s5-3-head", "metadata": {}, "source": [ "### 5.3. Allocating impacts to streams\n", "\n", "When a system makes several products (or has several waste streams), you often want to attribute the footprint to a particular stream. The workflow has two parts:\n", "\n", "1. **Exclude** the streams of interest from the total with `get_total_impacts(exclude_streams=...)`, so their own assigned impacts are not double-counted; and\n", "2. **Allocate** that remaining total to one or more chosen streams with `get_allocated_impacts` (a dict) or `get_allocated_impact_table` (a table with an allocation-factor column).\n", "\n", "This is the LCA counterpart to `TEA.solve_price`: there you isolate one stream's economic role, here its share of the environmental footprint." ] }, { "cell_type": "code", "execution_count": 40, "id": "s5-exclude", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.431378Z", "iopub.status.busy": "2026-05-30T19:57:45.431378Z", "iopub.status.idle": "2026-05-30T19:57:45.439090Z", "shell.execute_reply": "2026-05-30T19:57:45.439090Z" } }, "outputs": [ { "data": { "text/plain": [ "{'FossilEnergyConsumption': 36639655, 'GlobalWarming': 3144677}" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# the total with the two outlets' own impacts removed (this is what gets allocated)\n", "{k: round(v) for k, v in lca_aer.get_total_impacts(exclude_streams=aer.outs).items()}" ] }, { "cell_type": "markdown", "id": "s5-alloc-mass-md", "metadata": {}, "source": [ "By default the impacts are allocated by **mass**. The effluent carries almost all the mass, so it receives almost all of the allocated impact:" ] }, { "cell_type": "code", "execution_count": 41, "id": "s5-alloc-mass", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.441103Z", "iopub.status.busy": "2026-05-30T19:57:45.441103Z", "iopub.status.idle": "2026-05-30T19:57:45.448434Z", "shell.execute_reply": "2026-05-30T19:57:45.448434Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
FossilEnergyConsumption [MJ]GlobalWarming [kg CO2-eq]Allocation factor
Stream
aer_effluent3.66e+073.14e+061
aer_sludge3.18e+032738.67e-05
\n", "
" ], "text/plain": [ " FossilEnergyConsumption [MJ] GlobalWarming [kg CO2-eq] Allocation factor\n", "Stream \n", "aer_effluent 3.66e+07 3.14e+06 1\n", "aer_sludge 3.18e+03 273 8.67e-05" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_allocated_impact_table(aer.outs, allocate_by='mass')" ] }, { "cell_type": "markdown", "id": "s5-alloc-custom-md", "metadata": {}, "source": [ "`allocate_by` also accepts `'energy'` (by heating value), `'value'` (by price), or your own iterable of ratios (no need to normalize). Choose the basis that matches your accounting. For example, an explicit 60/40 split:" ] }, { "cell_type": "code", "execution_count": 42, "id": "s5-alloc-custom", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.451445Z", "iopub.status.busy": "2026-05-30T19:57:45.450448Z", "iopub.status.idle": "2026-05-30T19:57:45.458875Z", "shell.execute_reply": "2026-05-30T19:57:45.457862Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
FossilEnergyConsumption [MJ]GlobalWarming [kg CO2-eq]Allocation factor
Stream
aer_effluent2.2e+071.89e+060.6
aer_sludge1.47e+071.26e+060.4
\n", "
" ], "text/plain": [ " FossilEnergyConsumption [MJ] GlobalWarming [kg CO2-eq] Allocation factor\n", "Stream \n", "aer_effluent 2.2e+07 1.89e+06 0.6\n", "aer_sludge 1.47e+07 1.26e+06 0.4" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lca_aer.get_allocated_impact_table(aer.outs, allocate_by=(0.6, 0.4))" ] }, { "cell_type": "markdown", "id": "s5-alloc-note", "metadata": {}, "source": [ "
\n", "\n", "**Choosing a basis.** `'energy'` requires the streams to have a heating value and `'value'` requires them to have a price; pick the basis that reflects how the burden should be shared among coproducts. `get_allocated_impacts` returns the same numbers as a nested dict if you prefer to work with them directly.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "s5-norm-md", "metadata": {}, "source": [ "**Normalizing to a functional unit.** The counterpart to `TEA.solve_price` (a stream's *cost* per unit) is the *footprint* **per functional unit**. `get_normalized_impacts` divides the impacts by the throughput of the reference stream(s): for a treatment plant the natural unit is per m³ of wastewater treated. Pass `normalize_by='mass'` (per kg), `'volume'` (per m³), or `'energy'` (per MJ). By default it normalizes the *total* impacts; pass `allocate_by` to instead normalize the impacts *allocated* to those streams (per kg of a product). Just as dividing by time gives a per-year number, this gives a per-m³ or per-kg number, so you can normalize to whatever basis your study needs." ] }, { "cell_type": "code", "execution_count": 43, "id": "s5-norm", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.460393Z", "iopub.status.busy": "2026-05-30T19:57:45.460393Z", "iopub.status.idle": "2026-05-30T19:57:45.465797Z", "shell.execute_reply": "2026-05-30T19:57:45.464903Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GWP per m3 treated: 0.125 kg CO2-eq/m3\n", "GWP per kg outflow: 1.08e-04 kg CO2-eq/kg\n" ] } ], "source": [ "# per m3 of wastewater treated (total impacts / influent volume): the plant's functional unit\n", "print('GWP per m3 treated:',\n", " round(lca_aer.get_normalized_impacts(aer.ins[0], normalize_by='volume')['GlobalWarming'], 3),\n", " 'kg CO2-eq/m3')\n", "\n", "# or normalize the impacts *allocated* to the outlet streams, per kg of their combined flow\n", "alloc_per_kg = lca_aer.get_normalized_impacts(aer.outs, normalize_by='mass', allocate_by='mass')\n", "print('GWP per kg outflow:', '%.2e' % alloc_per_kg['GlobalWarming'], 'kg CO2-eq/kg')" ] }, { "cell_type": "markdown", "id": "s5-4-head", "metadata": {}, "source": [ "### 5.4. Quantities that change with simulation\n", "\n", "For an \"other\" item whose quantity depends on the simulation (for example, electricity that changes when you adjust the plant), pass a **function** instead of a number. `refresh_other_items` re-evaluates it, so the LCA stays in sync after you change the system:" ] }, { "cell_type": "code", "execution_count": 44, "id": "s5-dynamic", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.468026Z", "iopub.status.busy": "2026-05-30T19:57:45.467423Z", "iopub.status.idle": "2026-05-30T19:57:45.474300Z", "shell.execute_reply": "2026-05-30T19:57:45.474300Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GWP before: 3,650,107 | after +50% COD: 5,393,847\n" ] } ], "source": [ "# pass a function so the electricity quantity tracks the simulation\n", "lca_aer.add_other_item('electricity', lambda: aer.power_utility.rate*24*365*lifetime)\n", "feed = aer.ins[0]\n", "base_substrate = feed.imass['Substrate']\n", "before = lca_aer.get_total_impacts()['GlobalWarming']\n", "\n", "feed.imass['Substrate'] = base_substrate*1.5 # 50% stronger influent -> more aeration\n", "aer_sys.simulate(); lca_aer.refresh_other_items()\n", "after = lca_aer.get_total_impacts()['GlobalWarming']\n", "print(f'GWP before: {before:,.0f} | after +50% COD: {after:,.0f}')\n", "\n", "feed.imass['Substrate'] = base_substrate # restore the base case\n", "aer_sys.simulate()\n", "lca_aer.refresh_other_items()" ] }, { "cell_type": "markdown", "id": "s5-dynamic-ctor-note", "metadata": {}, "source": [ "
\n", "\n", "**Tip.** Any LCA quantity (an \"other\" item, and also any `Construction` or `Transportation` quantity) can be a zero-argument callable (function) rather than a fixed number; the LCA reads it lazily (i.e., doesn't run until needed) on each impact query. The callable can equivalently be passed straight to the `qs.LCA` constructor, skipping the follow-up `add_other_item` call:\n", "\n", "```python\n", "lca_aer = qs.LCA(\n", " system=aer_sys, lifetime=lifetime,\n", " electricity=lambda: aer.power_utility.rate * 24 * 365 * lifetime,\n", " simulate_system=False,\n", ")\n", "```\n", "\n", "Use whichever form reads more clearly for the situation. The combined form is the natural choice when the LCA is wrapped in an uncertainty `Model` whose parameters change the system between samples; the [9. Uncertainty and Sensitivity Analyses](https://qsdsan.readthedocs.io/en/latest/tutorials/9_Uncertainty_and_Sensitivity_Analyses.html) tutorial uses it for exactly that reason.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "s5-4-agile-note", "metadata": {}, "source": [ "
\n", "\n", "**Operating over the year.** When the system runs in several modes across the year, such as seasonal load, day and night, or feedstock switching, an AgileSystem gives one annualized LCA weighted by operating hours. See [Operational flexibility in Tutorial 6](6_System.ipynb#4.-Operational-flexibility).\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "s5-5-head", "metadata": {}, "source": [ "### 5.5. Comparing systems and exporting\n", "\n", "In the TEA tutorial the aerobic plant was more cost-effective for this dilute municipal wastewater; the LCA tells a different story, because the anaerobic plant's biogas recovery offsets fossil energy and gives it a much lower (here net-negative) carbon footprint. Cost and carbon do not always agree:" ] }, { "cell_type": "code", "execution_count": 45, "id": "s5-compare", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.474300Z", "iopub.status.busy": "2026-05-30T19:57:45.474300Z", "iopub.status.idle": "2026-05-30T19:57:45.484661Z", "shell.execute_reply": "2026-05-30T19:57:45.483826Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
plantGWP (kg CO2-eq)FEC (MJ)
0aerobic365010736639655
1anaerobic-14059593-356717192
\n", "
" ], "text/plain": [ " plant GWP (kg CO2-eq) FEC (MJ)\n", "0 aerobic 3650107 36639655\n", "1 anaerobic -14059593 -356717192" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "rows = []\n", "for name, lca in (('aerobic', lca_aer), ('anaerobic', lca_ana)):\n", " tot = lca.get_total_impacts()\n", " rows.append((name, round(tot['GlobalWarming']), round(tot['FossilEnergyConsumption'])))\n", "pd.DataFrame(rows, columns=['plant', 'GWP (kg CO2-eq)', 'FEC (MJ)'])" ] }, { "cell_type": "markdown", "id": "s5-save-md", "metadata": {}, "source": [ "Finally, export everything to one Excel workbook with `save_report`. It is unified across `System`, `TEA`, and `LCA`, so `aer_sys.save_report(...)`, a TEA's `save_report(...)`, and `lca_aer.save_report(...)` all write the same file (system design, costs, utilities, and the LCA tables above on an `LCA` sheet). For the LCA tables only, use `get_impact_table` as shown in Section 5.2." ] }, { "cell_type": "code", "execution_count": 46, "id": "s5-save", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.486673Z", "iopub.status.busy": "2026-05-30T19:57:45.485673Z", "iopub.status.idle": "2026-05-30T19:57:45.488750Z", "shell.execute_reply": "2026-05-30T19:57:45.488750Z" } }, "outputs": [], "source": [ "# lca_aer.save_report('aer_report.xlsx')" ] }, { "cell_type": "markdown", "id": "s5-6-head", "metadata": {}, "source": [ "### 5.6. Cost and environmental trade-off" ] }, { "cell_type": "markdown", "id": "s5-6-intro", "metadata": {}, "source": [ "Section 5.5 compared the two plants on environmental impact, and the [7. TEA](https://qsdsan.readthedocs.io/en/latest/tutorials/7_TEA.html) tutorial compared them on cost. The decision a user actually faces puts the two together: which option wins on cost *and* on carbon?\n", "\n", "Because both analyses use the same two plants treating the same wastewater, we can line up a cost metric and an impact metric on the same functional unit (per m³ of wastewater treated) in one table. We reuse the break-even treatment fee from the TEA tutorial (the per-m³ fee that brings NPV to zero) and the GWP per m³ from Section 5.4:" ] }, { "cell_type": "code", "execution_count": 47, "id": "s5-6-code", "metadata": { "execution": { "iopub.execute_input": "2026-05-30T19:57:45.491761Z", "iopub.status.busy": "2026-05-30T19:57:45.491761Z", "iopub.status.idle": "2026-05-30T19:57:45.538397Z", "shell.execute_reply": "2026-05-30T19:57:45.538397Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
plantbreak-even fee (USD/m3)GWP (kg CO2-eq/m3)
0aerobic0.30.125
1anaerobic0.83-0.481
\n", "
" ], "text/plain": [ " plant break-even fee (USD/m3) GWP (kg CO2-eq/m3)\n", "0 aerobic 0.3 0.125\n", "1 anaerobic 0.83 -0.481" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "qs.PowerUtility.price = 0.08 # USD/kWh, matching the TEA tutorial\n", "\n", "rows = []\n", "for name, sys_, u, lca in (('aerobic', aer_sys, aer, lca_aer),\n", " ('anaerobic', ana_sys, ana, lca_ana)):\n", " tea = qs.TEA(sys_, discount_rate=0.05, lifetime=20)\n", " fee = -tea.solve_price(u.ins[0]) * 1000 # USD/m3, break-even treatment fee\n", " gwp = lca.get_normalized_impacts(u.ins[0], normalize_by='volume')['GlobalWarming']\n", " rows.append((name, round(fee, 2), round(gwp, 3)))\n", "pd.DataFrame(rows, columns=['plant', 'break-even fee (USD/m3)', 'GWP (kg CO2-eq/m3)'])" ] }, { "cell_type": "markdown", "id": "s5-6-interp", "metadata": {}, "source": [ "The two metrics point in opposite directions: the aerobic plant is cheaper to run for this dilute municipal wastewater, while the anaerobic plant has the lower (here net-negative) carbon footprint thanks to its biogas credit. No single option wins on both, so the choice depends on what a project weights more heavily, cost or carbon, and on context such as the wastewater strength (the TEA tutorial shows the cost ranking itself flips for stronger wastewater) or a price on carbon. Putting both metrics on the same per-m³ basis is what makes the trade-off legible." ] }, { "cell_type": "markdown", "id": "s5-6-note", "metadata": {}, "source": [ "
\n", "\n", "**Comparing like with like.** A comparison across systems is only meaningful when both share the same functional unit and the same system boundary. Here both plants treat the same wastewater (4,000 m³/d, COD about 430 mg/L) and both metrics are expressed per m³ treated, so the rows can be read across directly. If the systems treated different flows, or you drew the boundary differently (for example, crediting recovered biogas in one but not the other), normalize to a common functional unit (Section 5.4) and align the boundaries first.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "nav-footer-8_lca", "metadata": {}, "source": [ "\n", "\n", "---\n", "\n", "↑ Back to top\n" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }