diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 347bcf6..4ffc7bf 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -31,7 +31,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - py-version: ['3.9', '3.10', '3.11'] + py-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_methods.yml b/.github/workflows/test_methods.yml index 140a8ac..87845b7 100644 --- a/.github/workflows/test_methods.yml +++ b/.github/workflows/test_methods.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Update pip & install testing pkgs run: | diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..94f10b8 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,38 @@ +{ + "description": "Standardizes the format and flows of life cycle impact assessment (LCIA) data.", + "license": "MIT", + "title": "LCIA Formatter", + "upload_type": "software", + "creators": [ + { + "affiliation": "Eastern Research Group, Inc.", + "name": "Ben Young", + "orcid": "https://orcid.org/0000-0001-6276-8670" + }, + { + "affiliation": "GreenDelta", + "name": "Michael Srocka" + }, + { + "affiliation": "US Environmental Protection Agency", + "name": "Wesley Ingwersen", + "orcid": "https://orcid.org/0000-0002-9614-701X" + }, + { + "affiliation": "Eastern Research Group, Inc.", + "name": "Ben Morelli", + "orcid": "https://orcid.org/0000-0002-7660-6485" + }, + { + "affiliation": "Eastern Research Group, Inc.", + "name": "Sarah Cashman", + "orcid": "https://orcid.org/0000-0001-9859-9557" + }, + { + "affiliation": "Eastern Research Group, Inc.", + "name": "Andrew Henderson", + "orcid": "https://orcid.org/0000-0003-2436-7512" + } + ], + "access_right": "open" +} diff --git a/README.md b/README.md index 84c32a4..10b338b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # LCIA formatter -[![DOI](https://joss.theoj.org/papers/10.21105/joss.03392/status.svg)](https://doi.org/10.21105/joss.03392) +[![JOSS](https://joss.theoj.org/papers/10.21105/joss.03392/status.svg)](https://doi.org/10.21105/joss.03392) +[![DOI](https://zenodo.org/badge/188049640.svg)](https://zenodo.org/doi/10.5281/zenodo.7400857) [![build](https://github.com/USEPA/LCIAformatter/actions/workflows/python-package.yml/badge.svg)](https://github.com/USEPA/LCIAformatter/actions/workflows/python-package.yml) The LCIA formatter, or `lciafmt`, is a Python tool for standardizing the format and flows of life cycle impact assessment (LCIA) data. The tool acquires LCIA data transparently from its original @@ -18,6 +19,7 @@ The LCIA Formatter v1 was peer-reviewed internally at USEPA and externally throu |ImpactWorld+ Endpoint*|International Reference Center for Life Cycle of Products, Services and Systems (CIRAIG)|[ImpactWorld+](http://www.impactworldplus.org/en/team.php)| |IPCC GWP|Intergovernmental Panel on Climate Change (IPCC)| | |FEDEFL Inventory Methods|US Environmental Protection Agency|[FEDEFL Inventory Methods](https://github.com/USEPA/LCIAformatter/wiki/Inventory-Methods)| +|Cumulative Energy Demand|Federal LCA Commons|[FEDEFL Inventory Methods](https://github.com/USEPA/LCIAformatter/wiki/Inventory-Methods)| \* only works on Windows installations diff --git a/lciafmt/__init__.py b/lciafmt/__init__.py index 3843c40..5c9868f 100644 --- a/lciafmt/__init__.py +++ b/lciafmt/__init__.py @@ -13,6 +13,7 @@ import pandas as pd import lciafmt.cache as cache +import lciafmt.ced as ced import lciafmt.fmap as fmap import lciafmt.jsonld as jsonld import lciafmt.traci as traci @@ -34,6 +35,7 @@ class Method(Enum): TRACI = "TRACI 2.1" RECIPE_2016 = "ReCiPe 2016" FEDEFL_INV = "FEDEFL Inventory" + CED = "Cumulative Energy Demand" ImpactWorld = "ImpactWorld" IPCC = "IPCC" @@ -122,6 +124,8 @@ def get_method(method_id, add_factors_for_missing_contexts=True, return ipcc.get() if method_id == Method.FEDEFL_INV: return fedefl_inventory.get(subset) + if method_id == Method.CED: + return ced.get() def clear_cache(): diff --git a/lciafmt/ced.py b/lciafmt/ced.py new file mode 100644 index 0000000..a6c531c --- /dev/null +++ b/lciafmt/ced.py @@ -0,0 +1,60 @@ +# ced.py (lciafmt) +# !/usr/bin/env python3 +# coding=utf-8 +""" +Generate method for Cumulative Energy Demand (CED) +""" + +import numpy as np +import pandas as pd + +import lciafmt +from lciafmt.util import store_method + + +def get() -> pd.DataFrame(): + + inv_orig = lciafmt.get_method(method_id = 'FEDEFL_INV', subset = ['ced']) + inv = inv_orig.copy() + inv['Indicator'] = '' + inv['Method'] = 'Cumulative Energy Demand' + + conditions = [ + inv['Flowable'].isin(['Wood, primary forest']), + inv['Flowable'].isin(['Biomass', 'Softwood', 'Hardwood', 'Wood']), + inv['Flowable'].isin(['Energy, hydro']), + inv['Flowable'].str.contains('|'.join(['wind', 'solar', 'geothermal'])), + inv['Flowable'].str.contains('Uranium'), + inv['Flowable'].str.contains('|'.join(['Coal', 'Oil', 'Crude', + 'gas', 'Methane'])), + ] + + indicators = ['Non-renewable, biomass', + 'Renewable, biomass', + 'Renewable, water', + 'Renewable, wind, solar, geothermal', + 'Non-renewable, nuclear', + 'Non-renewable, fossil', + ] + inv['Indicator'] = np.select(conditions, indicators, default='') + + ## Original CED Method included the + # "Energy, fossil, unspecified" technosphere flow; + # this has been dropped + # https://www.lcacommons.gov/lca-collaboration/National_Renewable_Energy_Laboratory/USLCI_Database_Public/dataset/FLOW/46dc4693-2f24-39d2-b69f-dd059737fd5e + + ## Original CED Method used HHV for biomass/wood flows + # Some wood flows were removed after the original method in FEDEFLv1.0.8 + + # Dropped flows from FEDEFL inv method: "Hydrogen", "Energy, heat" + inv = inv.query('Indicator != ""').reset_index(drop=True) + + return inv + + +if __name__ == "__main__": + method = lciafmt.Method.CED + df = get() + store_method(df, method) + lciafmt.util.save_json(method, df) + # lciafmt.util.save_json(method, df, write_flows=True) diff --git a/lciafmt/data/ReCiPe2016_endpoint_to_midpoint.csv b/lciafmt/data/ReCiPe2016_endpoint_to_midpoint.csv index e3baedf..4684607 100644 --- a/lciafmt/data/ReCiPe2016_endpoint_to_midpoint.csv +++ b/lciafmt/data/ReCiPe2016_endpoint_to_midpoint.csv @@ -8,7 +8,7 @@ ,Toxicity - Human health (non-cancer),Human noncarcinogenic toxicity,Human health ,Water consumption - human health,Water consumption,Human health ,Global Warming - Terrestrial ecosystems,Global Warming,Terrestrial ecosystems -,Photochemical ozone formation - Terrestrial ecosystems,Ecosyste damage ozone formation,Terrestrial ecosystems +,Photochemical ozone formation - Terrestrial ecosystems,Ecosystem damage ozone formation,Terrestrial ecosystems ,Acidification - Terrestrial ecosystems,Terrestrial acidification,Terrestrial ecosystems ,Toxicity - Terrestrial ecosystems,Terrestrial ecotoxicity,Terrestrial ecosystems ,Water consumption - terrestrial ecosystems,Water consumption,Terrestrial ecosystems @@ -16,7 +16,7 @@ ,Global Warming - Freshwater ecosystems,Global Warming,Freshwater ecosystems ,Eutrophication - Freshwater ecosystems,Freshwater eutrophication,Freshwater ecosystems ,Toxicity - Freshwater ecosystems,Freshwater ecotoxicity,Freshwater ecosystems -,Water consumption -aquatic ecosystems,Water consumption,Freshwater ecosystems +,Water consumption - aquatic ecosystems,Water consumption,Freshwater ecosystems ,Toxicity - Marine ecosystems,Marine ecotoxicity,Marine ecosystems ,Eutrophication - Marine ecosystems,Marine eutrophication,Marine ecosystems ,Mineral resource scarcity,Mineral resource scarcity,Resources diff --git a/lciafmt/data/description.yaml b/lciafmt/data/description.yaml index de20bad..70fea4e 100644 --- a/lciafmt/data/description.yaml +++ b/lciafmt/data/description.yaml @@ -1,12 +1,14 @@ base: >+ [Method][version] is built from LCIA Formatter v[LCIAfmt_version] and - flows from the Federal Elementary Flow List (FEDEFL) v[FEDEFL_version] + flows from v[FEDEFL_version] of the Federal Elementary Flow List (FEDEFL), + using fedelemflowlist v[fedelemflowlist_version]. description: >+ [Method] source file: [url] Source citation: [citation] +mapping: >+ [Method] flowable and context input files are maintained in the FEDEFL GitHub Repository: https://github.com/USEPA/fedelemflowlist diff --git a/lciafmt/data/lcia.bib b/lciafmt/data/lcia.bib index 5f96129..f6a3bb0 100644 --- a/lciafmt/data/lcia.bib +++ b/lciafmt/data/lcia.bib @@ -45,3 +45,12 @@ @incollection{smith_earths_2021 author = {Smith, Chris and Nicholls, Zebedee R. J. and Armour, Kyle and Collins, William and Forster, Piers and Meinshausen, Malte and Palmer, Matthew D. and Watanabe, Masahiro}, year = {2021}, } + +@report{frischknecht_implementation_2007, + title = {Implementation of life cycle impact assessment methods}, + url = {https://esu-services.ch/fileadmin/download/publicLCI/03_LCIA-Implementation.pdf}, + number = {ecoinvent report No. 3}, + author = {Frischknecht, Rolf and Jungbluth, Niels and Althaus, H. J. and Bauer, C. and Doka, G. and Dones, R. and Hischier, R. and Hellweg, S. and Humbert, S. and Köllner, T.}, + urldate = {2024-11-13}, + year = {2007}, +} diff --git a/lciafmt/data/methods.json b/lciafmt/data/methods.json index d971d5f..1c733cc 100644 --- a/lciafmt/data/methods.json +++ b/lciafmt/data/methods.json @@ -25,7 +25,7 @@ "ReCiPe 2016 - Midpoint/E":"ReCiPe 2016 v1.1 midpoint method, Egalitarian version. The typical ReCiPe midpoint method used is the Hierarchist version."}, "path": "recipe", "case_insensitivity": "True", - "url": "http://www.rivm.nl/sites/default/files/2018-11/ReCiPe2016_CFs_v1.1_20180117.xlsx", + "url": "https://www.rivm.nl/sites/default/files/2024-10/ReCiPe2016_CFs_v1.1_20180117.xlsx", "bib_id": "huijbregts_recipe_2017", "citation": "Huijbregts 2017", "source_type": "Excel file" @@ -33,9 +33,9 @@ { "id": "FEDEFL_INV", "name": "FEDEFL Inventory", - "version": "1.0.0", + "version": "1.1.0", "path": "fedefl", - "url": "http://www.github.com/usepa//Federal-LCA-Commons-Elementary-Flow-List", + "url": "https://github.com/USEPA/fedelemflowlist/blob/master/fedelemflowlist/subset_list.py", "citation": "", "source_type": "FEDEFL Python function" }, @@ -49,7 +49,7 @@ "ImpactWorld+ - Endpoint":"This method includes all global endpoint flows and their attributes."}, "path": "impactworld", "case_insensitivity": "False", - "url": "http://www.impactworldplus.org/en/writeToFile.php", + "url": "https://www.dropbox.com/sh/2sdgbqf08yn91bc/AABIGLlb_OwfNy6oMMDZNrm0a/IWplus_public_v1.3.accdb?dl=1", "citation": "Bulle, Cecile, Manuele Margni, Laure Patouillard, Anne-Marie Boulay, Guillaume Bourgault, Vincent De Bruille, Viet Cao, et al. IMPACT World+: A Globally Regionalized Life Cycle Impact Assessment Method. The International Journal of Life Cycle Assessment 24, no. 9 (September 2019), 1653–74. https://doi.org/10.1007/s11367-019-01583-0", "source_type": "Access file" }, @@ -67,6 +67,17 @@ }, "citation": "Forster and Ramaswamy 2007 (AR4), Myhre and Shindell 2013 (AR5), Forster and Storelvmo 2021 (AR6)", "source_type": "csv" + }, + { + "id": "CED", + "name": "Cumulative Energy Demand", + "version": "1.0", + "detail_note": "All heating values are per the FEDEFL, and the list of external references is found on the EPA FEDEFL GitHub at: https://github.com/USEPA/fedelemflowlist. This CED method is based on the categorization scheme used in Frischknecht et al. (2007) found in the ecoinvent report 'Implementation of Life Cycle Impact Assessment Methods.'", + "path": "fedefl", + "bib_id": "frischknecht_implementation_2007", + "url": "https://github.com/USEPA/fedelemflowlist/blob/master/fedelemflowlist/subset_list.py", + "citation": "Frischknecht et al. 2007", + "source_type": "" } ] diff --git a/lciafmt/fedefl_inventory.py b/lciafmt/fedefl_inventory.py index 4b243c4..8adedfa 100644 --- a/lciafmt/fedefl_inventory.py +++ b/lciafmt/fedefl_inventory.py @@ -27,7 +27,8 @@ def get(subset=None) -> pd.DataFrame: method['Characterization Factor']) if subset is None: - list_of_inventories = subsets.get_subsets() + list_of_inventories = [s for s in subsets.get_subsets() if s + not in ('ced')] else: list_of_inventories = subset @@ -39,7 +40,7 @@ def get(subset=None) -> pd.DataFrame: axis=1, inplace=True) flows['Indicator'] = inventory flows['Indicator unit'] = subsets.get_inventory_unit(inventory) - flows['Characterization Factor'] = 1 + flows['Characterization Factor'] = 1.0 # Apply unit conversions where flow unit differs from indicator unit flows_w_conversion = pd.merge(flows, alt_units, how='left', @@ -56,3 +57,6 @@ def get(subset=None) -> pd.DataFrame: method['Method'] = 'FEDEFL Inventory' return method + +if __name__ == "__main__": + df = get() diff --git a/lciafmt/iw.py b/lciafmt/iw.py index de66529..fdf9f1c 100644 --- a/lciafmt/iw.py +++ b/lciafmt/iw.py @@ -6,6 +6,7 @@ """ import pandas as pd +import lciafmt import lciafmt.cache as cache import lciafmt.df as dfutil from lciafmt.util import log, format_cas @@ -38,13 +39,10 @@ def get(file=None, url=None, region=None) -> pd.DataFrame: "Please install drivers to remotely connect to Access Database. " "Drivers only available on windows platform. For instructions visit: " "https://github.com/mkleehammer/pyodbc/wiki/Connecting-to-Microsoft-Access") - + method_meta = lciafmt.Method.ImpactWorld.get_metadata() f = file if f is None: - fname = "Impact_World.accdb" - if url is None: - url = "https://www.dropbox.com/sh/2sdgbqf08yn91bc/AABIGLlb_OwfNy6oMMDZNrm0a/IWplus_public_v1.3.accdb?dl=1" - f = cache.get_or_download(fname, url) + f = _get_file(method_meta, url) df = _read(f, region) # Identify midpoint and endpoint records and differentiate in data frame. @@ -58,6 +56,12 @@ def get(file=None, url=None, region=None) -> pd.DataFrame: return df +def _get_file(method_meta, url=None): + fname = "Impact_World.accdb" + if url is None: + url = method_meta['url'] + f = cache.get_or_download(fname, url) + return f def _read(access_file: str, region) -> pd.DataFrame: """Read the Access database at passed access_file into DataFrame.""" diff --git a/lciafmt/jsonld.py b/lciafmt/jsonld.py index 8eb10a9..8bf2ff6 100644 --- a/lciafmt/jsonld.py +++ b/lciafmt/jsonld.py @@ -74,7 +74,7 @@ def write(self, df: pd.DataFrame, write_flows=False, preferred_only=False): indicator = self.__indicator(row) factor = o.ImpactFactor() unit = row['Unit'] - factor.flow = self.__flow(row) + factor.flow = self.__flow(row).to_ref() factor.flow_property = units.property_ref(unit) factor.unit = units.unit_ref(unit) factor.value = row['Characterization Factor'] diff --git a/lciafmt/recipe.py b/lciafmt/recipe.py index 3ddcfa5..0bf46bf 100644 --- a/lciafmt/recipe.py +++ b/lciafmt/recipe.py @@ -9,6 +9,7 @@ import pandas as pd import openpyxl +import lciafmt import lciafmt.cache as cache import lciafmt.df as dfutil import lciafmt.xls as xls @@ -52,13 +53,10 @@ def get(add_factors_for_missing_contexts=True, endpoint=True, :return: DataFrame of method in standard format """ log.info("getting method ReCiPe 2016") + method_meta = lciafmt.Method.RECIPE_2016.get_metadata() f = file if f is None: - fname = "recipe_2016.xlsx" - if url is None: - url = ("http://www.rivm.nl/sites/default/files/2018-11/" + - "ReCiPe2016_CFs_v1.1_20180117.xlsx") - f = cache.get_or_download(fname, url) + f = _get_file(method_meta, url) df = _read(f) if add_factors_for_missing_contexts: log.info("adding average factors for primary contexts") @@ -135,6 +133,14 @@ def get(add_factors_for_missing_contexts=True, endpoint=True, return df +def _get_file(method_meta, url=None): + fname = "recipe_2016.xlsx" + if url is None: + url = method_meta['url'] + f = cache.get_or_download(fname, url) + return f + + def _read(file: str) -> pd.DataFrame: log.info(f"read ReCiPe 2016 from file {file}") wb = openpyxl.load_workbook(file, read_only=True, data_only=True) @@ -171,7 +177,7 @@ def _read_endpoints(file: str) -> pd.DataFrame: continue endpoints['Method'] = "ReCiPe 2016 - Midpoint/" + perspectives[i] endpoints['EndpointMethod'] = "ReCiPe 2016 - Endpoint/" + perspectives[i] - endpoints['EndpointIndicator'] = indicator + endpoints['EndpointIndicator'] = indicator.replace(' -a', ' - a') # fix missing space endpoints['EndpointUnit'] = indicator_unit endpoints['EndpointConversion'] = val endpoint = pd.concat( @@ -215,6 +221,7 @@ def _read_mid_points(sheet: openpyxl.worksheet.worksheet.Worksheet, cas_col = _find_cas_column(sheet) indicator_unit, flow_unit, unit_col = _determine_units(sheet) compartment, compartment_col = _determine_compartments(sheet) + sheet_title = sheet.title.replace('Ecosyste damage', 'Ecosystem damage') perspectives = ["I", "H", "E"] factor_count = 0 @@ -238,7 +245,7 @@ def _read_mid_points(sheet: openpyxl.worksheet.worksheet.Worksheet, continue dfutil.record(records, method="ReCiPe 2016 - Midpoint/" + perspectives[i], - indicator=sheet.title, + indicator=sheet_title, indicator_unit=indicator_unit, flow=xls.cell_str(row[flow_col]), flow_category=compartment, @@ -253,7 +260,7 @@ def _read_mid_points(sheet: openpyxl.worksheet.worksheet.Worksheet, for p in perspectives: dfutil.record(records, method="ReCiPe 2016 - Midpoint/" + p, - indicator=sheet.title, + indicator=sheet_title, indicator_unit=indicator_unit, flow=xls.cell_str(row[flow_col]), flow_category=compartment, diff --git a/lciafmt/traci.py b/lciafmt/traci.py index 97d6265..d061633 100644 --- a/lciafmt/traci.py +++ b/lciafmt/traci.py @@ -38,10 +38,7 @@ def get(add_factors_for_missing_contexts=True, file=None, method_meta = lciafmt.Method.TRACI.get_metadata() f = file if f is None: - fname = "traci_2.1.xlsx" - if url is None: - url = method_meta['url'] - f = cache.get_or_download(fname, url) + f = _get_file(method_meta, url) df = _read(f) if add_factors_for_missing_contexts: log.info("adding average factors for primary contexts") @@ -72,6 +69,12 @@ def get(add_factors_for_missing_contexts=True, file=None, return df +def _get_file(method_meta, url=None): + fname = "traci_2.1.xlsx" + if url is None: + url = method_meta['url'] + f = cache.get_or_download(fname, url) + return f def _read(xls_file: str) -> pd.DataFrame: """Read the data from Excel with given path into a DataFrame.""" diff --git a/lciafmt/util.py b/lciafmt/util.py index 6ef28e1..f91decb 100644 --- a/lciafmt/util.py +++ b/lciafmt/util.py @@ -21,7 +21,7 @@ # set version number of package, needs to be updated with setup.py -pkg_version_number = '1.1.3' +pkg_version_number = '1.1.4' MODULEPATH = Path(__file__).resolve().parent datapath = MODULEPATH / 'data' @@ -187,6 +187,8 @@ def generate_method_description(name: str, else: method_meta = method.get_metadata() desc += generic['description'] + if 'mapping' in method_meta: + desc += generic['mapping'] if 'detail_note' in method_meta: desc += method_meta['detail_note'] if 'methods' in method_meta: @@ -208,6 +210,7 @@ def generate_method_description(name: str, desc = (desc .replace('[LCIAfmt_version]', pkg_version_number) .replace('[FEDEFL_version]', flow_list_specs['list_version']) + .replace('[fedelemflowlist_version]', flow_list_specs['tool_version']) .replace('[Method]', method_meta['name']) .replace('[version]', version) .replace('[citation]', method_meta['citation']) diff --git a/setup.py b/setup.py index d0e0e56..72d7b1c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="lciafmt", - version="1.1.3", + version="1.1.4", packages=["lciafmt"], package_dir={'lciafmt': 'lciafmt'}, package_data={'lciafmt': ["data/*.*"]}, diff --git a/tests/test_generate_methods.py b/tests/test_generate_methods.py index 4d516e5..67a48fd 100644 --- a/tests/test_generate_methods.py +++ b/tests/test_generate_methods.py @@ -23,6 +23,12 @@ def test_generate_methods(): assert not error_list +def test_url_access(): + import lciafmt.iw as impactworld + f = lciafmt.recipe._get_file(lciafmt.Method.RECIPE_2016.get_metadata()) + f = lciafmt.traci._get_file(lciafmt.Method.TRACI.get_metadata()) + f = impactworld._get_file(lciafmt.Method.ImpactWorld.get_metadata()) + def test_endpoint_method(): method = lciafmt.generate_endpoints('Weidema_valuation', name='Weidema Valuation', @@ -63,4 +69,5 @@ def test_compilation_method(): if __name__ == "__main__": # test_generate_methods() # test_method_write_json() + # test_url_access() test_compilation_method()