diff --git a/bw2calc/multi_lca.py b/bw2calc/multi_lca.py index b13ab04..cf4de16 100644 --- a/bw2calc/multi_lca.py +++ b/bw2calc/multi_lca.py @@ -15,6 +15,7 @@ from .errors import OutsideTechnosphere from .lca import LCABase from .method_config import MethodConfig +from .restricted_sparse_matrix_dict import RestrictedSparseMatrixDict from .single_value_diagonal_matrix import SingleValueDiagonalMatrix from .utils import consistent_global_index, get_datapackage, utc_now @@ -300,8 +301,9 @@ def load_normalization_data( if len(value.matrix.data) == 0: warnings.warn(f"All values in normalization matrix for {key} are zero") - self.normalization_matrices = mu.SparseMatrixDict( - [(key, value.matrix) for key, value in self.normalization_mm_dict.items()] + self.normalization_matrices = RestrictedSparseMatrixDict( + self.config["normalizations"], + [(key, value.matrix) for key, value in self.normalization_mm_dict.items()], ) def load_weighting_data( @@ -330,8 +332,9 @@ def load_weighting_data( if len(value.matrix.data) == 0: warnings.warn(f"All values in weighting matrix for {key} are zero") - self.weighting_matrices = mu.SparseMatrixDict( - [(key, value.matrix) for key, value in self.weighting_mm_dict.items()] + self.weighting_matrices = RestrictedSparseMatrixDict( + self.config["weightings"], + [(key, value.matrix) for key, value in self.weighting_mm_dict.items()], ) ################ diff --git a/bw2calc/restricted_sparse_matrix_dict.py b/bw2calc/restricted_sparse_matrix_dict.py new file mode 100644 index 0000000..6587c96 --- /dev/null +++ b/bw2calc/restricted_sparse_matrix_dict.py @@ -0,0 +1,35 @@ +from typing import Any + +from matrix_utils import SparseMatrixDict +from pydantic import BaseModel + + +class RestrictionsValidator(BaseModel): + restrictions: dict[tuple[str, ...], list[tuple[str, ...]]] + + +class RestrictedSparseMatrixDict(SparseMatrixDict): + def __init__(self, restrictions: dict, *args, **kwargs): + """Like SparseMatrixDict, but follows `restrictions` on what can be multiplied. + + Only for use with normalization and weighting.""" + super().__init__(*args, **kwargs) + RestrictionsValidator(restrictions=restrictions) + self._restrictions = restrictions + + def __matmul__(self, other: Any) -> SparseMatrixDict: + """Define logic for `@` matrix multiplication operator. + + Note that the sparse matrix dict must come first, i.e. `self @ other`. + """ + if isinstance(other, (SparseMatrixDict, RestrictedSparseMatrixDict)): + return SparseMatrixDict( + { + (a, *b): c @ d + for a, c in self.items() + for b, d in other.items() + if b[0] in self._restrictions[a] + } + ) + else: + return super().__matmul__(other) diff --git a/tests/fixtures/create_fixtures.py b/tests/fixtures/create_fixtures.py index 1498500..9787882 100644 --- a/tests/fixtures/create_fixtures.py +++ b/tests/fixtures/create_fixtures.py @@ -517,6 +517,15 @@ def create_multilca_simple(): distributions_array=distributions_array, global_index=0, ) + dp6.add_persistent_vector( + matrix="normalization_matrix", + data_array=np.ones(2).astype(float), + name="normalization-2", + identifier=("n", "2"), + indices_array=indices_array, + distributions_array=distributions_array, + global_index=0, + ) dp7 = create_datapackage( fs=ZipFileSystem(fixture_dir / "multi_lca_simple_weighting.zip", mode="w"), @@ -536,6 +545,14 @@ def create_multilca_simple(): indices_array=indices_array, distributions_array=distributions_array, ) + dp7.add_persistent_vector( + matrix="weighting_matrix", + data_array=np.array([84]), + name="weighting-2", + identifier=("w", "2"), + indices_array=indices_array, + distributions_array=distributions_array, + ) dp1.finalize_serialization() dp2.finalize_serialization() diff --git a/tests/fixtures/multi_lca_simple_1.zip b/tests/fixtures/multi_lca_simple_1.zip index d71dbfd..fd0bdfd 100644 Binary files a/tests/fixtures/multi_lca_simple_1.zip and b/tests/fixtures/multi_lca_simple_1.zip differ diff --git a/tests/fixtures/multi_lca_simple_2.zip b/tests/fixtures/multi_lca_simple_2.zip index ca893a1..44f74f9 100644 Binary files a/tests/fixtures/multi_lca_simple_2.zip and b/tests/fixtures/multi_lca_simple_2.zip differ diff --git a/tests/fixtures/multi_lca_simple_3.zip b/tests/fixtures/multi_lca_simple_3.zip index 7864180..3794695 100644 Binary files a/tests/fixtures/multi_lca_simple_3.zip and b/tests/fixtures/multi_lca_simple_3.zip differ diff --git a/tests/fixtures/multi_lca_simple_4.zip b/tests/fixtures/multi_lca_simple_4.zip index 627f599..148da14 100644 Binary files a/tests/fixtures/multi_lca_simple_4.zip and b/tests/fixtures/multi_lca_simple_4.zip differ diff --git a/tests/fixtures/multi_lca_simple_5.zip b/tests/fixtures/multi_lca_simple_5.zip index 15b5fa0..da3d5c0 100644 Binary files a/tests/fixtures/multi_lca_simple_5.zip and b/tests/fixtures/multi_lca_simple_5.zip differ diff --git a/tests/fixtures/multi_lca_simple_normalization.zip b/tests/fixtures/multi_lca_simple_normalization.zip index 1a2a6a3..48f8485 100644 Binary files a/tests/fixtures/multi_lca_simple_normalization.zip and b/tests/fixtures/multi_lca_simple_normalization.zip differ diff --git a/tests/fixtures/multi_lca_simple_weighting.zip b/tests/fixtures/multi_lca_simple_weighting.zip index bb162bd..0f4c564 100644 Binary files a/tests/fixtures/multi_lca_simple_weighting.zip and b/tests/fixtures/multi_lca_simple_weighting.zip differ diff --git a/tests/multi_lca.py b/tests/multi_lca.py index 86cfefe..b2ad0a7 100644 --- a/tests/multi_lca.py +++ b/tests/multi_lca.py @@ -61,18 +61,6 @@ def test_inventory_matrix_construction(dps, config, func_units): mlca.lci() mlca.lcia() - print(mlca.scores) - print(mlca.technosphere_matrix.todense()) - print(mlca.biosphere_matrix.todense()) - - for name, mat in mlca.characterization_matrices.items(): - print(name) - print(mat.todense()) - - for name, arr in mlca.supply_arrays.items(): - print(name) - print(arr) - tm = [ (100, 1, -0.2), (100, 2, -0.5), @@ -208,10 +196,6 @@ def test_normalization_with_weighting(dps, func_units): rows = np.zeros(mlca.biosphere_matrix.shape[0]) rows = np.array([mlca.dicts.biosphere[201], mlca.dicts.biosphere[203]]) - for k, v in mlca.weighting_matrices.items(): - print(k) - print(v.todense()) - for key, mat in mlca.weighted_inventories.items(): expected = mlca.characterized_inventories[key[2:]][rows, :].sum() assert np.allclose(mat.sum(), expected * 42) @@ -542,3 +526,51 @@ def test_monte_carlo_multiple_iterations_selective_use_in_list_comprehension(dps for key, lst in aggregated.items(): assert np.unique(lst).shape == (10,) + + +def test_bug_108(dps, config, func_units): + # https://github.com/brightway-lca/brightway2-calc/issues/108 + config = { + "impact_categories": [ + ("first", "category"), + ("second", "category"), + ], + "normalizations": { + ("n", "1"): [("first", "category")], + ("n", "2"): [("second", "category")], + }, + "weightings": { + ("w", "1"): [("n", "1")], + ("w", "2"): [("n", "2")], + }, + } + + dps.append( + get_datapackage(fixture_dir / "multi_lca_simple_normalization.zip"), + ) + dps.append( + get_datapackage(fixture_dir / "multi_lca_simple_weighting.zip"), + ) + + mlca = MultiLCA(demands=func_units, method_config=config, data_objs=dps) + mlca.lci() + mlca.lcia() + mlca.normalize() + mlca.weight() + + assert len(mlca.scores) == len(func_units) * 2 + assert sorted(mlca.scores) == sorted( + [ + (("w", "1"), ("n", "1"), ("first", "category"), "γ"), + (("w", "1"), ("n", "1"), ("first", "category"), "ε"), + (("w", "1"), ("n", "1"), ("first", "category"), "ζ"), + (("w", "2"), ("n", "2"), ("second", "category"), "γ"), + (("w", "2"), ("n", "2"), ("second", "category"), "ε"), + (("w", "2"), ("n", "2"), ("second", "category"), "ζ"), + ] + ) + assert ( + mlca.scores[(("w", "2"), ("n", "2"), ("second", "category"), "ζ")] + == 3 * (3 * 10 + 1 * 10) * 84 + ) + assert mlca.scores[(("w", "1"), ("n", "1"), ("first", "category"), "γ")] == 3 * 42 diff --git a/tests/test_restricted_sparse_matrix_dict.py b/tests/test_restricted_sparse_matrix_dict.py new file mode 100644 index 0000000..bc48788 --- /dev/null +++ b/tests/test_restricted_sparse_matrix_dict.py @@ -0,0 +1,33 @@ +import pytest +from matrix_utils import SparseMatrixDict +from pydantic import ValidationError + +from bw2calc.restricted_sparse_matrix_dict import RestrictedSparseMatrixDict, RestrictionsValidator + + +class Dummy: + def __init__(self, a): + self.a = a + + def __matmul__(self, other): + return self.a + other + + +def test_restricted_sparse_matrix_dict(): + smd = SparseMatrixDict({(("one",), "foo"): 1, (("two",), "bar"): 2}) + rsmd = RestrictedSparseMatrixDict( + {("seven",): [("one",)], ("eight",): [("two",)]}, + {("seven",): Dummy(7), ("eight",): Dummy(8)}, + ) + + result = rsmd @ smd + assert isinstance(result, SparseMatrixDict) + assert len(result) == 2 + assert result[(("seven",), ("one",), "foo")] == 8 + assert result[(("eight",), ("two",), "bar")] == 10 + + +def test_restrictions_validator(): + assert RestrictionsValidator(restrictions={("seven",): [("one",)], ("eight",): [("two",)]}) + with pytest.raises(ValidationError): + RestrictionsValidator(restrictions={"seven": [("one",)], ("eight",): [("two",)]})