Repository: USEPA/LCIAformatter

Issue:
Enable writing sources for methods and impact categories to json-ld

Discussion:

Patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index b699eae..347bcf6 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -34,11 +34,11 @@ jobs:
         py-version: ['3.9', '3.10', '3.11']
         
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     
     # general Python setup
     - name: Set up Python ${{ matrix.py-version }}
-      uses: actions/setup-python@v3
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.py-version }}
         
@@ -52,6 +52,7 @@ jobs:
     - name: Install package and dependencies
       run: |
         pip install .
+        pip install bibtexparser # for testing bibliographies via esupy
         
     # - name: Install package and dependencies (Windows)
     #   if: matrix.os == 'windows-latest'
diff --git a/format specs/LCIAcompilation.md b/format specs/LCIAcompilation.md
new file mode 100644
index 0000000..43cfd9d
--- /dev/null
+++ b/format specs/LCIAcompilation.md
@@ -0,0 +1,15 @@
+## LCIA Compilation
+
+Custom methods consisting of indicators from multiple indicators can be created using `lciafmt.generate_lcia_compilation()`. 
+This requires an input `.yaml` file with the following specifications.
+See [compilation.yaml](../tests/compilation.yaml) for an example.
+
+```
+name: # Name given to new method
+rename_indicators: # True or False
+indicators: # Include as many unique indicators as desired as shown
+    Climate change: # The provided indicator name will be used if `rename_indicators` is `True`
+      method: IPCC # Method name from exisiting methods available in LCIAfmt
+      indicator: AR5-100 # Indicator name available within the selected method
+      code: GHG # (optional) code
+```
diff --git a/format specs/LCIAmethod.md b/format specs/LCIAmethod.md
index 222d3d8..d74b24d 100644
--- a/format specs/LCIAmethod.md
+++ b/format specs/LCIAmethod.md
@@ -6,9 +6,9 @@ The flow name fields match those in the fedelemflowlist [FlowList](https://githu
  Index | Field | Type | Required |  Note |
 | ---- | ------ |  ---- | ---------| -----  |
  0 | Method | string | Y | The LCIA method name, e.g. 'Traci 2.1' |
- 1 | Method UUID | string | Y | ID for the method  |
+ 1 | Method UUID | string | N | ID for the method, generated if not supplied  |
  2 | Indicator | string | Y | Name of indicator, e.g. 'Acidification Potential' |
- 3 | Indicator UUID| string | Y | ID for the indicator |
+ 3 | Indicator UUID| string | N | ID for the indicator, generated if not supplied |
  4 | Indicator unit | string | Y | The unit for the indicator, e.g. 'kg CO2 eq' |
  5 | Flowable | string | Y | The flow name, e.g. 'Sulfur dioxide' |
  6 | Flow UUID | string | Y | ID of the flow |
@@ -18,3 +18,4 @@ The flow name fields match those in the fedelemflowlist [FlowList](https://githu
  10 | Location | string | N | Name of the location
  11 | Location UUID | string | N | ID of the location
  12 | Characterization factor | float | Y | LCIA characterization factor
+ 13 | Code | string | N | string abbreviation for indicator
diff --git a/lciafmt/__init__.py b/lciafmt/__init__.py
index c78c5d0..382019a 100644
--- a/lciafmt/__init__.py
+++ b/lciafmt/__init__.py
@@ -22,6 +22,7 @@ import lciafmt.fedefl_inventory as fedefl_inventory
 import lciafmt.util as util
 import lciafmt.endpoint as ep
 import lciafmt.custom as custom
+from lciafmt.custom import generate_lcia_compilation
 
 
 from enum import Enum
@@ -153,10 +154,11 @@ def map_flows(df: pd.DataFrame, system=None, mapping=None,
     x = mapped[mapped[['Method', 'Indicator', 'Flowable', 'Flow UUID']
                       ].duplicated(keep=False)]
     duplicates = list(set(zip(x.Indicator, x.Flowable)))
-    util.log.warn(f'Identified duplicate factors for {len(duplicates)} '
-                  f'flow/indicator combinations and {len(x)} factors.')
-    util.log.debug(f'{duplicates}')
-    util.log.warn(f'Use collapse_indicators() to drop these duplicates.')
+    if len(duplicates) > 0:
+        util.log.warning(f'Identified duplicate factors for {len(duplicates)} '
+                         f'flow/indicator combinations and {len(x)} factors.')
+        util.log.debug(f'{duplicates}')
+        util.log.warning('Use collapse_indicators() to drop these duplicates.')
     return mapped
 
 
diff --git a/lciafmt/custom.py b/lciafmt/custom.py
index 3d7118e..fcbb267 100644
--- a/lciafmt/custom.py
+++ b/lciafmt/custom.py
@@ -6,8 +6,12 @@ Functions to create custom manual methods
 """
 
 import pandas as pd
+from pathlib import Path
+import yaml
 
+import lciafmt
 from lciafmt.df import lciafmt_cols
+from lciafmt.util import datapath
 
 def get_custom_method(file: str=None, input_df=None):
     """Converts a dataframe or a csv filepath to a dataframe suitable for
@@ -23,3 +27,45 @@ def get_custom_method(file: str=None, input_df=None):
     else:
         raise Exception
 
+
+def generate_lcia_compilation(filename: str,
+                              filepath: Path=None
+                              ) -> pd.DataFrame:
+    """Generates a dataframe consisting of indicators from various LCIA methods
+    based on specs provided in yaml file.
+    """
+    if not filepath:
+        filepath = datapath
+    with open(filepath / filename) as f:
+        build_file = yaml.safe_load(f)
+    build_df = pd.DataFrame(build_file['indicators']).transpose()
+
+    indicator_dict = (build_df.groupby('method')
+                      .apply(lambda x: [y for y in x['indicator']])
+                      .to_dict())
+    name_dict = build_df['indicator'].to_dict()
+    code_dict = {}
+    if 'code' in build_df:
+        code_dict = build_df['code'].to_dict()
+
+    method_list = []
+    for method, indicators in indicator_dict.items():
+        df = lciafmt.get_mapped_method(method_id = method,
+                                       indicators = indicators,
+                                       methods = [method])
+        method_list.append(df)
+
+    df = pd.concat(method_list, ignore_index=True)
+
+    df['category'] = df['Method']
+    df['source_method'] = df['Method']
+    df['Method'] = build_file.get('name')
+    if build_file.get('rename_indicators', False):
+        df['category'] = df['Method']
+        df['source_indicator'] = df['Indicator']
+        df['Indicator'] = df['Indicator'].replace({v: k for k, v in name_dict.items()})
+        if code_dict:
+            df['Code'] = df['Indicator'].map(code_dict)
+
+    return df
+
diff --git a/lciafmt/data/ISO21930-LCIA-US.yaml b/lciafmt/data/ISO21930-LCIA-US.yaml
new file mode 100644
index 0000000..932b38c
--- /dev/null
+++ b/lciafmt/data/ISO21930-LCIA-US.yaml
@@ -0,0 +1,28 @@
+name: ISO21930-LCIA-US
+version: v0.1
+rename_indicators: True
+indicators:
+    Greenhouse Gases:
+      method: IPCC
+      indicator: AR5-100
+      code: GHG
+
+    Ozone depletion potential:
+      method: TRACI 2.1
+      indicator: Ozone depletion
+      code: ODP
+
+    Eutrophication potential:
+      method: TRACI 2.1
+      indicator: Eutrophication
+      code: EP
+
+    Acidification potential:
+      method: TRACI 2.1
+      indicator: Acidification
+      code: AP
+
+    Photochemical oxidant creation potential:
+      method: TRACI 2.1
+      indicator: Smog formation
+      code: POCP
diff --git a/lciafmt/data/description.yaml b/lciafmt/data/description.yaml
index 96721c6..de20bad 100644
--- a/lciafmt/data/description.yaml
+++ b/lciafmt/data/description.yaml
@@ -1,26 +1,27 @@
-description:
-    '[Method][version] is built from LCIA Formatter v[LCIAfmt_version] and flows from the Federal 
-    Elementary Flow List (FEDEFL) v[FEDEFL_version]
-    
+base: >+
+    [Method][version] is built from LCIA Formatter v[LCIAfmt_version] and
+    flows from the Federal Elementary Flow List (FEDEFL) v[FEDEFL_version]
+
+description: >+
     [Method] source file: [url]
     
     Source citation: [citation]
     
-    [Method] flowable and context input files are maintained in the FEDEFL GitHub Repository: 
-    https://github.com/USEPA/Federal-LCA-Commons-Elementary-Flow-List 
+    [Method] flowable and context input files are maintained in the FEDEFL
+    GitHub Repository: https://github.com/USEPA/fedelemflowlist
     
-        
-    The LCIA Formatter (https://github.com/USEPA/LCIAformatter) applies [Method] 
-    characterization factors to flows using the mapping housed in the FEDEFL. 
-    The flowable and context input files for [Method] are used by the LCIA Formatter 
-    to translate the names from the [Method] source file and generate a mapping 
-    file for the FEDEFL. This file contains all of the accepted flowable and context 
+    The LCIA Formatter (https://github.com/USEPA/LCIAformatter) applies [Method]
+    characterization factors to flows using the mapping housed in the FEDEFL.
+    The flowable and context input files for [Method] are used by the LCIA Formatter
+    to translate the names from the [Method] source file and generate a mapping
+    file for the FEDEFL. This file contains all of the accepted flowable and context
     combinations which can be mapped to [Method] characterization factors. It also
     includes target UUID, units, and conversion factors as well as tracking
     who mapped and verified each record and when they were last updated.
-    
-    
-    '
 
-indicator:
-    '[Indicator] is built from LCIA Formatter v[LCIAfmt_version] for use with [Method]'
+indicator: >
+    [Indicator] is built with LCIA Formatter v[LCIAfmt_version] for use with [Method]
+
+source_indicator: >
+    [Indicator] is built with LCIA Formatter v[LCIAfmt_version] based on
+    [source_indicator] in [Method]
diff --git a/lciafmt/data/lcia.bib b/lciafmt/data/lcia.bib
new file mode 100644
index 0000000..1fd71f2
--- /dev/null
+++ b/lciafmt/data/lcia.bib
@@ -0,0 +1,18 @@
+@article{bare_traci_2011,
+	title = {{TRACI} 2.0: the tool for the reduction and assessment of chemical and other environmental impacts 2.0},
+	volume = {13},
+	pages = {687--696},
+	number = {5},
+	journal = {Clean Technologies and Environmental Policy},
+	author = {Bare, Jane},
+	doi = {10.1007/s10098-010-0338-9},
+	year = {2011}
+}
+
+@article{huijbregts_recipe_2017,
+	title = {{ReCiPe} 2016: A harmonized life cycle impact assessment method at midpoint and endpoint level Report I: Characterization},
+	journaltitle = {International Journal of Life Cycle Assessment},
+	author = {Huijbregts, M. A. J. and Steinmann, Z. J. N. and Elshout, P. M. F. and Stam, G. and Verones, F. and Vieira, M. D. M. and Hollander, A. and Zijp, M. and {van Zelm}, R.},
+	year = {2017},
+	DOI = {10.1007/s11367-016-1246-y}
+}
diff --git a/lciafmt/data/methods.json b/lciafmt/data/methods.json
index f42adaa..92314dc 100644
--- a/lciafmt/data/methods.json
+++ b/lciafmt/data/methods.json
@@ -7,6 +7,7 @@
     "mapping": "TRACI2.1",
     "case_insensitivity": "False",
     "url": "https://www.epa.gov/sites/default/files/2015-12/traci_2_1_2014_dec_10_0.xlsx",
+    "bib_id": "bare_traci_2011",
     "citation": "Bare 2012",
     "source_type": "Excel file"
   },
@@ -25,6 +26,7 @@
     "path": "recipe",
     "case_insensitivity": "True",
     "url": "http://www.rivm.nl/sites/default/files/2018-11/ReCiPe2016_CFs_v1.1_20180117.xlsx",
+    "bib_id": "huijbregts_recipe_2017",
     "citation": "Huijbregts 2017",
     "source_type": "Excel file"
   },
@@ -55,6 +57,7 @@
     "id": "IPCC",
     "name": "IPCC",
     "mapping": "IPCC",
+    "detail_note": "Factors are based on those reported in Table 2.14 (AR4), Table 8.A.1 (AR5), and Table 7.SM.7 (AR6).",
     "path": "ipcc",
     "case_insensitivity": "False",
     "url": "https://github.com/USEPA/LCIAformatter/blob/master/lciafmt/data/IPCC_GWP_values.csv",
diff --git a/lciafmt/endpoint.py b/lciafmt/endpoint.py
index ea3430e..3fc49b8 100644
--- a/lciafmt/endpoint.py
+++ b/lciafmt/endpoint.py
@@ -30,7 +30,7 @@ def apply_endpoints(endpoints, matching_fields, download_from_remote=False):
     method = pd.DataFrame()
 
     for e in matching_fields:
-        endpoints[e].fillna("", inplace=True)
+        endpoints[e] = endpoints[e].fillna("")
 
     for m in indicators['Method'].unique():
         method_indicators = indicators[indicators['Method'] == m]
diff --git a/lciafmt/ipcc.py b/lciafmt/ipcc.py
index b61b54b..1a0e670 100644
--- a/lciafmt/ipcc.py
+++ b/lciafmt/ipcc.py
@@ -9,6 +9,10 @@ import pandas as pd
 from lciafmt.util import log, datapath
 from lciafmt.df import lciafmt_cols
 
+### IPCC data for each AR were sourced from the following:
+## AR4: Table 2.14 (https://archive.ipcc.ch/publications_and_data/ar4/wg1/en/errataserrata-errata.html#table214)
+## AR5: Table 8.A.1 (https://archive.ipcc.ch/pdf/assessment-report/ar5/wg1/WG1AR5_Chapter08_FINAL.pdf)
+## AR6: Table 7.SM.7 (https://github.com/IPCC-WG1/Chapter-7/blob/main/data_output/7sm/metrics_supplement_cleaned.csv)
 
 def get() -> pd.DataFrame:
     """Generate a method for IPCC in standard format.
diff --git a/lciafmt/iw.py b/lciafmt/iw.py
index 5d7dd68..de66529 100644
--- a/lciafmt/iw.py
+++ b/lciafmt/iw.py
@@ -18,12 +18,13 @@ except ImportError:
               "and retry.")
 
 
-def get(file=None, url=None) -> pd.DataFrame:
+def get(file=None, url=None, region=None) -> pd.DataFrame:
     """Generate a method for ImpactWorld+ in standard format.
 
     :param file: str, alternate filepath for method, defaults to file stored
         in cache
     :param url: str, alternate url for method, defaults to url in method config
+    :param region: str, 3-digit code for Region; if not specified uses Global values
     :return: DataFrame of method in standard format
     """
     log.info("get method ImpactWorld+")
@@ -44,7 +45,7 @@ def get(file=None, url=None) -> pd.DataFrame:
         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)
-    df = _read(f)
+    df = _read(f, region)
 
     # Identify midpoint and endpoint records and differentiate in data frame.
     end_point_units = ['DALY', 'PDF.m2.yr']
@@ -58,7 +59,7 @@ def get(file=None, url=None) -> pd.DataFrame:
     return df
 
 
-def _read(access_file: str) -> pd.DataFrame:
+def _read(access_file: str, region) -> pd.DataFrame:
     """Read the Access database at passed access_file into DataFrame."""
     log.info(f"read ImpactWorld+ from file {access_file}")
 
@@ -84,6 +85,7 @@ def _read(access_file: str) -> pd.DataFrame:
                       flow_category=row[3] + "/" + row[4],
                       flow_unit=row[8],
                       cas_number=format_cas(row[6]).lstrip("0"),
+                      location='Global',
                       factor=row[7])
 
     """List relevant sheets in Impact World Access file. Second item in tuple
@@ -103,11 +105,15 @@ def _read(access_file: str) -> pd.DataFrame:
                        ("CF - regionalized - EutroFW - aggregated", "Compartment"),
                        ]
 
-    for x in regional_sheets:
-        if x[0] == "CF - regionalized - PartMatterForm - aggregated":
+    for sheet, compartment in regional_sheets:
+        if sheet == "CF - regionalized - PartMatterForm - aggregated":
             # Extract global flows from the particulate matter Access sheet
-            # Structure of this sheet is
-            sql = "SELECT * FROM [" + x[0] + "] WHERE (([" + x[0] + "].Region In('World')))"
+
+            region_dict = {
+                'USA': 'US+Latin America',
+                }
+            reg = region_dict.get(region, 'World')
+            sql = f"SELECT * FROM [{sheet}] WHERE (([{sheet}].Region In('{reg}')))"
             crsr.execute(sql)
             rows = crsr.fetchall()
 
@@ -120,10 +126,15 @@ def _read(access_file: str) -> pd.DataFrame:
                               flow_category="Air/" + row.__getattribute__("Archetype 1"),
                               flow_unit=row.Unit.strip('[]').split('/')[1],
                               cas_number="",
+                              location=reg,
                               factor=row.CFvalue)
 
         else:
-            sql = "SELECT * FROM [" + x[0] + "] WHERE (([" + x[0] + "].Resolution In('Global', 'Not regionalized')))"
+            reg = region if region else 'GLO'
+            sql = (f"SELECT * FROM [{sheet}] WHERE "
+                   f"(([{sheet}].[Region code] In('{reg}')) OR "
+                   f"([{sheet}].Resolution In('Not regionalized')))")
+            # ^^ 'Not regionalized' applies in all cases
             crsr.execute(sql)
             rows = crsr.fetchall()
 
@@ -132,7 +143,7 @@ def _read(access_file: str) -> pd.DataFrame:
 
             for row in rows:
                 # Add water to detailed context information available in Access file
-                if x[0] in ['CF - regionalized - WaterScarc - aggregated',
+                if sheet in ['CF - regionalized - WaterScarc - aggregated',
                             'CF - regionalized - WaterAvailab_HH - aggregated']:
                     flow_stmt = 'Water, ' + row.__getattribute__('Elem flow')
                 else:
@@ -140,14 +151,14 @@ def _read(access_file: str) -> pd.DataFrame:
 
                 # Define context/compartment for flow based on impact category.
                 if {'Compartment', 'Subcompartment'}.issubset(cols):
-                    category_stmt = row.Compartment + "/" + row.Subcompartment
-                elif x[0] in ['CF - regionalized - LandTrans - aggregated',
-                              'CF - regionalized - LandOcc - aggregated',
-                              'CF - regionalized - WaterScarc - aggregated',
-                              'CF - regionalized - WaterAvailab_HH - aggregated']:
+                    category_stmt = f"{row.Compartment}/{row.Subcompartment}"
+                elif sheet in ['CF - regionalized - LandTrans - aggregated',
+                               'CF - regionalized - LandOcc - aggregated',
+                               'CF - regionalized - WaterScarc - aggregated',
+                               'CF - regionalized - WaterAvailab_HH - aggregated']:
                     category_stmt = flow_stmt
                 else:
-                    category_stmt = x[1]
+                    category_stmt = compartment
 
                 dfutil.record(records,
                               method="ImpactWorld+",
@@ -157,6 +168,7 @@ def _read(access_file: str) -> pd.DataFrame:
                               flow_category=category_stmt,
                               flow_unit=row.Unit.strip('[]').split('/')[1],
                               cas_number="",
+                              location=reg,
                               factor=row.__getattribute__('Weighted Average'))
 
     return dfutil.data_frame(records)
diff --git a/lciafmt/jsonld.py b/lciafmt/jsonld.py
index 9b590a0..b2b0f0c 100644
--- a/lciafmt/jsonld.py
+++ b/lciafmt/jsonld.py
@@ -16,8 +16,9 @@ except ImportError:
                       "openLCA v2.0. Use pip install olca-schema")
 
 from esupy.util import make_uuid
+from esupy.bibtex import generate_sources
 from .util import is_non_empty_str, generate_method_description,\
-    log, pkg_version_number
+    log, pkg_version_number, datapath, check_as_class
 
 
 class Writer(object):
@@ -28,6 +29,9 @@ class Writer(object):
         self.__methods = {}
         self.__indicators = {}
         self.__flows = {}
+        self.__sources = {}
+        self.__bibids = {}
+        self.__bibpath = datapath / 'lcia.bib'
 
     def __enter__(self):
         return self
@@ -36,6 +40,21 @@ class Writer(object):
         self.__writer.close()
 
     def write(self, df: pd.DataFrame, write_flows=False):
+        if 'source_method' not in df:
+            df['source_method'] = df['Method']
+        if 'source_indicator' not in df:
+            df['source_indicator'] = ''
+        if 'category' not in df:
+            df['category'] = df['Method']
+
+        for method in df['source_method'].unique():
+            m = check_as_class(method)
+            bib = m.get_metadata().get('bib_id')
+            if bib:
+                self.__bibids[bib] = m.value
+        for i in generate_sources(self.__bibpath, self.__bibids):
+            self.__sources[i.id] = i
+
         for _, row in df.iterrows():
             indicator = self.__indicator(row)
             factor = o.ImpactFactor()
@@ -49,7 +68,8 @@ class Writer(object):
         log.debug("write entities")
         dicts = [
             self.__indicators,
-            self.__methods
+            self.__methods,
+            self.__sources
         ]
         if write_flows:
             dicts.append(self.__flows)
@@ -60,8 +80,7 @@ class Writer(object):
     def __indicator(self, row) -> o.ImpactCategory:
         uid = row['Indicator UUID']
         if not is_non_empty_str(uid):
-            uid = make_uuid(row['Method'], row['Indicator'])
-
+            uid = make_uuid(row['category'], row['Indicator'])
         ind = self.__indicators.get(uid)
         if ind is not None:
             return ind
@@ -70,29 +89,30 @@ class Writer(object):
         ind.id = uid
         ind.name = row['Indicator']
         ind.ref_unit = row['Indicator unit']
-        ind.category = row['Method']
+        ind.category = row['category']
+        if 'Code' in row:
+            ind.code = row['Code']
         direction = ('INPUT' if row['Context'].startswith('resource')
                      else 'OUTPUT')
         ind.direction = o.Direction(direction)
-        ind.description = generate_method_description(row['Method'],
-                                                      row['Indicator'])
+        ind.description = generate_method_description(row['source_method'],
+                                                      row['Indicator'],
+                                                      row['source_indicator'])
         ind.impact_factors = []
         ind.version = pkg_version_number
+        source = self._return_source(row['Method'])
+        if source:
+            ind.source = source.to_ref()
         self.__indicators[uid] = ind
 
         method = self.__method(row)
-        ref = o.ImpactCategory()
-        ref.id = uid
-        ref.name = ind.name
-        ref.ref_unit = ind.ref_unit
-        method.impact_categories.append(ref)
+        method.impact_categories.append(ind.to_ref())
         return ind
 
     def __method(self, row) -> o.ImpactMethod:
         uid = row['Method UUID']
         if not is_non_empty_str(uid):
             uid = make_uuid(row['Method'])
-        description = generate_method_description(row['Method'])
         m = self.__methods.get(uid)
         if m is not None:
             return m
@@ -102,7 +122,7 @@ class Writer(object):
         m.name = row['Method']
         m.version = pkg_version_number
         m.impact_categories = []
-        m.description = description
+        m.description = generate_method_description(row['Method'])
         self.__methods[uid] = m
         return m
 
@@ -137,3 +157,9 @@ class Writer(object):
 
         self.__flows[uid] = flow
         return flow
+
+    def _return_source(self, name):
+        for uid, s in self.__sources.items():
+            if s.name == name:
+                return s
+        return None
diff --git a/lciafmt/util.py b/lciafmt/util.py
index 4daef83..6946131 100644
--- a/lciafmt/util.py
+++ b/lciafmt/util.py
@@ -21,7 +21,7 @@ from fedelemflowlist.globals import flow_list_specs
 
 
 # set version number of package, needs to be updated with setup.py
-pkg_version_number = '1.1.1'
+pkg_version_number = '1.1.2'
 MODULEPATH = Path(__file__).resolve().parent
 datapath = MODULEPATH / 'data'
 
@@ -60,6 +60,7 @@ def set_lcia_method_meta(method_id):
     lcia_method_meta.tool_version = pkg_version_number
     lcia_method_meta.ext = write_format
     lcia_method_meta.git_hash = GIT_HASH
+    lcia_method_meta.date_created = pd.to_datetime('today').strftime('%Y-%m-%d %H:%M:%S')
     return lcia_method_meta
 
 
@@ -170,18 +171,22 @@ def check_as_class(method_id):
     return method_id
 
 
-def generate_method_description(name: str, indicator: str='') -> str:
+def generate_method_description(name: str,
+                                indicator: str='',
+                                source_indicator: str=''
+                                ) -> str:
     with open(datapath / "description.yaml") as f:
         generic = yaml.safe_load(f)
-    desc = generic['description']
+    desc = generic['base']
     method = check_as_class(name)
-    if method is None:
+    if type(method) is str:
         method_meta = {}
         method_meta['name'] = name
         method_meta['url'] = ''
         method_meta['citation'] = ''
     else:
         method_meta = method.get_metadata()
+        desc += generic['description']
     if 'detail_note' in method_meta:
         desc += method_meta['detail_note']
     if 'methods' in method_meta:
@@ -192,6 +197,8 @@ def generate_method_description(name: str, indicator: str='') -> str:
             log.debug(f'{name} not found in methods.json')
     if indicator:
         desc = generic['indicator']
+    if source_indicator:
+        desc = generic['source_indicator'] 
 
     # Replace tagged fields
     if 'version' in method_meta:
@@ -206,6 +213,7 @@ def generate_method_description(name: str, indicator: str='') -> str:
             .replace('[citation]', method_meta['citation'])
             .replace('[url]', method_meta['url'])
             .replace('[Indicator]', indicator)
+            .replace('[source_indicator]', source_indicator)
             )
 
     return desc
diff --git a/scripts/ISO21930-LCIA-US.py b/scripts/ISO21930-LCIA-US.py
new file mode 100644
index 0000000..7a95f21
--- /dev/null
+++ b/scripts/ISO21930-LCIA-US.py
@@ -0,0 +1,20 @@
+"""
+Generate method for ISO21930-LCIA-US
+"""
+
+import lciafmt
+import yaml
+
+m = 'ISO21930-LCIA-US.yaml'
+with open(lciafmt.util.datapath / m) as f:
+    file = yaml.safe_load(f)
+version = file.get('version')
+
+df = lciafmt.custom.generate_lcia_compilation(m)
+name = df['Method'][0]
+if version:
+    name = f'{name}{version}'
+
+lciafmt.util.store_method(df, method_id=None, name=name)
+lciafmt.util.save_json(method_id=None, mapped_data=df, name=name)
+df.to_excel(f'{lciafmt.util.OUTPUTPATH}/{name}.xlsx', index=False)
diff --git a/setup.py b/setup.py
index deda5ab..bd516b9 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
 
 setup(
     name="lciafmt",
-    version="1.1.1",
+    version="1.1.2",
     packages=["lciafmt"],
     package_dir={'lciafmt': 'lciafmt'},
     package_data={'lciafmt': ["data/*.*"]},


Test patch:
diff --git a/.github/workflows/test_methods.yml b/.github/workflows/test_methods.yml
index 92adc95..140a8ac 100644
--- a/.github/workflows/test_methods.yml
+++ b/.github/workflows/test_methods.yml
@@ -13,9 +13,9 @@ jobs:
     runs-on: macos-latest
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python
-      uses: actions/setup-python@v3
+      uses: actions/setup-python@v5
       with:
         python-version: "3.10"
 
@@ -36,7 +36,7 @@ jobs:
 
     - name: Upload files
       if: always()
-      uses: actions/upload-artifact@v3.1.1
+      uses: actions/upload-artifact@v4
       with:
         # Artifact name
         name: lciafmt_methods
diff --git a/tests/compilation.yaml b/tests/compilation.yaml
new file mode 100644
index 0000000..5f2465c
--- /dev/null
+++ b/tests/compilation.yaml
@@ -0,0 +1,22 @@
+name: Test Compilation Method
+rename_indicators: False
+indicators:
+    Climate change:
+      method: IPCC
+      indicator: AR6-100
+      code: GHG
+
+    Acidification potential:
+      method: TRACI 2.1
+      indicator: Acidification
+      code: AP
+
+    Particulate matter formation potential:
+      method: ReCiPe 2016 - Midpoint/H
+      indicator: Particulate matter formation
+      code: PMFP
+
+    Water consumption:
+      method: FEDEFL Inventory
+      indicator: freshwater_resources
+      code: WAT
diff --git a/tests/test_generate_methods.py b/tests/test_generate_methods.py
index f894a94..b25e065 100644
--- a/tests/test_generate_methods.py
+++ b/tests/test_generate_methods.py
@@ -2,7 +2,7 @@
 
 import pytest
 import lciafmt
-from lciafmt.util import store_method
+from lciafmt.util import store_method, MODULEPATH
 
 skip_list = ['ImpactWorld'] # requires pyodbc
 
@@ -31,5 +31,35 @@ def test_endpoint_method():
     store_method(method, method_id=None)
     assert method is not None
 
+
+def test_method_write_json():
+    # Test TRACI2.1 Acidification
+    method_id = lciafmt.Method.TRACI
+    method = lciafmt.get_mapped_method(method_id = method_id,
+                                       indicators=['Acidification'],
+                                       download_from_remote=True)
+    lciafmt.util.save_json(method_id = method_id,
+                           name = 'test_TRACI',
+                           mapped_data = method,
+                           write_flows=True)
+    # Test FEDEFL Inventory
+    method_id = lciafmt.Method.FEDEFL_INV
+    method = lciafmt.get_mapped_method(method_id = method_id,
+                                       download_from_remote=True)
+    lciafmt.util.save_json(method_id = method_id,
+                           mapped_data = method,
+                           name = 'test_FEDEFL',
+                           write_flows=True)
+
+def test_compilation_method():
+    df = lciafmt.generate_lcia_compilation('compilation.yaml',
+                                           filepath=MODULEPATH.parent / 'tests')
+    name = df['Method'][0]
+    lciafmt.util.store_method(df, method_id=None, name=name)
+    lciafmt.util.save_json(method_id=None, mapped_data=df, name=name)
+
+
 if __name__ == "__main__":
-    test_generate_methods()
+    # test_generate_methods()
+    # test_method_write_json()
+    test_compilation_method()