"
]
},
"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",
" Quantity | \n",
" Item Ratio | \n",
" FossilEnergyConsumption [MJ] | \n",
" Category FossilEnergyConsumption Ratio | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Category GlobalWarming Ratio | \n",
"
\n",
" \n",
" | Construction | \n",
" SanUnit | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | Concrete [m3] | \n",
" aer | \n",
" 300 | \n",
" 1 | \n",
" 2.7e+05 | \n",
" 0.265 | \n",
" 9e+04 | \n",
" 0.6 | \n",
"
\n",
" \n",
" | Total | \n",
" 300 | \n",
" 1 | \n",
" 2.7e+05 | \n",
" 0.265 | \n",
" 9e+04 | \n",
" 0.6 | \n",
"
\n",
" \n",
" | Steel [kg] | \n",
" aer | \n",
" 3e+04 | \n",
" 1 | \n",
" 7.5e+05 | \n",
" 0.735 | \n",
" 6e+04 | \n",
" 0.4 | \n",
"
\n",
" \n",
" | Total | \n",
" 3e+04 | \n",
" 1 | \n",
" 7.5e+05 | \n",
" 0.735 | \n",
" 6e+04 | \n",
" 0.4 | \n",
"
\n",
" \n",
" | Sum | \n",
" All | \n",
" | \n",
" | \n",
" 1.02e+06 | \n",
" 1 | \n",
" 1.5e+05 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" Quantity | \n",
" Item Ratio | \n",
" FossilEnergyConsumption [MJ] | \n",
" Category FossilEnergyConsumption Ratio | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Category GlobalWarming Ratio | \n",
"
\n",
" \n",
" | Transportation | \n",
" SanUnit | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | Trucking [kg*km] | \n",
" aer | \n",
" 1.26e+08 | \n",
" 1 | \n",
" 2.53e+05 | \n",
" 1 | \n",
" 1.26e+04 | \n",
" 1 | \n",
"
\n",
" \n",
" | Total | \n",
" 1.26e+08 | \n",
" 1 | \n",
" 2.53e+05 | \n",
" 1 | \n",
" 1.26e+04 | \n",
" 1 | \n",
"
\n",
" \n",
" | Sum | \n",
" All | \n",
" | \n",
" | \n",
" 2.53e+05 | \n",
" 1 | \n",
" 1.26e+04 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" Mass [kg] | \n",
" FossilEnergyConsumption [MJ] | \n",
" Category FossilEnergyConsumption Ratio | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Category GlobalWarming Ratio | \n",
"
\n",
" \n",
" | Stream | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | aer_sludge | \n",
" 2.53e+06 | \n",
" 0 | \n",
" 0 | \n",
" 5.05e+05 | \n",
" 1 | \n",
"
\n",
" \n",
" | Sum | \n",
" | \n",
" 0 | \n",
" 1 | \n",
" 5.05e+05 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" Quantity | \n",
" FossilEnergyConsumption [MJ] | \n",
" Category FossilEnergyConsumption Ratio | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Category GlobalWarming Ratio | \n",
"
\n",
" \n",
" | Other | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | electricity | \n",
" 5.96e+06 | \n",
" 3.54e+07 | \n",
" 1 | \n",
" 2.98e+06 | \n",
" 1 | \n",
"
\n",
" \n",
" | Sum | \n",
" | \n",
" 3.54e+07 | \n",
" 1 | \n",
" 2.98e+06 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" Quantity/yr | \n",
" Item Ratio | \n",
" FossilEnergyConsumption [MJ/yr] | \n",
" Category FossilEnergyConsumption Ratio | \n",
" GlobalWarming [kg CO2-eq/yr] | \n",
" Category GlobalWarming Ratio | \n",
"
\n",
" \n",
" | Construction | \n",
" SanUnit | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | Concrete [m3] | \n",
" aer | \n",
" 15 | \n",
" 1 | \n",
" 1.35e+04 | \n",
" 0.265 | \n",
" 4.5e+03 | \n",
" 0.6 | \n",
"
\n",
" \n",
" | Total | \n",
" 15 | \n",
" 1 | \n",
" 1.35e+04 | \n",
" 0.265 | \n",
" 4.5e+03 | \n",
" 0.6 | \n",
"
\n",
" \n",
" | Steel [kg] | \n",
" aer | \n",
" 1.5e+03 | \n",
" 1 | \n",
" 3.75e+04 | \n",
" 0.735 | \n",
" 3e+03 | \n",
" 0.4 | \n",
"
\n",
" \n",
" | Total | \n",
" 1.5e+03 | \n",
" 1 | \n",
" 3.75e+04 | \n",
" 0.735 | \n",
" 3e+03 | \n",
" 0.4 | \n",
"
\n",
" \n",
" | Sum | \n",
" All | \n",
" | \n",
" | \n",
" 5.1e+04 | \n",
" 1 | \n",
" 7.5e+03 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" FossilEnergyConsumption [MJ] | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Allocation factor | \n",
"
\n",
" \n",
" | Stream | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | aer_effluent | \n",
" 3.66e+07 | \n",
" 3.14e+06 | \n",
" 1 | \n",
"
\n",
" \n",
" | aer_sludge | \n",
" 3.18e+03 | \n",
" 273 | \n",
" 8.67e-05 | \n",
"
\n",
" \n",
"
\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",
" FossilEnergyConsumption [MJ] | \n",
" GlobalWarming [kg CO2-eq] | \n",
" Allocation factor | \n",
"
\n",
" \n",
" | Stream | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | aer_effluent | \n",
" 2.2e+07 | \n",
" 1.89e+06 | \n",
" 0.6 | \n",
"
\n",
" \n",
" | aer_sludge | \n",
" 1.47e+07 | \n",
" 1.26e+06 | \n",
" 0.4 | \n",
"
\n",
" \n",
"
\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",
" plant | \n",
" GWP (kg CO2-eq) | \n",
" FEC (MJ) | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" aerobic | \n",
" 3650107 | \n",
" 36639655 | \n",
"
\n",
" \n",
" | 1 | \n",
" anaerobic | \n",
" -14059593 | \n",
" -356717192 | \n",
"
\n",
" \n",
"
\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",
" plant | \n",
" break-even fee (USD/m3) | \n",
" GWP (kg CO2-eq/m3) | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" aerobic | \n",
" 0.3 | \n",
" 0.125 | \n",
"
\n",
" \n",
" | 1 | \n",
" anaerobic | \n",
" 0.83 | \n",
" -0.481 | \n",
"
\n",
" \n",
"
\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
}