Repository: radis/radis (language: python)

Issue:
Read more Line Databases (ExoMol, Chianti, NIST, GEISA, etc.)
# Molecular databases

###  Exomol

See initial work in https://github.com/keflavich/Orion_ALMA_2016.1.00165.S/blob/master/analysis/parse_exomol_files.py

- See also: https://github.com/xnx/ExoMol_to_HITRAN
- and more generally the ExoMol tools http://exomol.com/software/
- Helios-K, the fastest ExoMol opacity calculator https://github.com/exoclime/HELIOS-K  
- [x] being done in #319 

### GEISA

- [x] Being done in #462 

### HITRAN

- [x] Read HITRAN in .par format 
- [ ] Read HITRAN with the Relational Database format. See [doi/10.1016/j.jqsrt.2013.04.027](https://www.sciencedirect.com/science/article/pii/S0022407313001751?via%3Dihub) 


# Atomic databases:

### NIST 

- [ ] use astroquery for that ?

### Chianti

There is the [ChiantiPy](https://github.com/chianti-atomic/ChiantiPy) package already. 

- [ ] Cross-reference them ? 




Discussion / hints:
autoclosing (old), reopen if needed

Fix patch:
diff --git a/examples/plot_compare_CO_geisa_hitran.py b/examples/plot_compare_CO_geisa_hitran.py
new file mode 100644
index 000000000..962379dac
--- /dev/null
+++ b/examples/plot_compare_CO_geisa_hitran.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+"""
+=========================================================
+Compare CO spectrum from the GEISA and HITRAN database
+=========================================================
+
+GEISA Database has been newly implemented in RADIS 0.13 release on May 15, 2022. This is among
+the very first attemps to compare the spectra generated from the two databases.
+
+Auto-download and calculate CO spectrum from the GEISA database, and the HITRAN database.
+
+Output should be similar, but not exactly! By default these two databases provide different
+broadening coefficients. However, the Einstein coefficients & linestrengths should be
+approximately the same, therefore the integrals under the lines should be similar.
+
+You can see it by running the code below.
+
+For your interest, GEISA and HITRAN lines can be downloaded and accessed separately using
+:py:func:`~radis.io.geisa.fetch_geisa` and :py:func:`~radis.io.hitran.fetch_hitran`
+
+"""
+import astropy.units as u
+
+from radis import calc_spectrum, plot_diff
+
+conditions = {
+    "wmin": 2002 / u.cm,
+    "wmax": 2300 / u.cm,
+    "molecule": "CO",
+    "pressure": 1.01325,  # bar
+    "Tgas": 1000,  # K
+    "mole_fraction": 0.1,
+    "path_length": 1,  # cm
+    "verbose": True,
+}
+
+s_geisa = calc_spectrum(**conditions, databank="geisa", name="GEISA's CO")
+
+s_hitran = calc_spectrum(
+    **conditions,
+    databank="hitran",
+    name="HITRAN's CO",
+)
+
+"""
+
+In :py:func:`~radis.io.geisa.fetch_geisa`, you can choose to additionally plot the
+absolute difference (method='diff') by default, or the ratio (method='ratio'), or both.
+
+"""
+
+plot_diff(s_geisa, s_hitran, method=["diff", "ratio"])
diff --git a/radis/io/cache_files.py b/radis/io/cache_files.py
index f721918fd..f745de919 100644
--- a/radis/io/cache_files.py
+++ b/radis/io/cache_files.py
@@ -631,7 +631,7 @@ def save_to_hdf(
         version = radis.__version__
     metadata.update({"version": version})
 
-    manager.add_metadata(fname, metadata)
+    manager.add_metadata(fname, metadata, key=key)
 
     if verbose >= 3:
         print("... saved {0} with metadata: {1}".format(fname, metadata))
diff --git a/radis/io/dbmanager.py b/radis/io/dbmanager.py
index c20be49f0..c61e12398 100644
--- a/radis/io/dbmanager.py
+++ b/radis/io/dbmanager.py
@@ -228,6 +228,8 @@ def get_filenames(self):
         if engine == "vaex":
             local_files = [fname.replace(".h5", ".hdf5") for fname in local_files]
 
+        local_files = [expanduser(f) for f in local_files]
+
         return local_files, urlnames
 
     def fetch_urlnames(self) -> list:
diff --git a/radis/io/geisa.py b/radis/io/geisa.py
new file mode 100644
index 000000000..023caf2fd
--- /dev/null
+++ b/radis/io/geisa.py
@@ -0,0 +1,618 @@
+# -*- coding: utf-8 -*-
+"""
+Summary
+-----------------------
+
+GEISA database parser
+
+-----------------------
+
+
+"""
+
+
+# import re
+import time
+from collections import OrderedDict
+from os.path import abspath, exists, expanduser, getmtime, join
+
+import numpy as np
+
+import radis
+from radis.io.cache_files import cache_file_name, load_h5_cache_file, save_to_hdf
+from radis.io.dbmanager import DatabaseManager
+from radis.io.tools import drop_object_format_columns, parse_hitran_file
+
+# from typing import Union
+
+
+# %% Parsing functions
+
+# General case : GEISA 2020
+# Provided by Thibault Delahaye
+
+# BE ADVISED #1: the columns "int" and "ierrB" should be in format FLOAT instead of str as below.
+# However, because of Fortran's double-precision floating-point values used in these 2 columns,
+# which numpy cannot typecast to float. Thus, we set them as string here and will typecast them
+# to float again later in geisa_utils.
+
+# BE ADVISED #2: the columns "idH" for HITRAN molecular number and "isoH" for HITRAN isotope number
+# have been changed to "id" and "iso" respectively. Meanwhile, original columns "id" and "iso" have
+# also been changed to "idG" and "isoG" to avoid duplication. This is to make sure the parsing process
+# and spectrum calculation using HAPI partition function can operate normally.
+
+# fmt: off
+columns_GEISA = OrderedDict(
+    [
+        # name  # format  # type  # description  # unit
+        ("wav", ("a12", float, "vacuum wavenumber", "cm-1")),
+        ("int", ("a11", str, "intensity at 296K", "cm-1/(molecule/cm-2)")),
+        ("airbrd", ("a6", float, "air-broadened half-width at 296K", "cm-1.atm-1")),
+        ("El", ("a10", float, "lower-state energy", "cm-1")),
+        ("globu", ("a25", str, "electronic and vibrational global upper quanta", "")),
+        ("globl", ("a25", str, "electronic and vibrational global lower quanta", "")),
+        ("locu", ("a15", str, "electronic and vibrational local upper quanta", "")),
+        ("locl", ("a15", str, "electronic and vibrational local lower quanta", "")),
+        ("Tdpair", ("a4", float, "temperature-dependance exponent for Gamma air", "")),
+        ("isoG", ("a3", int, "GEISA isotope number", "")),
+        ("mol", ("a3", int, "GEISA molecular number", "")),
+        ("idG", ("a3", str, "Internal GEISA code for the data identification", "")),
+        ("id", ("a2", int, "Hitran molecular number", "")),
+        ("iso", ("a1", int, "Hitran isotope number", "")),
+        ("A", ("a10", float, "Einstein A coefficient", "s-1")),
+        ("selbrd", ("a7", float, "self-broadened half-width at 296K", "cm-1.atm-1")),
+        ("Pshft", ("a9", float, "air pressure-induced line shift at 296K", "cm-1.atm-1")),
+        ("Tdppair", ("a6", float, "temperature-dependance exponent for air pressure-induced line shift", "")),
+        ("ierrA", ("a10", float, "estimated accuracy on the line position", "cm-1")),
+        ("ierrB", ("a11", str, "estimated accuracy on the intensity of the line", "cm-1/(molecule/cm-2)")),
+        ("ierrC", ("a6", float, "estimated accuracy on the air collision halfwidth", "cm-1.atm-1")),
+        ("ierrF", ("a4", float, "estimated accuracy on the temperature dependence coefficient of the air-broadening halfwidth", "")),
+        ("ierrO", ("a9", float, "estimated accuracy on the air pressure shift of the line transition at 296K", "cm-1.atm-1")),
+        ("ierrR", ("a6", float, "estimated accuracy on the temperature dependence coefficient of the air pressure shift", "")),
+        ("ierrN", ("a7", float, "estimated accuracy on the self-broadened at 296K", "cm-1.atm-1")),
+        ("Tdpself", ("a4", float, "temperature-dependance exponent for self-broadening halfwidth", "")),
+        ("ierrS", ("a4", float, "estimated accuracy on the temperature dependence coefficient of the self-broadening halfwidth", "")),
+        ("Pshfts", ("a8", float, "self pressure-induced line shift at 296K", "cm-1.atm-1")),
+        ("ierrT", ("a8", float, "estimated accuracy on the self-pressure shift of the line transition at 296K", "cm-1.atm-1")),
+        ("Tdpnself", ("a4", float, "temperature-dependance exponent for self pressure-induced line shift", "")),
+        ("ierrU", ("a4", float, "estimated accuracy on the temperature dependence coefficient of the self pressure shift", "")),
+    ]
+)
+""" OrderedDict: parsing order of GEISA2020 format """
+
+# The columns required for equilibrium calculations only
+equilibrium_columns = OrderedDict(
+    [
+        # name  # format  # type  # description  # unit
+        ("wav", ("a12", float, "vacuum wavenumber", "cm-1")),
+        ("int", ("a11", float, "intensity at 296K", "cm-1/(molecule/cm-2)")),
+        ("airbrd", ("a6", float, "air-broadened half-width at 296K", "cm-1.atm-1")),
+        ("iso", ("a3", int, "GEISA isotope number", "")),
+        ("mol", ("a3", int, "GEISA molecular number", "")),
+        ("id", ("a3", str, "Internal GEISA code for the data identification", "")),
+        ("selbrd", ("a7", float, "self-broadened half-width at 296K", "cm-1.atm-1")),
+        ("Pshft", ("a9", float, "air pressure-induced line shift at 296K", "cm-1.atm-1")),
+        ("Pshfts", ("a8", float, "self pressure-induced line shift at 296K", "cm-1.atm-1")),
+    ]
+)
+# fmt: on
+
+GEISA_MOLECULES_Nlines = {
+    "H2O": 362222,
+    "CO2": 532533,
+    "O3": 389378,
+    "N2O": 50633,
+    "CO": 14985,
+    "CH4": 240858,
+    "O2": 6428,
+    "NO": 105079,
+    "SO2": 68728,
+    "NO2": 113880,
+    "NH3": 29082,
+    "PH3": 34542,
+    "HNO3": 691161,
+    "OH": 42866,
+    "HF": 20010,
+    "HCL": 35985,
+    "HBR": 8980,
+    "HI": 4751,
+    "CLO": 7230,
+    "OCS": 33809,
+    "H2CO": 37050,
+    "C2H6": 28439,
+    "CH3D": 49237,
+    "C2H2": 11340,
+    "C2H4": 18378,
+    "GEH4": 32372,
+    "HCN": 81889,
+    "C3H8": 8983,
+    "C2N2": 2577,
+    "C4H2": 119480,
+    "HC3N": 179347,
+    "HOCL": 17862,
+    "N2": 120,
+    "CH3CL": 18344,
+    "H2O2": 126983,
+    "H2S": 20788,
+    "HCOOH": 62684,
+    "COF2": 70904,
+    "SF6": 92398,
+    "C3H4": 19001,
+    "HO2": 38804,
+    "CLONO2": 356899,
+    "CH3BR": 36911,
+    "CH3OH": 19897,
+    "NO+": 1206,
+    "HNC": 5619,
+    "C6H6": 9797,
+    "C2HD": 15512,
+    "CF4": 60033,
+    "CH3CN": 17172,
+    "HDO": 63641,
+    "SO3": 10881,
+    "HONO": 26041,
+    "COFCL": 215639,
+    "CH3I": 70291,
+    "CH3F": 1499,
+    "RUO4": 30205,
+    "H2C3H2": 31686,
+}
+
+GEISA_MOLECULES = list(GEISA_MOLECULES_Nlines.keys())
+
+
+def gei2df(
+    fname,
+    cache=True,
+    load_columns=None,
+    verbose=True,
+    drop_non_numeric=True,
+    load_wavenum_min=None,
+    load_wavenum_max=None,
+    engine="pytables",
+):
+    """Convert a GEISA [1]_ file to a Pandas dataframe.
+    Parameters
+    ----------
+    fname: str
+        GEISA file name.
+    cache: boolean, or 'regen'
+        if ``True``, a pandas-readable HDF5 file is generated on first access,
+        and later used. This saves on the datatype cast and conversion and
+        improves performances a lot (but changes in the database are not
+        taken into account). If ``False``, no database is used. If 'regen', temp
+        file are reconstructed. Default ``True``.
+    load_columns: list
+        columns to load. If ``None``, loads everything.
+        .. note::
+            this is only relevant when loading from a cache file. To generate
+            the cache file, all columns are loaded anyway.
+    Other Parameters
+    ----------------
+    drop_non_numeric: boolean
+        if ``True``, non numeric columns are dropped. This improves performances,
+        but make sure all the columns you need are converted to numeric formats
+        before hand. Default ``True``. Note that if a cache file is loaded it
+        will be left untouched.
+    load_wavenum_min, load_wavenum_max: float
+        if not ``'None'``, only load the cached file if it contains data for
+        wavenumbers above/below the specified value. See :py:func`~radis.io.cache_files.load_h5_cache_file`.
+        Default ``'None'``.
+    engine: 'pytables', 'vaex'
+        format for Hdf5 cache file, `pytables` by default.
+    Returns
+    -------
+    df: pandas Dataframe
+        dataframe containing all lines and parameters.
+    Notes
+    -----
+    GEISA Database 2020 release can be downloaded from [2]_
+    References
+    ----------
+    .. [1] `The 2020 edition of the GEISA spectroscopic database, Thibault Delahaye et al., 2021 <https://www.sciencedirect.com/science/article/abs/pii/S0022285221000928>`_
+    .. [2] `GEISA Database 2020 release <https://geisa.aeris-data.fr/interactive-access/?db=2020&info=ftp>`_
+    See Also
+    --------
+    :func:`~radis.io.hitran.hit2df`
+    :func:`~radis.io.cdsd.cdsd2df`
+    """
+
+    # Try to access last modification time of original file
+    metadata = {}
+    metadata["last_modification"] = time.ctime(getmtime(fname))
+    # Make sure the wavenums are legit
+    if load_wavenum_min and load_wavenum_max:
+        assert load_wavenum_min < load_wavenum_max
+    # Verbose
+    if verbose >= 2:
+        print("Opening file {0}, cache={1})".format(fname, cache))
+        print("Last Modification time: {0}".format(metadata["last_modification"]))
+
+    # Attempt to use cache file
+    fcache = cache_file_name(fname, engine=engine)
+    if cache and exists(fcache):
+        relevant_if_metadata_above = (
+            {"wavenum_max": load_wavenum_min} if load_wavenum_min else {}
+        )  # not relevant if wavenum_max of file is < wavenum min required
+        relevant_if_metadata_below = (
+            {"wavenum_min": load_wavenum_max} if load_wavenum_max else {}
+        )  # not relevant if wavenum_min of file is > wavenum max required
+        df = load_h5_cache_file(
+            fcache,
+            cache,
+            columns=load_columns,
+            valid_if_metadata_is=metadata,
+            relevant_if_metadata_above=relevant_if_metadata_above,
+            relevant_if_metadata_below=relevant_if_metadata_below,
+            current_version=radis.__version__,
+            last_compatible_version=radis.config["OLDEST_COMPATIBLE_VERSION"],
+            verbose=verbose,
+            engine=engine,
+        )
+        if df is not None:
+            return df
+
+    # If cache files are not found, commence reading of full file
+    df = parse_hitran_file(fname, columns_GEISA)
+
+    # Commence "D to E" conversion on 2nd and 20th columns, by using
+    # df.columns.str.replace() which is much faster than Python loop.
+    # Finally, typecast them to float.
+    df["int"] = df["int"].astype(str).str.replace("D", "E").astype(float)
+    df["ierrB"] = df["ierrB"].astype(str).str.replace("D", "E").astype(float)
+
+    # Remove non numerical attributes
+    if drop_non_numeric:
+        # The function replace_PQR_with_m101 below will be developed
+        # in later updates for GEISA's non-equilibrium calculations
+        # replace_PQR_with_m101(df)
+
+        # Basically ripping out 4 quanta columns
+        df = drop_object_format_columns(df, verbose=verbose)
+
+    # Generate cache file for later use
+    if cache:
+        new_metadata = {
+            # Last modification time of the original file :
+            "last_modification": time.ctime(getmtime(fname)),
+            "wavenum_min": df.wav.min(),
+            "wavenum_max": df.wav.max(),
+        }
+        if verbose:
+            print(
+                "Generating cache file {0} with metadata :\n{1}".format(
+                    fcache, new_metadata
+                )
+            )
+        try:
+            save_to_hdf(
+                df,
+                fcache,
+                metadata=new_metadata,
+                version=radis.__version__,
+                key="df",
+                overwrite=True,
+                verbose=verbose,
+                engine=engine,
+            )
+        except PermissionError:
+            if verbose:
+                print(
+                    "An error occured in cache file generation. Lookup access rights."
+                )
+            pass
+
+    return df
+
+
+#%%
+def get_last(b):
+    """Get non-empty lines of a chunk b, parsing the bytes."""
+    element_length = np.vectorize(lambda x: len(x.__str__()))(b)
+    non_zero = element_length > element_length[-1]
+    threshold = non_zero.argmin() - 1
+    assert (non_zero[: threshold + 1] == 1).all()
+    assert (non_zero[threshold + 1 :] == 0).all()
+    return b[non_zero]
+
+
+class GEISADatabaseManager(DatabaseManager):
+    def __init__(
+        self,
+        name,
+        molecule,
+        local_databases,
+        engine="default",
+        verbose=True,
+        chunksize=100000,
+        parallel=True,
+    ):
+        super().__init__(
+            name,
+            molecule,
+            local_databases,
+            engine,
+            verbose=verbose,
+            parallel=parallel,
+        )
+        self.chunksize = chunksize
+        self.downloadable = True
+        self.base_url = None
+        self.Nlines = None
+        self.wmin = None
+        self.wmax = None
+        self.urlnames = None
+
+    def fetch_urlnames(self):
+        """requires connexion"""
+
+        if self.urlnames is not None:
+            return self.urlnames
+
+        molecule = self.molecule
+        geisa_url = "https://aeris-geisa.ipsl.fr/geisa_files/2020/Lines/line_GEISA2020_asc_gs08_v1.0_"
+
+        print(f"Molecule: {molecule}")
+
+        if molecule.upper() in GEISA_MOLECULES:
+            url = geisa_url + molecule.lower()
+            urlnames = [url]
+        else:
+            raise KeyError(
+                f"Please choose one of GEISA molecules : {GEISA_MOLECULES}. Got '{molecule}'"
+            )
+
+        self.urlnames = urlnames
+
+        return urlnames
+
+    def parse_to_local_file(
+        self,
+        opener,
+        urlname,
+        local_file,
+        pbar_active=True,
+        pbar_t0=0,
+        pbar_Ntot_estimate_factor=None,
+        pbar_Nlines_already=0,
+        pbar_last=True,
+    ):
+        """Uncompress ``urlname`` into ``local_file``.
+        Also add metadata
+
+        Parameters
+        ----------
+        opener: an opener with an .open() command
+        gfile : file handler. Filename: for info"""
+
+        # molecule = self.molecule
+        # Ntotal_lines_expected = GEISA_MOLECULES_Nlines[molecule.upper()]
+
+        writer = self.get_hdf5_manager()
+
+        with opener.open(urlname) as gfile:  # locally downloaded file
+
+            gfile  #  so the linter doesn't annoy us. We're not using this file anyway, just unzipping the cache file directly :
+            df = gei2df(opener.abspath(urlname), drop_non_numeric=False, cache=False)
+
+            writer.write(local_file, df, append=False)
+
+            self.wmin = df.wav.min()
+            self.wmax = df.wav.max()
+
+            Nlines = len(df)
+
+        writer.combine_temp_batch_files(local_file)  # used for vaex mode only
+
+        # Check number of lines is consistent
+        # assert Nlines == Ntotal_lines_expected
+
+        # Add metadata
+        from radis import __version__
+
+        writer.add_metadata(
+            local_file,
+            {
+                "wavenumber_min": self.wmin,
+                "wavenumber_max": self.wmax,
+                "download_date": self.get_today(),
+                "download_url": urlname,
+                "total_lines": Nlines,
+                "version": __version__,
+            },
+        )
+
+        return Nlines
+
+    def register(self):
+        """register in ~/radis.json"""
+
+        local_files, urlnames = self.get_filenames()
+        info = f"GEISA {self.molecule} lines ({self.wmin:.1f}-{self.wmax:.1f} cm-1)"
+
+        dict_entries = {
+            "info": info,
+            "path": local_files,
+            "format": "geisa-radisdb",
+            "parfuncfmt": "hapi",
+            "wavenumber_min": self.wmin,
+            "wavenumber_max": self.wmax,
+            "download_date": self.get_today(),
+            "download_url": urlnames,
+        }
+
+        super().register(dict_entries)
+
+
+def fetch_geisa(
+    molecule,
+    local_databases=None,
+    databank_name="GEISA-{molecule}",
+    isotope=None,
+    load_wavenum_min=None,
+    load_wavenum_max=None,
+    columns=None,
+    cache=True,
+    verbose=True,
+    chunksize=100000,
+    clean_cache_files=True,
+    return_local_path=False,
+    engine="default",
+    output="pandas",
+    parallel=True,
+):
+    """Stream GEISA file from GEISA website. Unzip and build a HDF5 file directly.
+
+    Returns a Pandas DataFrame containing all lines.
+
+    Parameters
+    ----------
+    molecule: all 58 GEISA 2020 molecules. See here https://geisa.aeris-data.fr/interactive-access/?db=2020&info=ftp
+    local_databases: str
+        where to create the RADIS HDF5 files. Default ``"~/.radisdb/geisa"``.
+        Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in ~/radis.json config file
+    databank_name: str
+        name of the databank in RADIS :ref:`Configuration file <label_lbl_config_file>`
+        Default ``"GEISA-{molecule}"``
+    isotope: str, int or None
+        load only certain isotopes : ``'2'``, ``'1,2'``, etc. If ``None``, loads
+        everything. Default ``None``.
+    load_wavenum_min, load_wavenum_max: float (cm-1)
+        load only specific wavenumbers.
+    columns: list of str
+        list of columns to load. If ``None``, returns all columns in the file.
+
+    Other Parameters
+    ----------------
+    cache: ``True``, ``False``, ``'regen'`` or ``'force'``
+        if ``True``, use existing HDF5 file. If ``False`` or ``'regen'``, rebuild it.
+        If ``'force'``, raise an error if cache file cannot be used (useful for debugging).
+        Default ``True``.
+    verbose: bool
+    chunksize: int
+        number of lines to process at a same time. Higher is usually faster
+        but can create Memory problems and keep the user uninformed of the progress.
+    clean_cache_files: bool
+        if ``True`` clean downloaded cache files after HDF5 are created.
+    return_local_path: bool
+        if ``True``, also returns the path of the local database file.
+    engine: 'pytables', 'vaex', 'default'
+        which HDF5 library to use to parse local files. If 'default' use the value from ~/radis.json
+    output: 'pandas', 'vaex', 'jax'
+        format of the output DataFrame. If ``'jax'``, returns a dictionary of
+        jax arrays.
+    parallel: bool
+        if ``True``, uses joblib.parallel to load database with multiple processes
+
+    Returns
+    -------
+    df: pd.DataFrame
+        Line list
+        A HDF5 file is also created in ``local_databases`` and referenced
+        in the :ref:`RADIS config file <label_lbl_config_file>` with name
+        ``databank_name``
+    local_path: str
+        path of local database file if ``return_local_path``
+
+    Examples
+    --------
+    ::
+
+        from radis import fetch_geisa
+        df = fetch_geisa("CO")
+        print(df.columns)
+        >>> Index(['wav', 'int', 'airbrd', 'El', 'globu', 'globl', 'locu', 'locl',
+            'Tdpgair', 'isoG', 'mol', 'idG', 'id', 'iso', 'A', 'selbrd', 'Pshft',
+            'Tdpair', 'ierrA', 'ierrB', 'ierrC', 'ierrF', 'ierrO', 'ierrR', 'ierrN',
+            'Tdpgself', 'ierrS', 'Pshfts', 'ierrT', 'Tdppself', 'ierrU'],
+            dtype='object')
+
+    .. minigallery:: radis.fetch_hitemp
+
+    Notes
+    -----
+    if using ``load_only_wavenum_above/below`` or ``isotope``, the whole
+    database is anyway downloaded and uncompressed to ``local_databases``
+    fast access .HDF5 files (which will take a long time on first call). Only
+    the expected wavenumber range & isotopes are returned. The .HFD5 parsing uses
+    :py:func:`~radis.io.hdf5.hdf2df`
+
+    See Also
+    --------
+    :py:func:`~radis.io.hitran.fetch_hitran`, :py:func:`~radis.io.exomol.fetch_exomol`
+    :py:func:`~radis.io.hitemp.fetch_hitemp`, :py:func:`~radis.io.hdf5.hdf2df`
+    :py:meth:`~radis.lbl.loader.DatabankLoader.fetch_databank`
+
+    """
+
+    if r"{molecule}" in databank_name:
+        databank_name = databank_name.format(**{"molecule": molecule})
+
+    if local_databases is None:
+        import radis
+
+        local_databases = join(radis.config["DEFAULT_DOWNLOAD_PATH"], "geisa")
+    local_databases = abspath(expanduser(local_databases))
+
+    ldb = GEISADatabaseManager(
+        databank_name,
+        molecule=molecule,
+        local_databases=local_databases,
+        verbose=verbose,
+        chunksize=chunksize,
+        parallel=parallel,
+        engine=engine,
+    )
+
+    # Get list of all expected local files for this database:
+    local_files, urlnames = ldb.get_filenames()
+
+    # Delete files if needed:
+
+    if cache == "regen":
+        ldb.remove_local_files(local_files)
+    ldb.check_deprecated_files(
+        ldb.get_existing_files(local_files),
+        auto_remove=True if cache != "force" else False,
+    )
+
+    # Get missing files
+    download_files = ldb.get_missing_files(local_files)
+
+    # Download files
+    if len(download_files) > 0:
+        if urlnames is None:
+            urlnames = ldb.fetch_urlnames()
+        filesmap = dict(zip(local_files, urlnames))
+        download_urls = [filesmap[k] for k in download_files]
+        ldb.download_and_parse(download_urls, download_files)
+
+    # Register
+    if not ldb.is_registered():
+        ldb.register()
+
+    if len(download_files) > 0 and clean_cache_files:
+        ldb.clean_download_files()
+
+    if isotope and type(isotope) == int:
+        isotope = str(isotope)
+
+    df = ldb.load(
+        local_files,  # filter other files,
+        columns=columns,
+        isotope=isotope,
+        load_wavenum_min=load_wavenum_min,  # for relevant files, get only the right range
+        load_wavenum_max=load_wavenum_max,
+        output=output,
+    )
+
+    return (df, local_files) if return_local_path else df
+
+
+#%%
+
+if __name__ == "__main__":
+
+    import pytest
+
+    print("Testing factory:", pytest.main(["../test/io/test_geisa.py"]))
diff --git a/radis/io/hdf5.py b/radis/io/hdf5.py
index 00fb73c31..7824d0129 100644
--- a/radis/io/hdf5.py
+++ b/radis/io/hdf5.py
@@ -178,6 +178,10 @@ def combine_temp_batch_files(self, file, key="default", sort_values=None):
         file = expanduser(file)
         if self.engine == "vaex":
             if len(self._temp_batch_files) == 0:
+                # No temp file created. File is probably already created (append=False mode)
+                # Else, something unexpected happens --> raise error
+                if exists(file):
+                    return
                 raise ValueError(f"No batch temp files were written for {file}")
             if key == "default":
                 key = r"/table"
@@ -455,9 +459,16 @@ def read_metadata(self, fname: str, key="default") -> dict:
                     else:
                         try:
                             metadata = dict(hf[key].attrs)
-                        except KeyError as err:
-                            print(f"Error reading metadata from {fname}")
-                            raise err
+                        except (KeyError, OSError) as err:
+                            if key == r"/table":
+                                # backward compat : some old files were generated with key=None
+                                metadata = dict(hf.attrs)
+                                print(
+                                    f"Error reading metadata from {fname}. Regenerate file one day?"
+                                )
+                            else:
+                                print(f"Error reading metadata from {fname}")
+                                raise err
 
         else:
             raise NotImplementedError(
diff --git a/radis/io/hitemp.py b/radis/io/hitemp.py
index ef2f0c686..c6dddc379 100644
--- a/radis/io/hitemp.py
+++ b/radis/io/hitemp.py
@@ -85,6 +85,8 @@ def keep_only_relevant(
             files_wmin = min(float(fname_wmin), files_wmin)
             files_wmax = max(float(fname_wmax), files_wmax)
 
+    print(f"HITEMP keep only relevant input files: {relevantfiles}")
+
     return relevantfiles, files_wmin, files_wmax
 
 
@@ -261,6 +263,7 @@ def get_linereturn_format(self, opener, urlname, columns):
                     f"End of file while parsing file {opener.abspath(urlname)}. May be due to download error. Delete file ?"
                 ) from err
             linereturnformat = _get_linereturnformat(b, columns)
+
         return linereturnformat
 
     def parse_to_local_file(
@@ -518,7 +521,8 @@ def fetch_hitemp(
     See Also
     --------
     :py:func:`~radis.io.hitran.fetch_hitran`, :py:func:`~radis.io.exomol.fetch_exomol`
-    :py:func:`~radis.io.hdf5.hdf2df`, :py:meth:`~radis.lbl.loader.DatabankLoader.fetch_databank`
+    :py:func:`~radis.io.geisa.fetch_geisa`, :py:func:`~radis.io.hdf5.hdf2df`
+    :py:meth:`~radis.lbl.loader.DatabankLoader.fetch_databank`
 
     """
 
diff --git a/radis/io/tools.py b/radis/io/tools.py
index 1d2567316..92c7d3a11 100644
--- a/radis/io/tools.py
+++ b/radis/io/tools.py
@@ -16,9 +16,9 @@ def parse_hitran_file(fname, columns, count=-1):
     Parameters
     ----------
     fname: str
-        filename
+        filename.
     columns: dict
-        list of columns and their format
+        list of columns and their format.
 
     Other Parameters
     ----------------
@@ -28,7 +28,7 @@ def parse_hitran_file(fname, columns, count=-1):
     Returns
     -------
     df: pandas DataFrame
-        dataframe with lines
+        dataframe with lines.
 
     See Also
     --------
@@ -176,8 +176,8 @@ def _cast_to_dtype(data, dtype):
     data: array to cast
     dtype: (ordered) list of (param, type)
     """
-    dt = _format_dtype(dtype)
 
+    dt = _format_dtype(dtype)
     try:
         data = np.array(data, dtype=dt)
     except ValueError as err:
@@ -235,7 +235,8 @@ def replace_PQR_with_m101(df):
     ----------
 
     df: pandas Dataframe
-        ``branch`` must be a column name
+        ``branch`` must be a column name.
+
 
     Returns
     -------
diff --git a/radis/lbl/calc.py b/radis/lbl/calc.py
index e5f38a980..ee81ba170 100644
--- a/radis/lbl/calc.py
+++ b/radis/lbl/calc.py
@@ -20,6 +20,8 @@
 from copy import deepcopy
 from os.path import exists
 
+# from radis.db.classes import M
+
 try:  # Proper import
     from .base import get_waverange
     from .factory import SpectrumFactory
@@ -658,6 +660,7 @@ def _is_at_equilibrium():
             "hitran",
             "hitemp",
             "exomol",
+            "geisa",
         ]
         or (isinstance(databank, tuple) and databank[0] == "exomol")
         or (isinstance(databank, tuple) and databank[0] == "hitran")
@@ -686,6 +689,12 @@ def _is_at_equilibrium():
                 "source": "exomol",
                 "parfuncfmt": "exomol",  # download & use Exo partition functions for equilibrium}
             }
+        elif databank in ["geisa"]:
+            conditions = {
+                "source": "geisa",
+                "parfuncfmt": "hapi",
+                # TODO: replace with GEISA partition function someday.............
+            }
         elif isinstance(databank, tuple) and databank[0] == "exomol":
             conditions = {
                 "source": "exomol",
@@ -846,3 +855,5 @@ def _is_at_equilibrium():
     from radis.test.lbl.test_calc import _run_testcases
 
     print(_run_testcases(verbose=True))
+
+# %%
diff --git a/radis/lbl/loader.py b/radis/lbl/loader.py
index a7049d9b2..b59a3071b 100644
--- a/radis/lbl/loader.py
+++ b/radis/lbl/loader.py
@@ -51,7 +51,7 @@
 
 import warnings
 from copy import deepcopy
-from os.path import exists, join
+from os.path import exists, expanduser, join
 from time import time
 from uuid import uuid1
 
@@ -66,6 +66,9 @@
 from radis.io.cache_files import cache_file_name
 from radis.io.cdsd import cdsd2df
 from radis.io.exomol import fetch_exomol
+from radis.io.geisa import fetch_geisa
+
+# from radis.io.geisa import gei2df
 from radis.io.hdf5 import hdf2df
 from radis.io.hitemp import fetch_hitemp
 from radis.io.hitran import (
@@ -108,15 +111,17 @@
     "cdsd-4000",
     "hitemp-radisdb",
     "hdf5-radisdb",
+    "geisa",
 ]
 """list: Known formats for Line Databases:
 
-- ``'hitran'`` : [HITRAN-2020]_ original .par format
-- ``'hitemp'`` : [HITEMP-2010]_ original format (same format as 'hitran')
-- ``'cdsd-hitemp'`` : CDSD-HITEMP original format (CO2 only, same lines as HITEMP-2010)
-- ``'cdsd-4000'`` : [CDSD-4000]_ original format (CO2 only)
+- ``'hitran'`` : [HITRAN-2020]_ original .par format.
+- ``'hitemp'`` : [HITEMP-2010]_ original format (same format as 'hitran').
+- ``'cdsd-hitemp'`` : CDSD-HITEMP original format (CO2 only, same lines as HITEMP-2010).
+- ``'cdsd-4000'`` : [CDSD-4000]_ original format (CO2 only).
 - ``'hitemp-radisdb'`` : HITEMP under RADISDB format (pytables-HDF5 with RADIS column names).
 - ``'hdf5-radisdb'`` : arbitrary HDF5 file with RADIS column names.
+- ``'geisa'`` : [GEISA-2020]_ original .par format.
 
 To install all databases manually see the :ref:`Configuration file <label_lbl_config_file>`
 and the :ref:`list of databases <label_line_databases>` .
@@ -163,6 +168,7 @@
     "cdsd-hitemp": ["wang2", "lsrc"],
     "hdf5-radisdb": [],
     "hitemp-radisdb": [],
+    "geisa": [],
 }
 """ dict: drop these columns if using ``drop_columns='auto'`` in load_databank
 Based on the value of ``dbformat=``, some of these columns won't be used.
@@ -172,6 +178,7 @@
 - 'hitran': (HITRAN / HITEMP) :data:`~radis.io.hitran.columns_2004`,
 - 'cdsd-hitemp' (CDSD HITEMP): :data:`~radis.io.cdsd.columns_hitemp`,
 - 'cdsd-4000': (CDSD 4000) :data:`~radis.io.cdsd.columns_4000`,
+- 'geisa': (GEISA 2020) :data:`~radis.io.geisa.columns_GEISA`,
 """
 drop_auto_columns_for_levelsfmt = {
     "radis": [],
@@ -1044,7 +1051,7 @@ def fetch_databank(
                 )
             )
             source = "hitran"
-        if source not in ["hitran", "hitemp", "exomol"]:
+        if source not in ["hitran", "hitemp", "exomol", "geisa"]:
             raise NotImplementedError("source: {0}".format(source))
         if source == "hitran":
             dbformat = "hitran"
@@ -1060,6 +1067,10 @@ def fetch_databank(
             dbformat = (
                 "exomol-radisdb"  # downloaded in RADIS local databases ~/.radisdb
             )
+        elif source == "geisa":
+            dbformat = "geisa"
+            if database == "default":
+                database = "full"
 
         local_databases = config["DEFAULT_DOWNLOAD_PATH"]
 
@@ -1342,6 +1353,56 @@ def fetch_databank(
                 df = frames[0]
                 self.params.dbpath = local_paths[0]
 
+        elif source == "geisa":
+
+            # self.reftracker.add(doi["GEISA-2020"], "line database")
+            # TODO: add GEISA-2020 DOI
+
+            if memory_mapping_engine == "auto":
+                # temp fix for vaex not building on RTD
+                # see https://github.com/radis/radis/issues/404
+                if any("READTHEDOCS" in name for name in environ):
+                    memory_mapping_engine = "pytables"
+                    if self.verbose >= 3:
+                        print(
+                            f"ReadTheDocs environment detected. Memory-mapping-engine set to '{memory_mapping_engine}'. See https://github.com/radis/radis/issues/404"
+                        )
+                else:
+                    memory_mapping_engine = "vaex"
+
+            if database != "full":
+                raise ValueError(
+                    f"Got `database={database}`. When fetching GEISA, only the `database='full'` option is available."
+                )
+
+            # Download, setup local databases, and fetch (use existing if possible)
+
+            if isotope == "all":
+                isotope_list = None
+            else:
+                isotope_list = ",".join([str(k) for k in self._get_isotope_list()])
+
+            df, local_paths = fetch_geisa(
+                molecule,
+                isotope=isotope_list,
+                local_databases=join(local_databases, "geisa"),
+                load_wavenum_min=wavenum_min,
+                load_wavenum_max=wavenum_max,
+                columns=columns,
+                cache=db_use_cached,
+                verbose=self.verbose,
+                return_local_path=True,
+                engine=memory_mapping_engine,
+                parallel=parallel,
+            )
+            self.params.dbpath = ",".join(local_paths)
+
+            # ... explicitely write all isotopes based on isotopes found in the database
+            if isotope == "all":
+                self.input.isotope = ",".join(
+                    [str(k) for k in self._get_isotope_list(df=df)]
+                )
+
         else:
             raise NotImplementedError("source: {0}".format(source))
 
@@ -1705,7 +1766,7 @@ def _check_database_params(
             path = [path]
 
         # ... Parse all paths and read wildcards
-        path_list = path
+        path_list = [expanduser(p) for p in path]
         new_paths = []
         for pathrg in path_list:
             path = get_files_from_regex(pathrg)
diff --git a/radis/levels/partfunc.py b/radis/levels/partfunc.py
index b73cfd90c..73f270704 100644
--- a/radis/levels/partfunc.py
+++ b/radis/levels/partfunc.py
@@ -1224,6 +1224,25 @@ def _at(self, T):
                     + f"Max range : {str(err)}"
                     + tip
                 )
+            elif isinstance(err, KeyError):
+                molecule = self.molecule
+                # In current version, GEISA parser uses HAPI partition function, which does not
+                # support some of GEISA's isotopes, as listed in GEISA_nw_iso below:
+                GEISA_ns_iso = {
+                    "CO2": [0],
+                    "H2O": [8, 9],
+                    "N2O": [0],
+                    "NO2": [2],
+                }
+                raise KeyError(
+                    "KeyError spotted! "
+                    + f"If you are computing GEISA spectra, this result might be because of an "
+                    + f"unsupported isotope. Currently isotope ID {GEISA_ns_iso[molecule]} "
+                    + f"of molecule {molecule} is not supported by HAPI partitional function, "
+                    + "thus stopping this spectrum calculation. Please select other isotopes "
+                    + "lists such as isotope='1,2,3'."
+                ) from err
+
             else:
                 raise
 


Test patch:
diff --git a/radis/test/files/geisa_CO2_fragment.par b/radis/test/files/geisa_CO2_fragment.par
new file mode 100644
index 000000000..d62e6856e
--- /dev/null
+++ b/radis/test/files/geisa_CO2_fragment.par
@@ -0,0 +1,7 @@
+  396.788608 5.7530D-290.0689 3124.7412                 2 1 1 03                 1 2 2 01                         P 30E0.73626  2P19 21 1.553E-04 0.0907-0.000716-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  397.115257 1.2410D-300.0714 3623.3899                 2 2 2 03                 1 3 3 01                         R 21E0.69626  2P19 21 4.966E-05 0.0979-0.000646-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  397.184001 5.2680D-290.0687 3149.0139                 2 1 1 03                 1 2 2 01                         P 31F0.73626  2P19 21 1.550E-04 0.0897-0.000714-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  397.727316 4.0280D-290.0748 2903.3738                 2 1 1 03                 2 0 0 01                         R 16E0.68626  2P19 21 6.275E-05 0.1019-0.000580-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  397.894183 1.2100D-300.0709 3640.6416                 2 2 2 03                 1 3 3 01                         R 22F0.70626  2P19 21 5.058E-05 0.0971-0.000662-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  398.415615 6.7860D-290.0692 3078.5679                 2 1 1 03                 1 2 2 01                         P 28E0.72626  2P19 21 1.581E-04 0.0926-0.000707-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+  398.662365 6.2720D-290.0690 3101.2744                 2 1 1 03                 1 2 2 01                         P 29F0.72626  2P19 21 1.576E-04 0.0917-0.000714-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
diff --git a/radis/test/files/geisa_CO_fragment.par b/radis/test/files/geisa_CO_fragment.par
new file mode 100644
index 000000000..7a06f234f
--- /dev/null
+++ b/radis/test/files/geisa_CO_fragment.par
@@ -0,0 +1,8 @@
+    3.705026 2.3540D-440.0797 8414.4697                        4                        4                          R  00.76 26  5L19 51 2.868E-10 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.714217 2.2740D-320.0797 2116.2949                        1                        1                          R  00.76 27  5L19 54 3.871E-08 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.740024 1.1100D-380.0797 6350.4390                        3                        3                          R  00.76 26  5L19 51 5.999E-09 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.747902 1.1440D-270.0797    0.0000                        0                        0                          R  00.76 27  5L19 54 6.695E-08 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.775024 9.2330D-340.0797 4260.0620                        2                        2                          R  00.76 26  5L19 51 1.947E-08 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.810028 5.7060D-290.0797 2143.2710                        1                        1                          R  00.76 26  5L19 51 4.130E-08 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.845033 3.3000D-240.0797    0.0000                        0                        0                          R  00.76 26  5L19 51 7.207E-08 0.0860-0.000265-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+
diff --git a/radis/test/files/geisa_H2O_fragment.par b/radis/test/files/geisa_H2O_fragment.par
new file mode 100644
index 000000000..e4cf1911f
--- /dev/null
+++ b/radis/test/files/geisa_H2O_fragment.par
@@ -0,0 +1,7 @@
+    3.193958 2.1461D-340.0800  399.7992                    0 0 0                    0 0 0  5  5  1        6  4  2      0.65282  1K19 18-9.999E-01 0.4000 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.210950 1.5800D-270.0774 2126.4077                    0 1 0                    0 1 0  4  4  0        5  3  3      0.67161  1N10 11-9.999E-01 0.3913 0.001500-.9999 -0.999999-9.9999D-010.01550.34 0.000000-.9999 0.1956-.99-.990.0000000.000000-.99-.99
+    3.393282 1.0410D-330.0879 3488.3013                    0 2 0                    0 2 0  3  3  1        4  2  2      0.69171  1TE3 13 8.594E-07 0.4122 0.003000-.9999 -0.999999-9.9999D-010.01760.34 0.000000-.9999 0.2061-.99-.990.0000000.000000-.99-.99
+    3.412985 2.4320D-320.0368 3074.3198                   -2-2-2                   -2-2-2 14 -2 -1       15 -1 -2      0.37171  1TE3 13 2.172E-07 0.2214 0.000000-.9999 -0.999999-9.9999D-010.01840.19 0.000000-.9999 0.1107-.99-.990.0000000.000000-.99-.99
+    3.435397 9.5513D-330.0800 1114.8724                    0 0 0                    0 0 0  9  8  2       10  7  3      0.65262  1K19 17-9.999E-01 0.4000 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.437949 4.7827D-330.0800 1114.8698                    0 0 0                    0 0 0  9  8  1       10  7  4      0.65262  1K19 17-9.999E-01 0.4000 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    3.446362 1.1340D-290.0801 2117.0083                    0 1 0                    0 1 0  5  3  2        4  4  1      0.69181  1TE3 12 4.796E-07 0.4122-0.000300-.9999 -0.999999-9.9999D-010.01600.34 0.000000-.9999 0.2061-.99-.990.0000000.000000-.99-.99
diff --git a/radis/test/files/geisa_O2_fragment.par b/radis/test/files/geisa_O2_fragment.par
new file mode 100644
index 000000000..7a2e45214
--- /dev/null
+++ b/radis/test/files/geisa_O2_fragment.par
@@ -0,0 +1,7 @@
+    0.000003 0.0000D+000.0346 3126.4678                       X0                       X0           46.5 Q 47Q 47 47.5d0.72 67  7SA3 73 1.687E-29 0.0340 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 0.0000D+000.0346 3258.4031                       X0                       X0           47.5 Q 48Q 48 48.5d0.72 67  7SA3 73 1.582E-29 0.0340 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 0.0000D+000.0360 1295.5164                       X0                       X0           28.5 Q 30Q 30 27.5d0.71 67  7G03 73-9.999E-01 0.0353 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 1.2240D-400.0356  664.2595                       X0                       X0                Q 21Q 21     g0.72 66  7SA3 71 7.246E-30 0.0390 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 1.2510D-400.0420  664.2605                       X0                       X0                Q 21Q 21     d0.71 66  7G03 71-9.999E-01 0.0412 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 2.0900D-410.0352  933.5309                       X0                       X0                Q 25Q 25     g0.72 66  7SA3 71 3.862E-30 0.0370 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
+    0.000003 2.1730D-410.0390  933.5327                       X0                       X0                Q 25Q 25     d0.71 66  7G03 71-9.999E-01 0.0383 0.000000-.9999 -0.999999-9.9999D-01-.9999-.99 0.000000-.9999-9.9999-.99-.990.0000000.000000-.99-.99
\ No newline at end of file
diff --git a/radis/test/io/test_geisa.py b/radis/test/io/test_geisa.py
new file mode 100644
index 000000000..b119e6016
--- /dev/null
+++ b/radis/test/io/test_geisa.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+"""Test cases for GEISA parser. Currently able to test parsing of local files.
+Notes
+-----
+Runs tests for radis/io so that they can be accessed by pytest (and hopefully
+the CI test suite).
+Examples
+--------
+Run all tests::
+        pytest       (in command line, in project folder)
+Run only tests for local files (tests that have a 'local' label)::
+        pytest -m local
+-------------------------------------------------------------------------------
+"""
+
+# import os
+# from os.path import getmtime
+# from warnings import warn
+
+# import numpy as np
+import pytest
+
+from radis.io.geisa import gei2df
+
+# from radis.misc.warning import IrrelevantFileWarning
+from radis.test.utils import getTestFile
+
+files = {
+    "CO": "geisa_CO_fragment.par",
+    "H2O": "geisa_H2O_fragment.par",
+    "CO2": "geisa_CO2_fragment.par",
+}
+
+
+@pytest.mark.local
+def test_local_geisa_co(verbose=True, warnings=True, **kwargs):
+
+    # 1. Load local file
+    fileName = files["CO"]
+    df = gei2df(getTestFile(fileName), cache="regen")
+    if verbose:
+        print("Read {0}".format(fileName))
+        print("-------------------------------------")
+        print(df)
+
+    # 2. Check if the parser works correctly
+    # Check for accurate parsing of parameters for equilibrium calculations
+    assert list(
+        df.loc[3, ["wav", "int", "airbrd", "selbrd", "Pshft", "isoG", "iso", "mol"]]
+    ) == [
+        3.747902,
+        1.1440e-27,
+        0.0797,
+        0.086,
+        -0.000265,
+        27,
+        4,
+        5,
+    ]
+
+    print("GEISA parsing process of molecule CO works normally.\n")
+
+    return True
+
+
+@pytest.mark.local
+def test_local_geisa_h2o(verbose=True, warnings=True, **kwargs):
+
+    # 1. Load local file
+    fileName = files["H2O"]
+    df = gei2df(getTestFile(fileName), cache="regen")
+    if verbose:
+        print("Read {0}".format(fileName))
+        print("-------------------------------------")
+        print(df)
+
+    # 2. Check if the parser works correctly
+    # Check for accurate parsing of parameters for equilibrium calculations
+    assert list(
+        df.loc[3, ["wav", "int", "airbrd", "selbrd", "Pshft", "isoG", "iso", "mol"]]
+    ) == [
+        3.412985,
+        2.4320e-32,
+        0.0368,
+        0.2214,
+        0.000000,
+        171,
+        3,
+        1,
+    ]
+
+    print("GEISA parsing process of molecule H2O works normally.\n")
+
+    return True
+
+
+@pytest.mark.local
+def test_local_geisa_co2(verbose=True, warnings=True, **kwargs):
+
+    # 1. Load local file
+    fileName = files["CO2"]
+    df = gei2df(getTestFile(fileName), cache="regen")
+    if verbose:
+        print("Read {0}".format(fileName))
+        print("-------------------------------------")
+        print(df)
+
+    # 2. Check if the parser works correctly
+    # Check for accurate parsing of parameters for equilibrium calculations
+    assert list(
+        df.loc[3, ["wav", "int", "airbrd", "selbrd", "Pshft", "isoG", "iso", "mol"]]
+    ) == [
+        397.727316,
+        4.0280e-29,
+        0.0748,
+        0.1019,
+        -0.000580,
+        626,
+        1,
+        2,
+    ]
+
+    print("GEISA parsing process of molecule CO2 works normally.\n")
+
+    return True
+
+
+# Run all test cases
+def _run_testcases(verbose=True, *args, **kwargs):
+
+    test_local_geisa_co(verbose=verbose, *args, **kwargs)
+    test_local_geisa_h2o(verbose=verbose, *args, **kwargs)
+    test_local_geisa_co2(verbose=verbose, *args, **kwargs)
+
+    return True
+
+
+if __name__ == "__main__":
+    print("Running test_geisa.py")
+
+    _run_testcases(verbose=True)
diff --git a/radis/test/misc/test_utils.py b/radis/test/misc/test_utils.py
index f1f71a93c..fe4b7de30 100644
--- a/radis/test/misc/test_utils.py
+++ b/radis/test/misc/test_utils.py
@@ -25,6 +25,10 @@ def test_files_from_regex_1(*args, **kwargs):
     # This list will have to be updated if more .par files are added/removed in/from the folder
     # This list is what is the expected output from get_files_from_regex()
     path_actual = [
+        "geisa_CO_fragment.par",
+        "geisa_CO2_fragment.par",
+        "geisa_H2O_fragment.par",
+        "geisa_O2_fragment.par",
         "hitran_co2_626_bandhead_4165_4200nm.par",
         "hitran_2016_H2O_2iso_2000_2100cm.par",
         "hitran_CO_fragment.par",
@@ -58,6 +62,7 @@ def test_files_from_regex_2(*args, **kwargs):
     # This list will have to be updated if more files matching regular expression cdsd_* are added/removed in/from the folder
     # This list is what is the expected output from get_files_from_regex()
     path_actual = [
+        "geisa_CO2_fragment.par",
         "hitran_CO2_fragment.par",
         "hitran_co2_626_bandhead_4165_4200nm.par",
     ]