Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
515 changes: 515 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest


def pytest_configure(config: pytest.Config) -> None:
"""Register custom markers to suppress PytestUnknownMarkWarning."""
config.addinivalue_line(
"markers",
"slow: marks tests requiring Premise DB generation (>30s) — skip with '-m not slow'",
)
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "swolfpy"
dynamic = ["version", "readme", "dependencies"]
description = "Solid Waste Optimization Life-cycle Framework in Python(SwolfPy)."
license = {text = "GNU GENERAL PUBLIC LICENSE V2"}
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [
{name = "Mojtaba Sardarmehni", email = "msardar2@alumni.ncsu.edu"},
]
Expand Down Expand Up @@ -75,6 +75,9 @@ python_files = [ # a list of patterns to use to collect test modules
"tests/test_*.py"
]
addopts = "--cov=swolfpy --cov-report=xml --disable-warnings --ignore=swolfpy/UI/ --color=yes"
markers = [
"slow: marks tests requiring Premise DB generation (>30s) — skip with '-m not slow'",
]

#############################
####### pylint #######
Expand Down
15 changes: 8 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
brightway2==2.4.1 # TODO: Upgrade to 2.5
bw-migrations==0.1
bw2analyzer==0.10
bw2calc==1.8.2
bw2data==3.6.6
bw2io==0.8.7
bw2parameters==1.1.0
brightway25>=1.0
bw2data>=4.0
bw2calc>=2.0
bw2io>=0.9
bw2analyzer>=0.11
bw2parameters>=1.1.0
bw_temporalis>=1.0
premise>=2.3.7
coverage
graphviz
jupyter
Expand Down
144 changes: 103 additions & 41 deletions swolfpy/LCA_matrix.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,121 @@
# -*- coding: utf-8 -*-
import bw2calc as bc
import bw2data as bd
import numpy as np
import pandas as pd
from brightway2 import LCA, get_activity
import scipy.sparse


class LCA_matrix(LCA):
class LCA_matrix(bc.LCA):
"""
This class translate the ``row`` and ``col`` of the ``tech_param`` and ``bio_param``
to the activity `key` in the Brightway2 database. Both the ``tech_param`` and
``bio_param`` has the ``dtype=[('input', '<u4'), ('output', '<u4'), ('row', '<u4'),
('col', '<u4'), ('type', 'u1'), ('uncertainty_type', 'u1'), ('amount', '<f4'), ('loc',
'<f4'), ('scale', '<f4'), ('shape', '<f4'), ('minimum', '<f4'), ('maximum', '<f4'),
('negative', '?')])`` data type.
Translates the row and col indices of the technosphere and biosphere sparse matrices
to activity keys in the Brightway2 database.

``self.tech_matrix`` is a dictionary that includes all the technosphere and waste exchanges as tuple ``(product,Feed)`` key and amount as value:
``{(('LF', 'Aerobic_Residual'), ('SF1_product', 'Aerobic_Residual_MRDO')):0.828}``
``self.tech_matrix`` is a dictionary that maps ``(product_key, activity_key)`` tuples
to exchange amounts. Example:
``{(('LF', 'Aerobic_Residual'), ('SF1_product', 'Aerobic_Residual_MRDO')): 0.828}``

``self.bio_matrix`` is a dictionary that includes all the biosphere exchanges as tuple ``(product,Feed)`` `key` and amount as `value`
``{(('biosphere3', '0015ec22-72cb-4af1-8c7b-0ba0d041553c'), ('Technosphere', 'Boiler_Diesel')):6.12e-15}``
``self.bio_matrix`` is a dictionary that maps ``(biosphere_key, activity_key)`` tuples
to exchange amounts. Example:
``{(('biosphere3', '0015ec22-72cb-4af1-8c7b-0ba0d041553c'), ('Technosphere', 'Boiler_Diesel')): 6.12e-15}``

So we can update the ``tech_params`` and ``bio_params`` by tuple keys that are consistent with the keys
in the ``ProcessModel.report()``. Check :ref:`Process models class <ProcessModel>` for more info.
These dicts are updated by ``update_techmatrix`` and ``update_biomatrix`` and then
used to rebuild the sparse matrices for Monte Carlo and Optimization runs via
``rebuild_technosphere_matrix`` and ``rebuild_biosphere_matrix``.

"""

def __init__(self, functional_unit, method):
super().__init__(functional_unit, method[0])
def __init__(self, functional_unit: dict, method: list) -> None:
fu, data_objs, _ = bd.prepare_lca_inputs(functional_unit, method=method[0])
super().__init__(demand=fu, data_objs=data_objs)
self.lci()
self.lcia()

self.functional_unit = functional_unit
self.method = method
self._base_method = method[0]

self.activities_dict, _, self.biosphere_dict = self.reverse_dict()
# Populate lca.dicts.{activity, product, biosphere} with actual keys
self.remap_inventory_dicts()

# Note: bc.LCA has properties `activity_dict` and `biosphere_dict` that return
# forward mappings (key → int). We need reversed mappings (int → key) for
# compatibility with swolfpy code (e.g., Optimization.py line 194).
# Use underscored names to avoid collision with parent class properties.
self._activities_dict_reversed = self.dicts.activity.reversed
self._biosphere_dict_reversed = self.dicts.biosphere.reversed

# Build tech_matrix from sparse technosphere matrix (COO preserves entry order)
tech_coo = self.technosphere_matrix.tocoo()
self._tech_coo_rows = tech_coo.row.copy()
self._tech_coo_cols = tech_coo.col.copy()
self._tech_shape = self.technosphere_matrix.shape

self.tech_matrix = {}
for i in self.tech_params:
self.tech_matrix[(self.activities_dict[i[2]], self.activities_dict[i[3]])] = i[6]
for i, j, v in zip(self._tech_coo_rows, self._tech_coo_cols, tech_coo.data):
row_key = self.dicts.product.reversed[i]
col_key = self.dicts.activity.reversed[j]
self.tech_matrix[(row_key, col_key)] = v

# Build bio_matrix from sparse biosphere matrix (COO preserves entry order)
bio_coo = self.biosphere_matrix.tocoo()
self._bio_coo_rows = bio_coo.row.copy()
self._bio_coo_cols = bio_coo.col.copy()
self._bio_shape = self.biosphere_matrix.shape

self.bio_matrix = {}
for i in self.bio_params:
if (
self.biosphere_dict[i[2]],
self.activities_dict[i[3]],
) not in self.bio_matrix.keys():
self.bio_matrix[(self.biosphere_dict[i[2]], self.activities_dict[i[3]])] = i[6]
_bio_seen: set = set()
for i, j, v in zip(self._bio_coo_rows, self._bio_coo_cols, bio_coo.data):
bio_key = self.dicts.biosphere.reversed[i]
col_key = self.dicts.activity.reversed[j]
key = (bio_key, col_key)
if key not in _bio_seen:
self.bio_matrix[key] = v
_bio_seen.add(key)
else:
self.bio_matrix[
(str(self.biosphere_dict[i[2]]) + " - 1", self.activities_dict[i[3]])
] = i[6]
# print((str(biosphere_dict[i[2]]) + " - 1", activities_dict[i[3]]))
# Defensive: handle rare duplicate biosphere flows by appending suffix
self.bio_matrix[(str(bio_key) + " - 1", col_key)] = v

# ------------------------------------------------------------------
# Matrix rebuild helpers (BW2.5 replacement for rebuild_*_matrix)
# ------------------------------------------------------------------

def rebuild_technosphere_matrix(self, values: np.ndarray) -> None:
"""
Rebuild the technosphere sparse matrix from an ordered array of values.

The values array must be in the same insertion order as ``self.tech_matrix``
(i.e., COO order from matrix initialisation).

:param values: New exchange amounts in COO entry order.
:type values: numpy.ndarray
"""
self.technosphere_matrix = scipy.sparse.csr_matrix(
(values, (self._tech_coo_rows, self._tech_coo_cols)),
shape=self._tech_shape,
)

def rebuild_biosphere_matrix(self, values: np.ndarray) -> None:
"""
Rebuild the biosphere sparse matrix from an ordered array of values.

The values array must be in the same insertion order as ``self.bio_matrix``
(i.e., COO order from matrix initialisation).

:param values: New exchange amounts in COO entry order.
:type values: numpy.ndarray
"""
self.biosphere_matrix = scipy.sparse.csr_matrix(
(values, (self._bio_coo_rows, self._bio_coo_cols)),
shape=self._bio_shape,
)

# ------------------------------------------------------------------
# Static matrix update helpers
# ------------------------------------------------------------------

@staticmethod
def update_techmatrix(process_name, report_dict, tech_matrix):
def update_techmatrix(process_name: str, report_dict: dict, tech_matrix: dict) -> None:
"""
Updates the `tech_matrix` according to the `report_dict`. `tech_matrix` is an
instance of ``LCA_matrix.tech_matrix``. Useful for Monte Carlo simulation, and
Expand Down Expand Up @@ -176,7 +238,7 @@ def update_techmatrix(process_name, report_dict, tech_matrix):
)

@staticmethod
def update_biomatrix(process_name, report_dict, bio_matrix):
def update_biomatrix(process_name: str, report_dict: dict, bio_matrix: dict) -> None:
"""
Updates the `bio_matrix` according to the report_dict. `bio_matrix` is an
instance of ``LCA_matrix.bio_matrix``. Useful for Monte Carlo simulation, and
Expand Down Expand Up @@ -262,7 +324,7 @@ def update_biomatrix(process_name, report_dict, bio_matrix):
)

@staticmethod
def get_mass_flow(LCA, process):
def get_mass_flow(LCA, process: str) -> float:
"""
Calculates the total mass of flows to process based on the `supply_array` in
``bw2calc.lca.LCA``.
Expand All @@ -278,17 +340,17 @@ def get_mass_flow(LCA, process):

"""
mass = 0
for i in LCA.activity_dict:
for i in LCA.dicts.activity:
if process == i[0]:
unit = get_activity(i).as_dict()["unit"].split(" ")
unit = bd.get_node(database=i[0], code=i[1]).as_dict()["unit"].split(" ")
if len(unit) > 1:
mass += LCA.supply_array[LCA.activity_dict[i]] * float(unit[0])
mass += LCA.supply_array[LCA.dicts.activity[i]] * float(unit[0])
else:
mass += LCA.supply_array[LCA.activity_dict[i]]
mass += LCA.supply_array[LCA.dicts.activity[i]]
return mass

@staticmethod
def get_mass_flow_comp(LCA, process, index):
def get_mass_flow_comp(LCA, process: str, index) -> pd.Series:
"""
Calculates the mass of flows to process based on the `index` and `supply_array` in
``bw2calc.lca.LCA``.
Expand All @@ -307,13 +369,13 @@ def get_mass_flow_comp(LCA, process, index):

"""
mass = pd.Series(np.zeros(len(index)), index=index)
for i in LCA.activity_dict:
for i in LCA.dicts.activity:
if process == i[0]:
for j in index:
if j == i[1]:
unit = get_activity(i).as_dict()["unit"].split(" ")
unit = bd.get_node(database=i[0], code=i[1]).as_dict()["unit"].split(" ")
if len(unit) > 1:
mass[j] += LCA.supply_array[LCA.activity_dict[i]] * float(unit[0])
mass[j] += LCA.supply_array[LCA.dicts.activity[i]] * float(unit[0])
else:
mass[j] += LCA.supply_array[LCA.activity_dict[i]]
mass[j] += LCA.supply_array[LCA.dicts.activity[i]]
return mass
Loading