From c4bd4a0053c90be5874592985cf36905f745dfcc Mon Sep 17 00:00:00 2001 From: romainsacchi Date: Mon, 8 Jul 2024 21:00:02 +0200 Subject: [PATCH] Adapt MC calculations in violin.py for BW 2.5 compatibility. + code cleanup. --- dev/Untitled.ipynb | 134 +++++-------------------------------------- polyviz/__init__.py | 2 +- polyviz/chord.py | 11 +--- polyviz/choro.py | 17 ++---- polyviz/dataframe.py | 12 +--- polyviz/force.py | 12 +--- polyviz/sankey.py | 46 +++++++++------ polyviz/treemap.py | 15 ++--- polyviz/utils.py | 27 ++++----- polyviz/violin.py | 55 +++++++++++------- requirements.txt | 2 +- setup.py | 2 +- 12 files changed, 109 insertions(+), 226 deletions(-) diff --git a/dev/Untitled.ipynb b/dev/Untitled.ipynb index b26520d..149cb68 100644 --- a/dev/Untitled.ipynb +++ b/dev/Untitled.ipynb @@ -2,148 +2,42 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "id": "8a5241f6-74e4-466c-9790-7704b2ced3d1", "metadata": {}, "outputs": [], "source": [ - "from d3blocks import D3Blocks\n", - "from polyviz import sankey, chord, force, violin, choro, treemap\n", + "from polyviz import sankey, chord, force, violin, choro, treemap, __version__, __file__\n", "import brightway2 as bw\n", - "bw.projects.set_current(\"new\")" + "bw.projects.set_current(\"ei39\")" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 30, "id": "f6ca17c9-5527-41f1-982d-e5dabd79dbfb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'maintenance, locomotive' (unit, RoW, None)" + "'SAF production and combustion, petroleum refinery operation, with syncrude' (kilogram, ES, None)" ] }, - "execution_count": 2, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#act = bw.Database(\"ecoinvent 3.8 cutoff\").random()\n", - "act = [a for a in bw.Database(\"ecoinvent 3.8 cutoff\") if a[\"name\"] == \"maintenance, locomotive\"][0]\n", + "act = [a for a in bw.Database(\"synhelion_common\") if a[\"name\"] == \"SAF production and combustion, petroleum refinery operation, with syncrude\"][0]\n", "act" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "17980b13-a441-450b-90f0-755abcbfc5b3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'operation, computer, desktop, with cathode ray tube display, active mode' (hour, CH, None)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "act = bw.Database(\"ecoinvent 3.8 cutoff\").random()\n", - "act" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "bde3273a-6ac1-4901-bbd4-59a7b3163a96", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'clinker production' (kilogram, RoW, None)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#act = bw.Database(\"ecoinvent 3.8 cutoff\").random()\n", - "act = [a for a in bw.Database(\"ecoinvent 3.8 cutoff\") if a[\"name\"] == \"clinker production\"][0]\n", - "act" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a17a4a8c-37f9-4f4b-b72b-e2e48ca31c60", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'operation, computer, desktop, with cathode ray tube display, active mode'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "act[\"reference product\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "de470b74-b3fd-461d-9d8c-61c5fafd247c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Calculating LCIA score...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/homebrew/Caskroom/miniforge/base/envs/ab/lib/python3.9/site-packages/scikits/umfpack/umfpack.py:736: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 6.19e+13)\n", - " warnings.warn(msg, UmfpackWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "'/Users/romain/GitHub/polyviz/dev/electricity high voltage production mix kilowatt hour CAYK IPCC 2013climate changeGWP 100a treemap.html'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "treemap(\n", - " activity=act,\n", - " method=('IPCC 2013', 'climate change', 'GWP 100a')\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, + "execution_count": 32, "id": "53f0db41-723c-4105-aaba-4bb7ff449aa6", "metadata": {}, "outputs": [ @@ -158,17 +52,17 @@ "name": "stderr", "output_type": "stream", "text": [ - "/opt/homebrew/Caskroom/miniforge/base/envs/ab/lib/python3.9/site-packages/scikits/umfpack/umfpack.py:736: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 6.19e+13)\n", + "/opt/homebrew/Caskroom/miniforge/base/envs/ab/lib/python3.9/site-packages/scikits/umfpack/umfpack.py:736: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 1.21e+13)\n", " warnings.warn(msg, UmfpackWarning)\n" ] }, { "data": { "text/plain": [ - "'/Users/romain/GitHub/polyviz/dev/operation computer desktop with cathode ray tube display active mode hour CH IPCC 2013climate changeGWP 100a sankey.html'" + "'/Users/romain/GitHub/polyviz/dev/SAF production and combustion petroleum refinery operation with syncrude kilogram ES IPCC 2021climate changeglobal warming potential GWP100 sankey.html'" ] }, - "execution_count": 4, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -176,10 +70,10 @@ "source": [ "sankey(\n", " activity=act,\n", - " level=3,\n", + " level=4,\n", " cutoff=0.01,\n", " #flow_type=\"kilogram\",\n", - " method=('IPCC 2013', 'climate change', 'GWP 100a')\n", + " method=('IPCC 2021', 'climate change', 'global warming potential (GWP100)')\n", ")" ] }, @@ -2193,7 +2087,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.9.15" } }, "nbformat": 4, diff --git a/polyviz/__init__.py b/polyviz/__init__.py index f26c70f..edf8561 100644 --- a/polyviz/__init__.py +++ b/polyviz/__init__.py @@ -2,7 +2,7 @@ PolyViz init """ -__version__ = (1, 0, 1) +__version__ = (1, 0, 2) __all__ = ( "sankey", diff --git a/polyviz/chord.py b/polyviz/chord.py index 4c70b78..4a51028 100644 --- a/polyviz/chord.py +++ b/polyviz/chord.py @@ -11,18 +11,13 @@ from .utils import calculate_supply_chain, check_filepath try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None + from bw2data.backends import Activity def chord( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple = None, flow_type: str = None, level: int = 3, diff --git a/polyviz/choro.py b/polyviz/choro.py index cf3e2a4..1a5659d 100644 --- a/polyviz/choro.py +++ b/polyviz/choro.py @@ -11,20 +11,13 @@ from .utils import check_filepath, get_geo_distribution_of_impacts_for_choro_graph try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None - -valid_types = tuple(filter(None, (PeeweeActivity, BW25Activity))) + from bw2data.backends import Activity def choro( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, cutoff: float = 0.001, filepath: str = None, @@ -49,8 +42,8 @@ def choro( assert isinstance(method, tuple), "`method` should be a tuple." assert isinstance( - activity, valid_types - ), "`activity` should be a brightway2 activity." + activity, Activity + ), "`activity` should be a Brightway activity." # fetch unit of method unit = bw2data.Method(method).metadata["unit"] diff --git a/polyviz/dataframe.py b/polyviz/dataframe.py index 54adcb1..ffc35a5 100644 --- a/polyviz/dataframe.py +++ b/polyviz/dataframe.py @@ -4,7 +4,6 @@ """ from collections import defaultdict -from io import StringIO from typing import List, Union import bw2data @@ -14,14 +13,9 @@ from .utils import calculate_lcia_score, get_gdp_per_country, get_region_definitions try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None + from bw2data.backends import Activity def format_supply_chain_dataframe( @@ -148,7 +142,7 @@ def add_country_column(dataframe: pd.DataFrame) -> pd.DataFrame: def get_geo_distribution_of_impacts( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, cutoff: float = 0.0001, ): diff --git a/polyviz/force.py b/polyviz/force.py index 4f45aeb..6ab4a11 100644 --- a/polyviz/force.py +++ b/polyviz/force.py @@ -4,25 +4,19 @@ from typing import Union -import bw2data from d3blocks import D3Blocks from .dataframe import format_supply_chain_dataframe from .utils import calculate_supply_chain, check_filepath try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None + from bw2data.backends import Activity def force( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, level: int = 3, cutoff: float = 0.01, diff --git a/polyviz/sankey.py b/polyviz/sankey.py index 27f8aaa..3a5418c 100644 --- a/polyviz/sankey.py +++ b/polyviz/sankey.py @@ -1,36 +1,35 @@ """ This module contains the code to generate a Sankey diagram for a given activity and method. """ +from typing import Optional, Tuple from typing import Union import bw2data from d3blocks import D3Blocks +from pandas import DataFrame from .dataframe import format_supply_chain_dataframe from .utils import calculate_supply_chain, check_filepath try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None - + from bw2data.backends import Activity def sankey( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple = None, flow_type: str = None, + amount: int = 1, level: int = 3, cutoff: float = 0.01, filepath: str = None, title: str = None, notebook: bool = False, -) -> str: + labels_swap: dict = None, + figsize: tuple = None, +) -> Optional[tuple[str, DataFrame]]: """ Generate a Sankey diagram for a given activity and method. :param activity: Brightway2 activity @@ -41,6 +40,8 @@ def sankey( :param filepath: Path to save the HTML file :param title: Title of the Sankey diagram :param notebook: Whether to display the Sankey diagram in a Jupyter notebook + :param labels_swap: Dictionary to swap labels in the diagram + :param figsize: Size of the figure :return: Path to the generated HTML file """ @@ -50,7 +51,13 @@ def sankey( title = title or f"{activity['name']} ({activity['unit']}, {activity['location']})" filepath = check_filepath(filepath, title, "sankey", method, flow_type) - result, amount = calculate_supply_chain(activity, method, level, cutoff) + result, amount = calculate_supply_chain( + activity=activity, + method=method, + level=level, + cutoff=cutoff, + amount=amount, + ) if method: assert isinstance(method, tuple), "`method` should be a tuple." @@ -66,18 +73,23 @@ def sankey( dataframe["unit"] = unit - if level != 3: - figsize = (800 / 3 * level, 600) - else: - figsize = (800, 600) + if figsize is None: + if level != 3: + figsize = (800 / 3 * level, 600) + else: + figsize = (800, 600) # dataframe should at least be 3 rows if len(dataframe) < 3: print("Not enough data to generate a Sankey diagram.") return + if labels_swap: + dataframe = dataframe.replace(labels_swap, regex=True) + # Create a new D3Blocks object d3_graph = D3Blocks() + d3_graph.sankey( df=dataframe[1:], link={"color": "source-target"}, @@ -87,4 +99,6 @@ def sankey( figsize=figsize, ) - return str(filepath) + print("Sankey diagram generated.") + + return str(filepath), dataframe diff --git a/polyviz/treemap.py b/polyviz/treemap.py index 9e5b9d7..8079d2b 100644 --- a/polyviz/treemap.py +++ b/polyviz/treemap.py @@ -11,20 +11,13 @@ from .utils import check_filepath try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None - -valid_types = tuple(filter(None, (PeeweeActivity, BW25Activity))) + from bw2data.backends import Activity def treemap( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, cutoff: float = 0.0001, filepath: str = None, @@ -49,7 +42,7 @@ def treemap( assert isinstance(method, tuple), "`method` should be a tuple." assert isinstance( - activity, valid_types + activity, Activity ), "`activity` should be a brightway2 activity." # fetch unit of method diff --git a/polyviz/utils.py b/polyviz/utils.py index abc13fa..6b0d540 100644 --- a/polyviz/utils.py +++ b/polyviz/utils.py @@ -15,23 +15,16 @@ from packaging.version import Version try: - from bw2data.backends.peewee import Activity as PeeweeActivity + from bw2data.backends.peewee import Activity except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity -except ImportError: - BW25Activity = None - -valid_types = tuple(filter(None, (PeeweeActivity, BW25Activity))) - + from bw2data.backends import Activity def calculate_supply_chain( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, level: int = 3, cutoff: float = 0.01, + amount: int = 1, ) -> [StringIO, int]: """ Calculate the supply chain of an activity. @@ -43,10 +36,10 @@ def calculate_supply_chain( """ assert isinstance( - activity, valid_types + activity, Activity ), "`activity` should be a brightway2 activity." - amount = -1 if identify_waste_process(activity) else 1 + amount = amount * -1 if identify_waste_process(activity) else amount print("Calculating supply chain score...") @@ -68,7 +61,7 @@ def calculate_supply_chain( def calculate_lcia_score( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, ) -> float: """ @@ -78,7 +71,7 @@ def calculate_lcia_score( :return: LCIA score, C matrix, and reverse dictionary """ assert isinstance( - activity, valid_types + activity, Activity ), "`activity` should be a brightway2 activity." print("Calculating LCIA score...") @@ -105,7 +98,7 @@ def make_name_safe(filename: str) -> str: ).rstrip() -def identify_waste_process(activity: Union[PeeweeActivity, BW25Activity]) -> bool: +def identify_waste_process(activity: Activity) -> bool: """ Identify if a process is a waste process. :param activity: a brightway2 activity @@ -119,7 +112,7 @@ def identify_waste_process(activity: Union[PeeweeActivity, BW25Activity]) -> boo def get_geo_distribution_of_impacts_for_choro_graph( - activity: Union[PeeweeActivity, BW25Activity], + activity: Activity, method: tuple, cutoff: float = 0.0001, ) -> pd.DataFrame: diff --git a/polyviz/violin.py b/polyviz/violin.py index 2c72785..0584765 100644 --- a/polyviz/violin.py +++ b/polyviz/violin.py @@ -5,23 +5,23 @@ from typing import Union import bw2data +import numpy as np import pandas as pd -from bw2calc.monte_carlo import MultiMonteCarlo +import bw2calc + +try: + from bw2calc.monte_carlo import MultiMonteCarlo +except ModuleNotFoundError: + MultiMonteCarlo = None + from d3blocks import D3Blocks from .utils import check_filepath try: - from bw2data.backends.peewee import Activity as PeeweeActivity -except ImportError: - PeeweeActivity = None - -try: - from bw2data.backends import Activity as BW25Activity + from bw2data.backends.peewee import Activity except ImportError: - BW25Activity = None - -valid_types = tuple(filter(None, (PeeweeActivity, BW25Activity))) + from bw2data.backends import Activity def violin( @@ -47,8 +47,8 @@ def violin( for act in activities: assert isinstance( - act, valid_types - ), "`activity` should be a brightway2 activity." + act, Activity + ), "`activity` should be a Brightway activity." def make_name(activities): """ @@ -65,22 +65,35 @@ def make_name(activities): filepath = check_filepath(filepath, title, "violin", method) - res = MultiMonteCarlo( - [{act: 1} for act in activities], - method, - iterations, - ).calculate() + if MultiMonteCarlo: + res = MultiMonteCarlo( + [{act: 1} for act in activities], + method, + iterations, + ).calculate() + else: + lca = bw2calc.LCA( + demand={activities[0]: 1}, + method=method, + use_distributions=True + ) + lca.lci() + lca.lcia() + res = np.zeros((len(activities), iterations)) + for a, activity in enumerate(activities): + lca.lci({activity.id: 1}) + res[a, :] = [lca.score for _ in zip(range(iterations), lca)] list_res = [] - for _r, r in enumerate(res): - vals = r[1:] + for r in range(0, res.shape[0]): + vals = res[r, :] list_res.extend( [ [ v, - f"{activities[_r]['name']} ({activities[_r]['location']})", + f"{activities[r]['name']} ({activities[r]['location']})", ] - for v in vals[0] + for v in vals ] ) diff --git a/requirements.txt b/requirements.txt index d2cebc0..467629b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pandas numpy git+https://github.com/romainsacchi/d3blocks.git -bw2io<=0.8.8 +bw2io bw2data bw2calc country_converter diff --git a/setup.py b/setup.py index aef349a..8b546ba 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def package_files(directory): setup( name="polyviz", - version="1.0.1", + version="1.0.2", packages=packages, author="Romain Sacchi ", license=open("LICENSE").read(),