Repository: radis/radis (language: python)

Issue:
Bug Report: fetch_exomol() does not work for H2O, CO2, CH4, NH3, OH, NO, HCl
### 🐛 Describe the bug

``radis.io.exomol.fetch_exomol()`` does not work for H2O, CO2, CH4, NH3, OH, NO, HCl (and maybe for other species also, but I didn't test all species).
NB: ``radis.io.exomol.fetch_exomol()`` works perfectly for CO (and maybe for other species also, but I didn't test all species).

I tried the following script with molecule being ``"H2O"``, ``"CO2"``, ``"CH4"``, ``"NH3"``, ``"OH"``, ``"NO"``, or ``"HCl"``.
```
import radis
from radis.io.exomol import fetch_exomol
molecule="H2O" # "H2O", "CO2", "CH4", "NH3", "OH", "NO", "HCl"
fetch_exomol(
    molecule=molecule,
    database=None,
    isotope="1",
    cache='regen',
    verbose=True,
    clean_cache_files=True,
    return_local_path=False,
    engine="default",
)
```
The script was run on an HPC cluster (node: 1, cpu: 1, mem: 175GB) so there should be enough memory to load large databases.

For ``H2O``, I got the following error:
```
Using ExoMol database POKAZATEL for 1H2-16O (recommended by the ExoMol team). All available databases are ['BT2', 'VT', 'WAT-UV296', 'xsec-POKAZATEL', 'CKYKKY', 'xsec-VUV-DTU', 'YYZYK-HF', 'POKAZATEL']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Reading transition file
Note: Caching line transition data to the HDF5 format with vaex. After the second time, it will become much faster.
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="H2O",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:769, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    767 b = trans.nu_lines.isnan()  # TODO: check if can be made faster?
    768 if b.sum() > 0:
--> 769     raise NotImplementedError(
    770         "transition wavenumber not given. Has to be computed from upper/lower level energy look up"
    771     )
    773 # TODO Load extra columns (precomputed Sij0, etc.)
    774 # trans_file_extra = self.trans_file.with_name(
    775 #    self.trans_file.stem + "_extra" + self.trans_file.suffix
   (...)
    782 
    783 # mask needs to be applied
    784 mask_needed = True

NotImplementedError: transition wavenumber not given. Has to be computed from upper/lower level energy look up

```

For ``CO2``:
```
Using ExoMol database UCL-4000 for 12C-16O2 (recommended by the ExoMol team). All available databases are ['xsec-UCL-4000', 'xsec-VUV-Venot-2018', 'AMES', 'Ames-2016', 'CDSD-4000', 'Venot', 'HITEMP', 'Zak-16-12-16', 'xsec-VUV-DTU', 'UCL-4000']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Downloading http://www.exomol.com/db/CO2/12C-16O2/12C-16O2__H2.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/CO2/12C-16O2/12C-16O2__He.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/CO2/12C-16O2/12C-16O2__air.broad
[05/17/22 09:47:04] ERROR    error opening '/gpfs/workdir/mariottop/.radisdb/exomol/CO __init__.py:259
                             2/12C-16O2/UCL-4000/12C-16O2__UCL-4000.states.bz2.hdf5'                  
                             Traceback (most recent call last):                                       
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/__init__.py", line 232, in open                
                                 ds = vaex.dataset.open(path, fs_options=fs_options,                  
                             fs=fs, **kwargs)                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 73, in open                  
                                 return opener.open(path, fs_options=fs_options,                      
                             fs=fs, *args, **kwargs)                                                  
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 1438, in open                
                                 return cls(path, *args, **kwargs)                                    
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 71, in                  
                             __init__                                                                 
                                 self._load()                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 202, in                 
                             _load                                                                    
                                 self._load_columns(self.h5file["/table"])                            
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 374, in                 
                             _load_columns                                                            
                                 elif hasattr(column["data"], "dtype"):                               
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/h5py/_hl/group.py", line 305, in                    
                             __getitem__                                                              
                                 oid = h5o.open(self.id, self._e(name),                               
                             lapl=self._lapl)                                                         
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/h5o.pyx", line 190, in h5py.h5o.open                        
                             KeyError: "Unable to open object (object 'data' doesn't                  
                             exist)"                                                                  
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [7], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="CO2",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:577, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    575 if self.states_file.with_suffix(".bz2.hdf5").exists():
    576     states_file_cache = self.states_file.with_suffix(".bz2.hdf5")
--> 577     states = vaex.open(states_file_cache)
    578 elif self.states_file.with_suffix(".hdf5").exists():
    579     states_file_cache = self.states_file.with_suffix(".hdf5")

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:232, in open(path, convert, progress, shuffle, fs_options, fs, *args, **kwargs)
    230     ds = vaex.dataset.open(path_output, fs_options=fs_options, fs=fs, **kwargs)
    231 else:
--> 232     ds = vaex.dataset.open(path, fs_options=fs_options, fs=fs, **kwargs)
    233 df = vaex.from_dataset(ds)
    234 if df is None:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:73, in open(path, fs_options, fs, *args, **kwargs)
     71     if opener.quick_test(path, fs_options=fs_options, fs=fs):
     72         if opener.can_open(path, fs_options=fs_options, fs=fs, *args, **kwargs):
---> 73             return opener.open(path, fs_options=fs_options, fs=fs, *args, **kwargs)
     75 # otherwise try all openers
     76 for opener in opener_classes:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:1438, in DatasetFile.open(cls, path, *args, **kwargs)
   1436 @classmethod
   1437 def open(cls, path, *args, **kwargs):
-> 1438     return cls(path, *args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:71, in Hdf5MemoryMapped.__init__(self, path, write, fs_options, fs, nommap, group, _fingerprint)
     69 self.group = group
     70 self._version = 1
---> 71 self._load()
     72 if not write:  # in write mode, call freeze yourself, so the hashes are computed
     73     self._freeze()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:202, in Hdf5MemoryMapped._load(self)
    200 if "table" in self.h5file:
    201     self._version = 2
--> 202     self._load_columns(self.h5file["/table"])
    203     self.group = "/table"
    204 root_datasets = [dataset for name, dataset in self.h5file.items() if isinstance(dataset, h5py.Dataset)]

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:374, in Hdf5MemoryMapped._load_columns(self, h5data, first)
    372     column = self._map_hdf5_array(column)
    373     self.add_column(column_name, column)
--> 374 elif hasattr(column["data"], "dtype"):
    375     column = self._map_column(column)
    376     self.add_column(column_name, column)

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/h5py/_hl/group.py:305, in Group.__getitem__(self, name)
    303         raise ValueError("Invalid HDF5 object reference")
    304 elif isinstance(name, (bytes, str)):
--> 305     oid = h5o.open(self.id, self._e(name), lapl=self._lapl)
    306 else:
    307     raise TypeError("Accessing a group is done with bytes or str, "
    308                     " not {}".format(type(name)))

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File h5py/h5o.pyx:190, in h5py.h5o.open()

KeyError: "Unable to open object (object 'data' doesn't exist)"
```

For ``CH4``:
```
Using ExoMol database YT34to10 for 12C-1H4 (recommended by the ExoMol team). All available databases are ['xsec-YT10to10', 'YT10to10', 'YT34to10']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Reading transition file
Note: Caching line transition data to the HDF5 format with vaex. After the second time, it will become much faster.
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Input In [8], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="CH4",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:769, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    767 b = trans.nu_lines.isnan()  # TODO: check if can be made faster?
    768 if b.sum() > 0:
--> 769     raise NotImplementedError(
    770         "transition wavenumber not given. Has to be computed from upper/lower level energy look up"
    771     )
    773 # TODO Load extra columns (precomputed Sij0, etc.)
    774 # trans_file_extra = self.trans_file.with_name(
    775 #    self.trans_file.stem + "_extra" + self.trans_file.suffix
   (...)
    782 
    783 # mask needs to be applied
    784 mask_needed = True

NotImplementedError: transition wavenumber not given. Has to be computed from upper/lower level energy look up
```

For ``NH3``: 
```
Using ExoMol database CoYuTe for 14N-1H3 (recommended by the ExoMol team). All available databases are ['BYTe', 'C2018', 'xsec-VUV-DTU', 'xsec-BYTe', 'CoYuTe']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Note: Caching states data to the hdf5 format with vaex. After the second time, it will become much faster.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File /gpfs/workdir/mariottop/radis/radis/io/exomolapi.py:254, in read_states(statesf, dic_def, engine)
    253 try:
--> 254     dat = vaex.from_csv(
    255         statesf,
    256         compression="bz2",
    257         sep=r"\s+",
    258         usecols=usecol,
    259         names=names,
    260         convert=True,
    261     )
    262 except:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:567, in from_csv(filename_or_buffer, copy_index, chunk_size, convert, fs_options, fs, progress, **kwargs)
    566 path_output = convert if isinstance(convert, str) else vaex.convert._convert_name(filename_or_buffer)
--> 567 vaex.convert.convert_csv(
    568     path_input=filename_or_buffer, fs_options_input=fs_options, fs_input=fs,
    569     path_output=path_output, fs_options_output=fs_options, fs_output=fs,
    570     chunk_size=chunk_size,
    571     copy_index=copy_index,
    572     progress=progress,
    573     **kwargs
    574 )
    575 return open(path_output, fs_options=fs_options, fs=fs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:52, in convert_csv(path_input, fs_options_input, fs_input, path_output, fs_options_output, fs_output, progress, *args, **kwargs)
     51     _from_csv_convert_and_read(path_input, path_output=path_output, fs_options=fs_options_input, fs=fs_input, progress=progress, **kwargs)
---> 52 cached_output(*args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/cache.py:422, in output_file.<locals>.wrapper1.<locals>.wrapper2.<locals>.call(callable, *args, **kwargs)
    421 log.info('file %s does not exist yet, running conversion %s → %s', path_output_meta, path_input, path_output)
--> 422 value = callable(*args, **kwargs)
    423 write_fingerprint()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:51, in convert_csv.<locals>.cached_output(*args, **kwargs)
     50     kwargs['chunk_size'] = 5_000_000
---> 51 _from_csv_convert_and_read(path_input, path_output=path_output, fs_options=fs_options_input, fs=fs_input, progress=progress, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:70, in _from_csv_convert_and_read(filename_or_buffer, path_output, chunk_size, fs_options, fs, copy_index, progress, **kwargs)
     69 with vaex.file.open(filename_or_buffer, fs_options=fs_options, fs=fs, for_arrow=True) as f:
---> 70     csv_reader = pd.read_csv(filename_or_buffer, chunksize=chunk_size, **kwargs)
     71     for i, df_pandas in enumerate(csv_reader):

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/util/_decorators.py:311, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    306     warnings.warn(
    307         msg.format(arguments=arguments),
    308         FutureWarning,
    309         stacklevel=stacklevel,
    310     )
--> 311 return func(*args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:680, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, error_bad_lines, warn_bad_lines, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options)
    678 kwds.update(kwds_defaults)
--> 680 return _read(filepath_or_buffer, kwds)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:572, in _read(filepath_or_buffer, kwds)
    571 # Check for duplicates in names.
--> 572 _validate_names(kwds.get("names", None))
    574 # Create the parser.

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:532, in _validate_names(names)
    531 if len(names) != len(set(names)):
--> 532     raise ValueError("Duplicate names are not allowed.")
    533 if not (
    534     is_list_like(names, allow_sets=False) or isinstance(names, abc.KeysView)
    535 ):

ValueError: Duplicate names are not allowed.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Input In [9], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="NH3",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:585, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    581     else:
    582         print(
    583             "Note: Caching states data to the hdf5 format with vaex. After the second time, it will become much faster."
    584         )
--> 585         states = exomolapi.read_states(self.states_file, dic_def, engine="vaex")
    586     ndstates = vaex.array_types.to_numpy(states)
    588 # load pf

File /gpfs/workdir/mariottop/radis/radis/io/exomolapi.py:263, in read_states(statesf, dic_def, engine)
    254         dat = vaex.from_csv(
    255             statesf,
    256             compression="bz2",
   (...)
    260             convert=True,
    261         )
    262     except:
--> 263         dat = vaex.read_csv(
    264             statesf, sep=r"\s+", usecols=usecol, names=names, convert=True
    265         )
    266 elif engine == "csv":
    267     try:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:594, in read_csv(filepath_or_buffer, **kwargs)
    592 def read_csv(filepath_or_buffer, **kwargs):
    593     '''Alias to from_csv.'''
--> 594     return from_csv(filepath_or_buffer, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:567, in from_csv(filename_or_buffer, copy_index, chunk_size, convert, fs_options, fs, progress, **kwargs)
    565 import vaex.convert
    566 path_output = convert if isinstance(convert, str) else vaex.convert._convert_name(filename_or_buffer)
--> 567 vaex.convert.convert_csv(
    568     path_input=filename_or_buffer, fs_options_input=fs_options, fs_input=fs,
    569     path_output=path_output, fs_options_output=fs_options, fs_output=fs,
    570     chunk_size=chunk_size,
    571     copy_index=copy_index,
    572     progress=progress,
    573     **kwargs
    574 )
    575 return open(path_output, fs_options=fs_options, fs=fs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:52, in convert_csv(path_input, fs_options_input, fs_input, path_output, fs_options_output, fs_output, progress, *args, **kwargs)
     50         kwargs['chunk_size'] = 5_000_000
     51     _from_csv_convert_and_read(path_input, path_output=path_output, fs_options=fs_options_input, fs=fs_input, progress=progress, **kwargs)
---> 52 cached_output(*args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/cache.py:422, in output_file.<locals>.wrapper1.<locals>.wrapper2.<locals>.call(callable, *args, **kwargs)
    420 if not vaex.file.exists(path_output, fs_options=fs_options_output_, fs=fs_output):
    421     log.info('file %s does not exist yet, running conversion %s → %s', path_output_meta, path_input, path_output)
--> 422     value = callable(*args, **kwargs)
    423     write_fingerprint()
    424     return value

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:51, in convert_csv.<locals>.cached_output(*args, **kwargs)
     48 if 'chunk_size' not in kwargs:
     49     # make it memory efficient by default
     50     kwargs['chunk_size'] = 5_000_000
---> 51 _from_csv_convert_and_read(path_input, path_output=path_output, fs_options=fs_options_input, fs=fs_input, progress=progress, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/convert.py:70, in _from_csv_convert_and_read(filename_or_buffer, path_output, chunk_size, fs_options, fs, copy_index, progress, **kwargs)
     68     print("Converting csv to chunk files")
     69 with vaex.file.open(filename_or_buffer, fs_options=fs_options, fs=fs, for_arrow=True) as f:
---> 70     csv_reader = pd.read_csv(filename_or_buffer, chunksize=chunk_size, **kwargs)
     71     for i, df_pandas in enumerate(csv_reader):
     72         df = vaex.from_pandas(df_pandas, copy_index=copy_index)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/util/_decorators.py:311, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    305 if len(args) > num_allow_args:
    306     warnings.warn(
    307         msg.format(arguments=arguments),
    308         FutureWarning,
    309         stacklevel=stacklevel,
    310     )
--> 311 return func(*args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:680, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, error_bad_lines, warn_bad_lines, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options)
    665 kwds_defaults = _refine_defaults_read(
    666     dialect,
    667     delimiter,
   (...)
    676     defaults={"delimiter": ","},
    677 )
    678 kwds.update(kwds_defaults)
--> 680 return _read(filepath_or_buffer, kwds)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:572, in _read(filepath_or_buffer, kwds)
    569 nrows = kwds.get("nrows", None)
    571 # Check for duplicates in names.
--> 572 _validate_names(kwds.get("names", None))
    574 # Create the parser.
    575 parser = TextFileReader(filepath_or_buffer, **kwds)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/pandas/io/parsers/readers.py:532, in _validate_names(names)
    530 if names is not None:
    531     if len(names) != len(set(names)):
--> 532         raise ValueError("Duplicate names are not allowed.")
    533     if not (
    534         is_list_like(names, allow_sets=False) or isinstance(names, abc.KeysView)
    535     ):
    536         raise ValueError("Names should be an ordered collection.")

ValueError: Duplicate names are not allowed.
```
For ``OH``:
```
Using ExoMol database MoLLIST for 16O-1H (recommended by the ExoMol team). All available databases are ['HITEMP', 'xsec-HITEMP', 'MoLLIST']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Downloading http://www.exomol.com/db/OH/16O-1H/16O-1H__H2.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/OH/16O-1H/16O-1H__He.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/OH/16O-1H/16O-1H__air.broad
Error: Couldn't download .broad file and save.
/gpfs/workdir/mariottop/radis/radis/io/exomol.py:542: Warning: 
                    No default broadening exponent in def file. Assigned n = 0.5
                    
  warnings.warn(
/gpfs/workdir/mariottop/radis/radis/io/exomol.py:552: Warning: 
                    No default broadening in def file. Assigned alpha_ref = 0.07
                    
  warnings.warn(
[05/17/22 09:51:32] ERROR    error opening '/gpfs/workdir/mariottop/.radisdb/exomol/OH __init__.py:259
                             /16O-1H/MoLLIST/16O-1H__MoLLIST.states.bz2.hdf5'                         
                             Traceback (most recent call last):                                       
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/__init__.py", line 232, in open                
                                 ds = vaex.dataset.open(path, fs_options=fs_options,                  
                             fs=fs, **kwargs)                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 73, in open                  
                                 return opener.open(path, fs_options=fs_options,                      
                             fs=fs, *args, **kwargs)                                                  
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 1438, in open                
                                 return cls(path, *args, **kwargs)                                    
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 71, in                  
                             __init__                                                                 
                                 self._load()                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 202, in                 
                             _load                                                                    
                                 self._load_columns(self.h5file["/table"])                            
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 374, in                 
                             _load_columns                                                            
                                 elif hasattr(column["data"], "dtype"):                               
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/h5py/_hl/group.py", line 305, in                    
                             __getitem__                                                              
                                 oid = h5o.open(self.id, self._e(name),                               
                             lapl=self._lapl)                                                         
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/h5o.pyx", line 190, in h5py.h5o.open                        
                             KeyError: "Unable to open object (object 'data' doesn't                  
                             exist)"                                                                  
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [10], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="OH",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:577, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    575 if self.states_file.with_suffix(".bz2.hdf5").exists():
    576     states_file_cache = self.states_file.with_suffix(".bz2.hdf5")
--> 577     states = vaex.open(states_file_cache)
    578 elif self.states_file.with_suffix(".hdf5").exists():
    579     states_file_cache = self.states_file.with_suffix(".hdf5")

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:232, in open(path, convert, progress, shuffle, fs_options, fs, *args, **kwargs)
    230     ds = vaex.dataset.open(path_output, fs_options=fs_options, fs=fs, **kwargs)
    231 else:
--> 232     ds = vaex.dataset.open(path, fs_options=fs_options, fs=fs, **kwargs)
    233 df = vaex.from_dataset(ds)
    234 if df is None:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:73, in open(path, fs_options, fs, *args, **kwargs)
     71     if opener.quick_test(path, fs_options=fs_options, fs=fs):
     72         if opener.can_open(path, fs_options=fs_options, fs=fs, *args, **kwargs):
---> 73             return opener.open(path, fs_options=fs_options, fs=fs, *args, **kwargs)
     75 # otherwise try all openers
     76 for opener in opener_classes:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:1438, in DatasetFile.open(cls, path, *args, **kwargs)
   1436 @classmethod
   1437 def open(cls, path, *args, **kwargs):
-> 1438     return cls(path, *args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:71, in Hdf5MemoryMapped.__init__(self, path, write, fs_options, fs, nommap, group, _fingerprint)
     69 self.group = group
     70 self._version = 1
---> 71 self._load()
     72 if not write:  # in write mode, call freeze yourself, so the hashes are computed
     73     self._freeze()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:202, in Hdf5MemoryMapped._load(self)
    200 if "table" in self.h5file:
    201     self._version = 2
--> 202     self._load_columns(self.h5file["/table"])
    203     self.group = "/table"
    204 root_datasets = [dataset for name, dataset in self.h5file.items() if isinstance(dataset, h5py.Dataset)]

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:374, in Hdf5MemoryMapped._load_columns(self, h5data, first)
    372     column = self._map_hdf5_array(column)
    373     self.add_column(column_name, column)
--> 374 elif hasattr(column["data"], "dtype"):
    375     column = self._map_column(column)
    376     self.add_column(column_name, column)

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/h5py/_hl/group.py:305, in Group.__getitem__(self, name)
    303         raise ValueError("Invalid HDF5 object reference")
    304 elif isinstance(name, (bytes, str)):
--> 305     oid = h5o.open(self.id, self._e(name), lapl=self._lapl)
    306 else:
    307     raise TypeError("Accessing a group is done with bytes or str, "
    308                     " not {}".format(type(name)))

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File h5py/h5o.pyx:190, in h5py.h5o.open()

KeyError: "Unable to open object (object 'data' doesn't exist)"
```

For ``NO``:
```
Using ExoMol database XABC for 14N-16O (recommended by the ExoMol team). All available databases are ['HITEMP', 'xsec-XABC', 'NOname', 'XABC']. Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` 

Background atmosphere:  H2
Downloading http://www.exomol.com/db/NO/14N-16O/14N-16O__H2.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/NO/14N-16O/14N-16O__He.broad
Error: Couldn't download .broad file and save.
Downloading http://www.exomol.com/db/NO/14N-16O/14N-16O__air.broad
[05/17/22 09:52:20] ERROR    error opening '/gpfs/workdir/mariottop/.radisdb/exomol/NO __init__.py:259
                             /14N-16O/XABC/14N-16O__XABC.states.bz2.hdf5'                             
                             Traceback (most recent call last):                                       
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/__init__.py", line 232, in open                
                                 ds = vaex.dataset.open(path, fs_options=fs_options,                  
                             fs=fs, **kwargs)                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 73, in open                  
                                 return opener.open(path, fs_options=fs_options,                      
                             fs=fs, *args, **kwargs)                                                  
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/dataset.py", line 1438, in open                
                                 return cls(path, *args, **kwargs)                                    
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 71, in                  
                             __init__                                                                 
                                 self._load()                                                         
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 202, in                 
                             _load                                                                    
                                 self._load_columns(self.h5file["/table"])                            
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/vaex/hdf5/dataset.py", line 374, in                 
                             _load_columns                                                            
                                 elif hasattr(column["data"], "dtype"):                               
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "/gpfs/users/mariottop/.conda/envs/pynabam/lib/pyt                
                             hon3.8/site-packages/h5py/_hl/group.py", line 305, in                    
                             __getitem__                                                              
                                 oid = h5o.open(self.id, self._e(name),                               
                             lapl=self._lapl)                                                         
                               File "h5py/_objects.pyx", line 54, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/_objects.pyx", line 55, in                                  
                             h5py._objects.with_phil.wrapper                                          
                               File "h5py/h5o.pyx", line 190, in h5py.h5o.open                        
                             KeyError: "Unable to open object (object 'data' doesn't                  
                             exist)"                                                                  
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [11], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="NO",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:370, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    368 if load_wavenum_max is None:
    369     load_wavenum_max = np.inf
--> 370 mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    372 # Attributes of the DataFrame
    373 from radis.db.classes import HITRAN_MOLECULES

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:577, in MdbExomol.__init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    575 if self.states_file.with_suffix(".bz2.hdf5").exists():
    576     states_file_cache = self.states_file.with_suffix(".bz2.hdf5")
--> 577     states = vaex.open(states_file_cache)
    578 elif self.states_file.with_suffix(".hdf5").exists():
    579     states_file_cache = self.states_file.with_suffix(".hdf5")

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/__init__.py:232, in open(path, convert, progress, shuffle, fs_options, fs, *args, **kwargs)
    230     ds = vaex.dataset.open(path_output, fs_options=fs_options, fs=fs, **kwargs)
    231 else:
--> 232     ds = vaex.dataset.open(path, fs_options=fs_options, fs=fs, **kwargs)
    233 df = vaex.from_dataset(ds)
    234 if df is None:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:73, in open(path, fs_options, fs, *args, **kwargs)
     71     if opener.quick_test(path, fs_options=fs_options, fs=fs):
     72         if opener.can_open(path, fs_options=fs_options, fs=fs, *args, **kwargs):
---> 73             return opener.open(path, fs_options=fs_options, fs=fs, *args, **kwargs)
     75 # otherwise try all openers
     76 for opener in opener_classes:

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/dataset.py:1438, in DatasetFile.open(cls, path, *args, **kwargs)
   1436 @classmethod
   1437 def open(cls, path, *args, **kwargs):
-> 1438     return cls(path, *args, **kwargs)

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:71, in Hdf5MemoryMapped.__init__(self, path, write, fs_options, fs, nommap, group, _fingerprint)
     69 self.group = group
     70 self._version = 1
---> 71 self._load()
     72 if not write:  # in write mode, call freeze yourself, so the hashes are computed
     73     self._freeze()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:202, in Hdf5MemoryMapped._load(self)
    200 if "table" in self.h5file:
    201     self._version = 2
--> 202     self._load_columns(self.h5file["/table"])
    203     self.group = "/table"
    204 root_datasets = [dataset for name, dataset in self.h5file.items() if isinstance(dataset, h5py.Dataset)]

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/vaex/hdf5/dataset.py:374, in Hdf5MemoryMapped._load_columns(self, h5data, first)
    372     column = self._map_hdf5_array(column)
    373     self.add_column(column_name, column)
--> 374 elif hasattr(column["data"], "dtype"):
    375     column = self._map_column(column)
    376     self.add_column(column_name, column)

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File ~/.conda/envs/pynabam/lib/python3.8/site-packages/h5py/_hl/group.py:305, in Group.__getitem__(self, name)
    303         raise ValueError("Invalid HDF5 object reference")
    304 elif isinstance(name, (bytes, str)):
--> 305     oid = h5o.open(self.id, self._e(name), lapl=self._lapl)
    306 else:
    307     raise TypeError("Accessing a group is done with bytes or str, "
    308                     " not {}".format(type(name)))

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File h5py/h5o.pyx:190, in h5py.h5o.open()

KeyError: "Unable to open object (object 'data' doesn't exist)"
```

For ``HCl``:
```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Input In [12], in <cell line: 1>()
----> 1 fetch_exomol(
      2         molecule="HCl",
      3         database=None,
      4         # local_databases=None,
      5         # databank_name="EXOMOL-{molecule}",
      6         isotope="1",
      7         # load_wavenum_min=0.,
      8         # load_wavenum_max=100000.,
      9         # columns=None,
     10         cache='regen',
     11         verbose=True,
     12         clean_cache_files=True,
     13         return_local_path=False,
     14         engine="default",
     15     )

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:328, in fetch_exomol(molecule, database, local_databases, databank_name, isotope, load_wavenum_min, load_wavenum_max, columns, cache, verbose, clean_cache_files, return_local_path, return_partition_function, engine)
    322     raise ValueError(
    323         f"In fetch_exomol, ``isotope`` must be an integer. Got `{isotope}` "
    324         + "Only one isotope can be queried at a time. "
    325     )
    327 full_molecule_name = get_exomol_full_isotope_name(molecule, isotope)
--> 328 known_exomol_databases, recommended_database = get_exomol_database_list(
    329     molecule, full_molecule_name
    330 )
    332 _exomol_use_hint = "Select one of them with `radis.fetch_exomol(DATABASE_NAME)`, `SpectrumFactory.fetch_databank('exomol', exomol_database=DATABASE_NAME')`, or `calc_spectrum(..., databank=('exomol', DATABASE_NAME))` \n"
    333 if database is None or database == "default":

File /gpfs/workdir/mariottop/radis/radis/io/exomol.py:137, in get_exomol_database_list(molecule, isotope_full_name)
    131         print(
    132             f"Multiple recommended databases found for {molecule} in ExoMol : {databases_recommended}. This is unexpected. Using the first"
    133         )
    135 databases = databases + databases_recommended
--> 137 return databases, databases_recommended[0]

IndexError: list index out of range
```



### 💡 Possible solutions

_No response_

### 🎲 Radis version

0.13.0

### 💻 Operating system

Linux
Fix gpu
<!-- Please be sure to check out our contributing guidelines,
https://github.com/radis/radis/blob/develop/CONTRIBUTING.md . -->

### Description
<!-- Provide a general description of what your pull request does. -->

This pull request is to address a very serious indexing problem in the GPU spectra.

<!-- If the pull request closes any open issues you can add this.
If you replace <Issue Number> with a number, GitHub will automatically link it.
If this pull request is unrelated to any issues, please remove
the following line. -->

Also Fixes radiance calculation in GPU mode #461 
GPU only computes absorption coefficient. Other spectral arrays , and slit arrays, are computed by the CPU. 

Also, improve how quantities are recomputed from one-another in a Spectrum (don't compute everything at equilibrium --> faster)

Bug Report: Exomol, hdf5 tests failing on travis (buffer error)
### 🐛 Describe the bug

Travis failing: https://app.travis-ci.com/github/radis/radis/jobs/571532389

Steps to reproduce:
- create a new virtual environment using `conda create -n radis_env python=3.8`  and fetch latest develop branch locally
- `pip install -e .`  (pip install -e .[dev] -v didn't work)
- delete `radis.json` and `.radisdb`
- `pytest`

<details>
  <summary>Following is the version of dependencies in the environment:</summary>

- anyio==3.6.1
- aplus==0.11.0
- argon2-cffi==21.3.0
- argon2-cffi-bindings==21.2.0
- asgiref==3.5.2
- astropy==5.1
- astroquery==0.4.6
- asttokens==2.0.5
- attrs==21.4.0
- backcall==0.2.0
- beautifulsoup4==4.11.1
- blake3==0.3.1
- bleach==5.0.0
- bqplot==0.12.33
- cachetools==5.2.0
- certifi==2022.5.18.1
- cffi==1.15.0
- charset-normalizer==2.0.12
- click==8.1.3
- cloudpickle==2.1.0
- commonmark==0.9.1
- configparser==5.2.0
- cryptography==37.0.2
- cycler==0.11.0
- Cython==0.29.30
- dask==2022.5.2
- debugpy==1.6.0
- decorator==5.1.1
- defusedxml==0.7.1
- entrypoints==0.4
- executing==0.8.3
- fastapi==0.78.0
- fastjsonschema==2.15.3
- filelock==3.7.0
- fonttools==4.33.3
- frozendict==2.3.2
- fsspec==2022.5.0
- future==0.18.2
- h11==0.13.0
- h5py==3.7.0
- habanero==1.2.2
- hitran-api==1.2.2.0
- hjson==3.0.2
- html5lib==1.1
- httptools==0.4.0
- idna==3.3
- importlib-metadata==4.11.4
- importlib-resources==5.7.1
- iniconfig==1.1.1
- ipydatawidgets==4.3.1.post1
- ipykernel==6.13.0
- ipyleaflet==0.16.0
- ipympl==0.9.1
- ipython==8.4.0
- ipython-genutils==0.2.0
- ipyvolume==0.5.2
- ipyvue==1.7.0
- ipyvuetify==1.8.2
- ipywebrtc==0.6.0
- ipywidgets==7.7.0
- jedi==0.18.1
- jeepney==0.8.0
- Jinja2==3.1.2
- joblib==1.1.0
- json-tricks==3.15.5
- jsonschema==4.5.1
- jupyter-client==7.3.1
- jupyter-core==4.10.0
- jupyterlab-pygments==0.2.2
- jupyterlab-widgets==1.1.0
- keyring==23.5.1
- kiwisolver==1.4.2
- llvmlite==0.38.1
- locket==1.0.0
- lxml==4.8.0
- MarkupSafe==2.1.1
- matplotlib==3.5.2
- matplotlib-inline==0.1.3
- mistune==0.8.4
- mpldatacursor==0.7.1
- nbclient==0.6.3
- nbconvert==6.5.0
- nbformat==5.4.0
- nest-asyncio==1.5.5
- notebook==6.4.11
- numba==0.55.2
- numexpr==2.8.1
- numpy==1.22.4
- packaging==21.3
- pandas==1.4.2
- pandocfilters==1.5.0
- parso==0.8.3
- partd==1.2.0
- PeakUtils==1.3.3
- pexpect==4.8.0
- pickleshare==0.7.5
- Pillow==9.1.1
- plotly==5.8.0
- pluggy==1.0.0
- progressbar2==4.0.0
- prometheus-client==0.14.1
- prompt-toolkit==3.0.29
- psutil==5.9.1
- ptyprocess==0.7.0
- publib==0.3.3
- pure-eval==0.2.2
- py==1.11.0
- pyarrow==8.0.0
- pycparser==2.21
- pydantic==1.9.1
- pyerfa==2.0.0.1
- Pygments==2.12.0
- pyparsing==3.0.9
- pyrsistent==0.18.1
- pytest==7.1.2
- python-dateutil==2.8.2
- python-dotenv==0.20.0
- python-utils==3.3.2
- pythreejs==2.3.0
- pytz==2022.1
- pyvo==1.3
- PyYAML==6.0
- pyzmq==23.0.0
- -e git+https://github.com/anandxkumar/radis.git@1136890cc2b84734fe037373a48969b3444ce190#egg=radis
- requests==2.27.1
- rich==12.4.4
- scipy==1.8.1
- seaborn==0.11.2
- SecretStorage==3.3.2
- Send2Trash==1.8.0
- six==1.16.0
- sniffio==1.2.0
- soupsieve==2.3.2.post1
- stack-data==0.2.0
- starlette==0.19.1
- tables==3.7.0
- tabulate==0.8.9
- tenacity==8.0.1
- termcolor==1.1.0
- terminado==0.15.0
- tinycss2==1.1.1
- tomli==2.0.1
- toolz==0.11.2
- tornado==6.1
- tqdm==4.64.0
- traitlets==5.2.1.post0
- traittypes==0.2.1
- tuna==0.5.11
- typing_extensions==4.2.0
- urllib3==1.26.9
- uvicorn==0.17.6
- uvloop==0.16.0
- vaex==4.9.1
- vaex-astro==0.9.1
- vaex-core==4.9.1
- vaex-hdf5==0.12.1
- vaex-jupyter==0.7.0
- vaex-ml==0.17.0
- vaex-server==0.8.1
- vaex-viz==0.5.1
- watchgod==0.8.2
- wcwidth==0.2.5
- webencodings==0.5.1
- websockets==10.3
- widgetsnbextension==3.6.0
- xarray==2022.3.0
- xyzservices==2022.4.0
- zipp==3.8.0

</details>
<details>
<summary> Error </summary>

platform linux -- Python 3.8.13, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/travis/build/radis/radis, configfile: setup.cfg
plugins: cov-3.0.0, asdf-2.11.1, anyio-3.6.1
collected 227 items / 16 deselected / 211 selected                             
radis/test/test_misc.py ......                                           [  2%]
radis/test/test_phys.py ..                                               [  3%]
radis/test/db/test_molecule_lists.py ..                                  [  4%]
radis/test/db/test_molecules.py ...                                      [  6%]
radis/test/db/test_molparams.py .                                        [  6%]
radis/test/io/test_exomol.py .FF                                         [  8%]
radis/test/io/test_geisa.py ...                                          [  9%]
radis/test/io/test_hdf5.py FF                                            [ 10%]
radis/test/io/test_hitemp.py Warning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitemp/OH-13_HITEMP2020_temp00000.hdf5']
F..Warning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitemp/OH-13_HITEMP2020_temp00000.hdf5']
Exception ignored in: <function DataFileManager.__del__ at 0x7fcecc2eec10>
Traceback (most recent call last):
  File "/home/travis/build/radis/radis/radis/api/hdf5.py", line 238, in __del__
    self._close_temp_batch_files()
  File "/home/travis/build/radis/radis/radis/api/hdf5.py", line 225, in _close_temp_batch_files
    os.remove(self._temp_batch_files[i])
FileNotFoundError: [Errno 2] No such file or directory: '/home/travis/.radisdb/hitemp/OH-13_HITEMP2020_temp00000.hdf5'
FFWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitemp/CO2-02_02500-03000_HITEMP2010_temp00000.hdf5']
FFF                                    [ 14%]
radis/test/io/test_hitran_cdsd.py .......                                [ 17%]
radis/test/io/test_query.py ....Warning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitemp/CO-05_HITEMP2019_temp00000.hdf5']
FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/build/radis/radis/radis/test/files/hitran/CO_temp00000.hdf5']
FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO_temp00000.hdf5']
F                                      [ 20%]
radis/test/lbl/test_bands.py .                                           [ 21%]
radis/test/lbl/test_base.py .......F                                     [ 25%]
radis/test/lbl/test_broadening.py ........FF                             [ 29%]
radis/test/lbl/test_calc.py .FF.FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO2_temp00000.hdf5']
FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO_temp00000.hdf5']
F.F.                                   [ 34%]
radis/test/lbl/test_database.py ..                                       [ 35%]
radis/test/lbl/test_factory.py ................                          [ 43%]
radis/test/lbl/test_labels.py .                                          [ 43%]
radis/test/lbl/test_loader.py ....                                       [ 45%]
radis/test/lbl/test_overp.py .                                           [ 45%]
radis/test/lbl/test_spectrum.py ....                                     [ 47%]
radis/test/levels/test_partfunc.py .............                         [ 54%]
radis/test/los/test_slabs.py ....                                        [ 55%]
radis/test/misc/test_arrays.py .............                             [ 62%]
radis/test/misc/test_basics.py ...                                       [ 63%]
radis/test/misc/test_config.py ..                                        [ 64%]
radis/test/misc/test_utils.py ..                                         [ 65%]
radis/test/phys/test_blackbody.py ...                                    [ 66%]
radis/test/phys/test_dunham.py ..                                        [ 67%]
radis/test/phys/test_units.py .                                          [ 68%]
radis/test/spectrum/test_compare.py .FF                                  [ 69%]
radis/test/spectrum/test_operations.py ............                      [ 75%]
radis/test/spectrum/test_rescale.py ........                             [ 79%]
radis/test/spectrum/test_spectrum.py ...............                     [ 86%]
radis/test/spectrum/test_utils.py ..                                     [ 87%]
radis/test/tools/test_database.py ....F                                  [ 89%]
radis/test/tools/test_gascomp.py .                                       [ 90%]
radis/test/tools/test_line_survey.py ..                                  [ 90%]
radis/test/tools/test_slit.py ...........                                [ 96%]
radis/test/tools/test_track_ref.py .FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO_temp00000.hdf5']
FWarning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO_temp00000.hdf5']
FF                                 [ 98%]
radis/test/validation/test_CO2_3Tvib_vs_klarenaar.py .                   [ 99%]
radis/test/validation/test_RADIS_vs_HAPI_line_broadening.py F            [ 99%]
radis/test/validation/test_validation_vs_specair_noneqCO.py .            [100%]Warning : 1 files were written but not combined. Deleting them.
['/home/travis/.radisdb/hitran/CO2_temp00000.hdf5']
=================================== FAILURES ===================================
__________________________ test_calc_exomol_spectrum ___________________________
verbose = True, plot = True, args = (), kwargs = {}
calc_spectrum = <function calc_spectrum at 0x7fcec4b66b80>
    @pytest.mark.needs_connection
    def test_calc_exomol_spectrum(verbose=True, plot=True, *args, **kwargs):
        """Auto-fetch and calculate a SiO spectrum from the ExoMol database
    
        https://github.com/radis/radis/pull/320#issuecomment-884508206
        """
        from radis import calc_spectrum
    
>       s = calc_spectrum(
            1080,
            1320,  # cm-1
            molecule="SiO",
            isotope="1",
            pressure=1.01325,  # bar
            Tgas=1000,  # K
            mole_fraction=0.1,
            path_length=1,  # cm
            broadening_method="fft",  # @ dev: Doesn't work with 'voigt'
            databank=("exomol", "EBJT"),
            verbose=verbose,
        )
radis/test/io/test_exomol.py:55: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
radis/lbl/calc.py:514: in calc_spectrum
    generated_spectrum = _calc_spectrum_one_molecule(
radis/lbl/calc.py:753: in _calc_spectrum_one_molecule
    sf.fetch_databank(**conditions)
radis/lbl/loader.py:1302: in fetch_databank
    df, local_path, Z_exomol = fetch_exomol(
radis/io/exomol.py:202: in fetch_exomol
    mdb = MdbExomol(
radis/api/exomolapi.py:1104: in __init__
    mgr.write(mgr.cache_file(self.states_file), states)
radis/api/hdf5.py:192: in write
    df.export_hdf5(file, group=key, mode="w")
../../../micromamba/envs/radis-env/lib/python3.8/site-packages/vaex/dataframe.py:6912: in export_hdf5
    writer.write(
../../../micromamba/envs/radis-env/lib/python3.8/site-packages/vaex/hdf5/writer.py:42: in __exit__
    self.close()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <vaex.hdf5.writer.Writer object at 0x7fcec09fcc40>
    def close(self):
        if self.mmap is not None:
>           self.mmap.close()
E           BufferError: cannot close exported pointers exist
../../../micromamba/envs/radis-env/lib/python3.8/site-packages/vaex/hdf5/writer.py:34: BufferError
----------------------------- Captured stdout call -----------------------------
Background atmosphere:  Air
Downloading http://www.exomol.com/db/SiO/28Si-16O/EBJT/28Si-16O__EBJT.def
Downloading http://www.exomol.com/db/SiO/28Si-16O/EBJT/28Si-16O__EBJT.pf
Downloading http://www.exomol.com/db/SiO/28Si-16O/EBJT/28Si-16O__EBJT.states.bz2
Downloading http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__H2.broad
Error: Couldn't download .broad file at http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__H2.broad and save.
Downloading http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__He.broad
Error: Couldn't download .broad file at http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__He.broad and save.
Downloading http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__air.broad
Error: Couldn't download .broad file at http://www.exomol.com/db/SiO/28Si-16O/28Si-16O__air.broad and save.
Note: Caching states data to the vaex format. After the second time, it will become much faster.

</details>



### 💡 Possible solutions

Error is possibly occurring when fetching data from hitran, hitemp, and merging the temporary data files causing buffer issues. The issue might lie in the `hdf5.py` file which is responsible for downloading and merging the data lines.

Also, a thing to note is that the issue is not reproducible on `radis-lab` so it may be a dependency version specific issue, some compatibility issue between vaex or other packages.

### 🎲 Radis version

'0.13.0'

### 💻 Operating system

Linux
ReadTheDocs doesnt build
### 🐛 Describe the bug

bugs during Documentaiton build for the past 2 months : https://readthedocs.org/projects/radis/builds/17041385/

Error is unexpected and cannot be reproduced (Radis-LAb; and locally by @erwanp @sagarchotalia @TranHuuNhatHuy @Supriya1702 @arunavabasu-03 ) 


Error first appeared after [this commit](https://github.com/radis/radis/commit/1136890cc2b84734fe037373a48969b3444ce190), however not sure it's related

### 💡 Possible solutions

For the time being we'll just skip this example 

### 🎲 Radis version

0.13.0

### 💻 Operating system

Windows
Documentation fails in 0.11.0
### 🐛 Describe the bug
<!--- A clear and concise description of the bug (or unexpected behavior) -->

Doc build have been failing on the ReadTheDocs  since 0.11.0 was released   
Vaex (likely installed by Pip on RTD) is the culprit. 

### 💡 Possible solutions
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

Switch to Anaconda environment ?  
https://docs.readthedocs.io/en/stable/config-file/v2.html#conda


Reported https://github.com/vaexio/vaex/issues/1708
Refactor extra molecular parameters
refactor extra molparams : remove them from radis.config ; add dedicated molparams_extra.json file ; 
MolParams() can now look-up molecular parameters from the extra JSON file or from the HITRAN molparam.txt file , transparently fr the user  

also  : 
- prepare for 0.13.1 release
- improve docs, mention our Video Channel : https://www.youtube.com/channel/UCO-7NXkubTAiGGxXmvtQlsA
- remove default Tuna profiler support #325


Discussion / hints:




Note: anaconda will not help, it's just that Vaex skips importing the c modules when it detects it is running on readthedocs (fix is coming)
Great, thank you !
If you can point-out to the piece of code you use to detect you're on Readthedocs, I can implement a temporary-default-to-Pandas's pytables in RADIS until the fix is out. 
Actually I found `os.environ.get('READTHEDOCS', None) == 'True'` in your code, will use this


Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 955bf3183..eba5b72ff 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -46,9 +46,13 @@ jobs:
         # Your test script goes here
         - echo ">>> Run tests"
         # use XVFB to have headless display port, and still run the Matplotlib tests.
-        - xvfb-run pytest --cov=./
+        - xvfb-run -a pytest --cov=./ --durations=10
+        # --durations=N to print the slowest N tests
         # lookup 'addopts' in setup.cfg>[tools:pytest] for default tests
 
+      services:
+        - xvfb
+
       after_success:
         # codecoverage
         - pip install codecov
diff --git a/MANIFEST.in b/MANIFEST.in
index 80372e7f3..c2b925364 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -9,6 +9,7 @@ include radis/test/files/*.spec
 include radis/test/files/*.par
 include radis/test/files/*.levels
 include radis/db/molparam.txt
+include radis/db/molparams_extra.json
 include radis/default_radis.json
 recursive-include radis/db *.json
 include radis/test/validation/test_CO2_3Tvib_vs_klarenaar_data/*.csv
diff --git a/docs/_static/assets/general/radisicon.png b/docs/_static/assets/general/radisicon.png
new file mode 100644
index 000000000..1f966ace5
Binary files /dev/null and b/docs/_static/assets/general/radisicon.png differ
diff --git a/docs/_templates/buttons.html b/docs/_templates/buttons.html
index bce722e94..718b94b0b 100644
--- a/docs/_templates/buttons.html
+++ b/docs/_templates/buttons.html
@@ -37,4 +37,9 @@
          src="{{ pathto('_static/assets/general/radislab.png', 1) }}"
          alt="Radis-Lab">
 </a>
+<a href="https://radis.github.io/">
+    <img class="circle-icon"
+         src="{{ pathto('_static/assets/general/radisicon.png', 1) }}"
+         alt="Radis-Lab">
+</a>
 </p>
diff --git a/docs/conf.py b/docs/conf.py
index c5d5ccdb4..c8dad57b4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,6 +21,37 @@
 import os
 import sys
 
+import sphinx_gallery.gen_rst
+
+# %% Custom Example header
+# https://github.com/sphinx-gallery/sphinx-gallery/issues/978
+sphinx_gallery.gen_rst.EXAMPLE_HEADER = """
+.. DO NOT EDIT.
+.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
+.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
+.. "{0}"
+.. LINE NUMBERS ARE GIVEN BELOW.
+
+.. only:: html
+
+    .. note::
+        :class: sphx-glr-download-link-note
+
+        Run this example online :
+
+        - Click :ref:`here <sphx_glr_download_{1}>`
+          to download the full example code{2}
+
+        - Then start `Radis-Lab <https://radis.github.io/radis-lab/>`__,
+          upload the Jupyter notebook, and run it from there.
+
+.. rst-class:: sphx-glr-example-title
+
+.. _sphx_glr_{1}:
+
+"""
+
+#%%
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -142,7 +173,7 @@ def setup(app):
 intersphinx_mapping = {
     "astropy": ("https://docs.astropy.org/en/stable/", None),
     "astroquery": ("https://astroquery.readthedocs.io/en/latest/", None),
-    "cantera": ("https://www.cantera.org/documentation/docs-2.4/sphinx/html/", None),
+    "cantera": ("https://www.cantera.org/documentation/docs-2.6/sphinx/", None),
     "fitroom": ("https://fitroom.readthedocs.io/en/latest/", None),
     "habanero": ("https://habanero.readthedocs.io/en/latest/", None),
     "joblib": ("https://joblib.readthedocs.io/en/latest/", None),
@@ -153,6 +184,7 @@ def setup(app):
     "scipy": ("https://docs.scipy.org/doc/scipy/", None),
     "seaborn": ("https://seaborn.pydata.org/", None),
     "specutils": ("https://specutils.readthedocs.io/en/stable/", None),
+    "vaex": ("https://vaex.readthedocs.io/en/latest/", None),
 }
 
 napoleon_google_docstring = False
@@ -263,7 +295,10 @@ def setup(app):
     "fixed_sidebar": True,
     "analytics_id": "UA-113616205-1",
     "link": "#7A306C",
-    "extra_nav_links": {"RADIS Website": "https://radis.github.io/"},
+    "extra_nav_links": {
+        "RADIS Website": "https://radis.github.io/",
+        "Video Tutorials": "https://www.youtube.com/channel/UCO-7NXkubTAiGGxXmvtQlsA",
+    },
 }
 
 # Add any paths that contain custom themes here, relative to this directory.
diff --git a/docs/dev/_architecture.rst b/docs/dev/_architecture.rst
index ede899355..66612ddd5 100644
--- a/docs/dev/_architecture.rst
+++ b/docs/dev/_architecture.rst
@@ -8,6 +8,7 @@ The RADIS modules are organized with the following flow chart
 .. image:: RADIS_flow_chart.*
     :alt: https://radis.readthedocs.io/en/latest/_images/RADIS_flow_chart.svg
     :scale: 100 %
+    :height: 500px
 
 -------------------------------------------------------------------------
 
diff --git a/docs/lbl/gpu.rst b/docs/lbl/gpu.rst
index a8c73a37b..188f1a5de 100644
--- a/docs/lbl/gpu.rst
+++ b/docs/lbl/gpu.rst
@@ -6,29 +6,42 @@ RADIS-GPU Spectrum Calculation
 
 RADIS provides GPU acceleration to massively speedup spectral computations.
 The GPU code is written in CUDA-C++, which is compiled at runtime through the python package cupy.
-Because the CUDA code is compiled at runtime, a CUDA compiler such as NVRTC or NVCC must be installed before running the code. The easiest way to make sure a CUDA compiler is installed is by installing the Nvidia GPU computing toolkit, see: https://developer.nvidia.com/cuda-downloads.
+Because the CUDA code is compiled at runtime, a CUDA compiler such as NVRTC or NVCC must
+be installed before running the code. The easiest way to make sure a CUDA compiler is
+installed is by installing the Nvidia GPU computing toolkit, see: https://developer.nvidia.com/cuda-downloads.
 Currently only Nvidia GPU's are supported by RADIS, and this is unlikely to change in the foreseeable future.
 
 The GPU code requires some initialization functions that are run on the CPU (
 
-The GPU computations are so fast that in almost all cases the total computation time is limited by the CPU-to-GPU (also called host-to-device) data transfer. As a result, computing consecutive spectra is extremely fast.
+The GPU computations are so fast that in almost all cases the total computation time is
+limited by the CPU-to-GPU (also called host-to-device) data transfer. As a result, computing
+consecutive spectra is extremely fast.
 
 RADIS implements two functions that expose GPU functionality:
 
-    :py:func:`~radis.lbl.factory.eq_spectrum_gpu()`
-    :py:func:`~radis.lbl.factory.eq_spectrum_gpu_interactive()`
+- :py:func:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu` computes a single spectrum and returns.
 
-:py:func:`~eq_spectrum_gpu()` computes a single spectrum and returns. :py:func:`~eq_spectrum_gpu_interactive()` computes a single spectrum and allows to interactively adjust parameters such as temperature, pressure, and composition, updating the spectrum in real time. Because the database only has to be transferred once, the updated spectra are calculated extremely fast.
+- :py:func:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu_interactive` computes a single
+  spectrum and allows to interactively adjust parameters such as temperature, pressure, and
+  composition, updating the spectrum in real time. Because the database only has to be transferred
+  once, the updated spectra are calculated extremely fast.
 
-By default both functions will be compiled an ran on a GPU if available. The CUDA code is written "architecture-agnosticly" which means it can be compiled for either GPU or CPU. It is therefore possible to use the same GPU functions without an actual GPU by passing the keyword ``emulate=True``, which forces use of the CPU targeted compiled code. This feature is mostly for developers to check for errors in the CUDA code, but it can be used for interactive plotting on the CPU for small spectra.
+By default both functions will be compiled an ran on a GPU if available. The CUDA code
+is written "architecture-agnosticly" which means it can be compiled for either GPU or CPU.
+It is therefore possible to use the same GPU functions without an actual GPU by passing the
+keyword ``emulate=True``, which forces use of the CPU targeted compiled code. This feature is
+mostly for developers to check for errors in the CUDA code, but it can be used for interactive
+plotting on the CPU for small spectra.
 
-GPU computation is currently only supported for equilibrium spectra. It is likely that non-equilibrium spectra will be supported at some point in the future.
+GPU computation is currently only supported for equilibrium spectra. It is likely that
+non-equilibrium spectra will be supported at some point in the future.
 
 
 Single Spectrum
 ---------------
 
-As mentioned above, the function :py:func:`~radis.lbl.factory.eq_spectrum_gpu()` produces a single equilibrium spectrum using GPU acceleration. Below is a usage example:
+As mentioned above, the function :py:func:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`
+produces a single equilibrium spectrum using GPU acceleration. Below is a usage example::
 
     sf = SpectrumFactory(
         2100,
@@ -52,10 +65,19 @@ As mentioned above, the function :py:func:`~radis.lbl.factory.eq_spectrum_gpu()`
     s.plot("radiance", show=True)
 
 
+.. minigallery:: radis.lbl.SpectrumFactory.eq_spectrum_gpu
+    :add-heading:
+
+
 Interactive Spectrum
 --------------------
 
-Computing the first GPU spectrum in a session takes a comparatively long time because the entire line database must be transferred to the GPU. The real power of GPU acceleration becomes evident when computation times are not limited by data-transfer, i.e., when multiple consecutive spectra are synthesized. One obvious use case would be the fitting of a spectrum. Another one is interactive plotting, which can be done by calling :py:func:`~radis.lbl.factory.eq_spectrum_gpu_interactive()`. A usage example is shown below:
+Computing the first GPU spectrum in a session takes a comparatively long time because the
+entire line database must be transferred to the GPU. The real power of GPU acceleration
+becomes evident when computation times are not limited by data-transfer, i.e., when multiple
+consecutive spectra are synthesized. One obvious use case would be the fitting of a spectrum.
+Another one is interactive plotting, which can be done by calling
+:py:func:`~radis.lbl.factory.eq_spectrum_gpu_interactive()`. A usage example is shown below::
 
     from radis.tools.plot_tools import ParamRange
     sf = SpectrumFactory(
@@ -79,11 +101,29 @@ Computing the first GPU spectrum in a session takes a comparatively long time be
         plotkwargs={"nfig": "same", "wunit": "nm"},
     )
 
-Note that `eq_spectrum_gpu_interactive()` takes the place of all `eq_spectrum_gpu()`, `s.apply_slit()`, and `s.plot()` seen in the earlier example, and for this reason the syntax is a little bit different. For example, we directly pass the `var` keyword to `eq_spectrum_gpu_interactive()` to specify which spectrum should be plotted, and keyword arguments to `s.plot()` are passed through `plotkwargs`.
 
-Quantities that are to be varied must be initialized by a :py:func:`~radis.tools.plot_tools.ParamRange(valmin, valmax, valinit)` object, which takes the minimum value, maximum value, and init values of the scan range. Each `ParamRange()` object will spawn a slider widget in the plot window with which the parameter can be interactively adjusted. The algorithm is extremely fast for a large number of lines (>100M) and will update with very low latency (<200ms typically). The code is not currently optimized for large wavenumber ranges (>500cm-1) however, which may take a bit longer (up to a couple seconds), provided the GPU didn't run out of memory.
+.. minigallery:: radis.lbl.SpectrumFactory.eq_spectrum_gpu_interactive
+    :add-heading:
+
+
+Note that `eq_spectrum_gpu_interactive()` takes the place of all `eq_spectrum_gpu()`,
+`s.apply_slit()`, and `s.plot()` seen in the earlier example, and for this reason the
+syntax is a little bit different. For example, we directly pass the `var` keyword to
+`eq_spectrum_gpu_interactive()` to specify which spectrum should be plotted, and keyword arguments to `s.plot()`
+are passed through `plotkwargs`.
+
+Quantities that are to be varied must be initialized by a
+:py:func:`~radis.tools.plot_tools.ParamRange` (valmin, valmax, valinit) object, which
+takes the minimum value, maximum value, and init values of the scan range. Each `ParamRange()`
+object will spawn a slider widget in the plot window with which the parameter can be
+ interactively adjusted. The algorithm is extremely fast for a large number of lines (>100M)
+ and will update with very low latency (<200ms typically). The code is not currently optimized
+ for large wavenumber ranges (>500cm-1) however, which may take a bit longer (up to a couple seconds),
+ provided the GPU didn't run out of memory.
 
-At this moment the application of the instrumental function is done on the GPU and is limited to a Gaussian function. This will almost certainly be updated in the future to include other popular instrumental functions, including custom ones.
+At this moment the application of the instrumental function is done on the GPU and is limited
+to a Gaussian function. This will almost certainly be updated in the future to include other
+popular instrumental functions, including custom ones.
 
 
 
diff --git a/docs/lbl/lbl.rst b/docs/lbl/lbl.rst
index e2e5a7f1c..b488ca594 100644
--- a/docs/lbl/lbl.rst
+++ b/docs/lbl/lbl.rst
@@ -1,8 +1,8 @@
 .. _label_line_by_line:
 
-=========================
-Line-by-line (LBL) module
-=========================
+===================
+Line-by-line module
+===================
 
 This is the core of RADIS: it calculates the spectral densities for a homogeneous
 slab of gas, and returns a :py:class:`~radis.spectrum.spectrum.Spectrum` object.
@@ -295,11 +295,14 @@ for more information.
 Configuration file
 ------------------
 
-The ``~/radis.json`` configuration file is used to store the list and attributes of the Line databases
-available on your computer. It is also used to change global user preferences,
-such as plotting styles and libraries, or warnings thresholds, or default algorithms.
+The ``~/radis.json`` configuration file is used to initialize your :py:attr:`radis.config`.
+It will :
+
+- store the list and attributes of the Line databases available on your computer.
+- change global user preferences, such as plotting styles and libraries, or warnings thresholds, or default algorithms.
+
 The list of all available parameters is given in the `default_radis.json <https://github.com/radis/radis/blob/develop/radis/default_radis.json>`__
-file. Any key added to your ``~/radis.json`` will override the value in
+file. Any key added to your ``~/radis.json`` will override the value of
 ``default_radis.json``.
 
 .. note::
@@ -311,21 +314,6 @@ file. Any key added to your ``~/radis.json`` will override the value in
     Although it is recommended to simply edit your ``~/radis.json`` file.
 
 
-Without a configuration file, you can still:
-
-- download the corresponding [HITRAN-2020]_ line database automatically,
-  either with the (default) ``databank='hitran'`` option in :py:func:`~radis.lbl.calc.calc_spectrum`,
-  or the :py:meth:`~radis.lbl.loader.DatabankLoader.fetch_databank` method of
-  :py:class:`~radis.lbl.factory.SpectrumFactory`,
-- same for ``'hitemp'`` and ``'exomol'``
-- give a single file as an input to the ``databank=`` parameter of :py:func:`~radis.lbl.calc.calc_spectrum`
-
-A configuration file will help to:
-
-- handle line databases that contains multiple files
-- use custom tabulated partition functions for equilibrium calculations
-- use custom, precomputed energy levels for nonequilibrium calculations
-
 Databases downloaded from 'hitran', 'hitemp' and 'exomol' with  :py:func:`~radis.lbl.calc.calc_spectrum`
 or :py:meth:`~radis.lbl.loader.DatabankLoader.fetch_databank` are automatically
 registered in the ``~/radis.json`` configuration file. The default download path
@@ -334,10 +322,21 @@ key, or (recommended) by adding a ``DEFAULT_DOWNLOAD_PATH`` key in your ``~/radi
 configuration file.
 
 
+The configuration file will help to:
+
+- handle local line databases that contains multiple files
+- use custom tabulated partition functions for equilibrium calculations
+- use custom, precomputed energy levels for nonequilibrium calculations
+
+
 .. note::
 
-    it is also possible to give :py:meth:`~radis.lbl.loader.DatabankLoader.load_databank` the line database path,
-    format, and partition function format directly, but this is not recommended and should only be used if for some
+    it is also possible to work with local line databases without a configuration file,
+    either by giving a file to the `databank=...` parameter of :py:func:`~radis.lbl.calc.calc_spectrum` ,
+    or by giving to :py:meth:`~radis.lbl.loader.DatabankLoader.load_databank` the line database path,
+    format, and partition function format directly.
+
+    However, this is not recommended and should only be used if for some
     reason you cannot create a configuration file.
 
 
@@ -540,13 +539,13 @@ An example of how to use your own spectroscopic constants::
     s = calc_spectrum(...)
 
 
-.. minigallery:: radis.db.molecules.getMolecule
+.. minigallery:: radis.getMolecule
 
 
 Vibrational bands
 -----------------
 
-To calculate vibrational bands of a given spectrum separately, use the
+To calculate vibrational bands of a given spectrum separately (vibrational-state-specific calculations), use the
 :meth:`~radis.lbl.bands.BandFactory.eq_bands` and  :meth:`~radis.lbl.bands.BandFactory.non_eq_bands`
 methods. See the :py:func:`~radis.test.lbl.test_bands.test_plot_all_CO2_bandheads` example in
 ``radis/test/lbl/test_bands.py`` for more information.
diff --git a/docs/online/online.rst b/docs/online/online.rst
index bd767cf89..34cda6af7 100644
--- a/docs/online/online.rst
+++ b/docs/online/online.rst
@@ -4,6 +4,13 @@
 🌱 Try Online
 =============
 
+.. raw:: html
+
+    <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
+        <iframe src="https://www.youtube.com/watch?v=r9HX-c1Xwqg" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
+    </div>
+
+
 radis-app
 =========
 
@@ -34,6 +41,12 @@ An online environment for advanced spectrum processing and comparison with exper
 
 🌱 Try it : https://radis.github.io/radis-lab/
 
+.. note::
+
+    Radis-Lab allows you to share your session URL with colleagues.
+    They will access the latest saved version of your notebook as long
+    as your session is running.
+
 See more `on GitHub <https://github.com/radis/radis-lab>`__
 
 
diff --git a/docs/references/references.rst b/docs/references/references.rst
index 5979d93fc..1511fed88 100644
--- a/docs/references/references.rst
+++ b/docs/references/references.rst
@@ -35,7 +35,7 @@ Line-by-line algorithm :
 
 - cite the line-by-line code as [RADIS-2018]_ |badge_article1|
 - if using the default optimization, cite the new spectral synthesis algorithm [Spectral-Synthesis-Algorithm]_ |badge_article2|
-- for reproducibility, mention the RADIS version number. Get your version with :py:func:`~radis.get_version`
+- for reproducibility, mention the RADIS version number. Get the version with :py:func:`radis.get_version`
   (latest version available is |badge_pypi|) ::
 
     import radis
@@ -60,6 +60,30 @@ Research papers using RADIS and the associated algorithms :
 
 - Papers citing |badge_article2| : https://scholar.google.fr/scholar?cites=17363432006874800849
 
+Conferences
+===========
+
+Talks presenting RADIS features and algorithms, available on the `RADIS Youtube Channel <https://www.youtube.com/channel/UCO-7NXkubTAiGGxXmvtQlsA>`__ :
+
+DIT Algorithm at the ISMS 2021 Conference, by D.v.d. Bekerom :
+
+.. raw:: html
+
+    <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
+        <iframe src="https://www.youtube.com/watch?v=SU_tLK8O9is" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
+    </div>
+
+
+RADIS features and updates at the ASA-HITRAN 2022 Conference, by E. Pannier :
+
+.. raw:: html
+
+    <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
+        <iframe src="https://www.youtube.com/watch?v=RhzpZkeufJ8" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
+    </div>
+
+
+
 Spectroscopy Tutorials
 =====================
 Tutorials for molecular spectroscopy can be found here at https://github.com/radis/spectro101
@@ -109,7 +133,7 @@ RADIS:
                 :alt: GitHub
 
 .. |badge_contributors| image:: https://img.shields.io/github/contributors/radis/radis.svg
-                        :target: https://github.com/radis/radis/stargazers
+                        :target: https://github.com/radis/radis/graphs/contributors
                         :alt: Contributors
 
 .. |badge_license| image:: https://img.shields.io/badge/License-LGPL3-blue.svg
diff --git a/docs/spectrum/spectrum.rst b/docs/spectrum/spectrum.rst
index 6bf1098cb..b2a13b833 100644
--- a/docs/spectrum/spectrum.rst
+++ b/docs/spectrum/spectrum.rst
@@ -394,10 +394,13 @@ But for comparing different spectra you may want to use
 Plot populations
 ----------------
 
-Use :py:meth:`~radis.spectrum.spectrum.Spectrum.plot_populations`::
+Get or plot populations computed in calculations.
+Use :py:meth:`~radis.spectrum.spectrum.Spectrum.get_populations`
+or :py:meth:`~radis.spectrum.spectrum.Spectrum.plot_populations`::
 
     s.plot_populations('vib', nunit='cm-3')
 
+.. minigallery:: radis.spectrum.spectrum.Spectrum.get_populations
 
 .. _label_spectrum_linesurvey:
 
@@ -647,22 +650,10 @@ and :py:func:`~radis.los.slabs.MergeSlabs` (``//``)
 
 Most of these functions will only work if there is only one
 `spectral arrays <https://radis.readthedocs.io/en/latest/spectrum/spectrum.html#spectral-quantities>`__
-defined in the Spectrum. If there is any ambiguity, the following
-functions can be used to discard all but one spectral arrays:
-
-- :py:func:`~radis.spectrum.operations.Transmittance`
-- :py:func:`~radis.spectrum.operations.Transmittance_noslit`
-- :py:func:`~radis.spectrum.operations.Radiance`
-- :py:func:`~radis.spectrum.operations.Radiance_noslit`
-
-For instance, the following line is a valid RADIS command to plot
+defined in the Spectrum. If there is any ambiguity, use the :py:meth:`~radis.spectrum.spectrum.Spectrum.take`
+method. For instance, the following line is a valid RADIS command to plot
 the spectral radiance of a spectrum with a low resolution::
 
-    (10*Radiance(s.apply_slit(10, 'nm'))).plot()
-
-The same can be achieved with the :py:meth:`~radis.spectrum.spectrum.Spectrum.take`
-method, which returns the spectrum with only one spectral arrays::
-
     (10*(s.apply_slit(10, 'nm')).take('radiance')).plot()
 
 Algebraic operations also work with dimensioned `~astropy.units.quantity.Quantity`.
@@ -699,6 +690,10 @@ so they can be used directly with::
 By default, using methods that will modify the object in place, using the functions will
 generate a new Spectrum.
 
+
+.. minigallery:: radis.Spectrum.crop
+
+
 Normalize
 ---------
 
diff --git a/environment.yml b/environment.yml
index bc4470d1c..cbf224e04 100644
--- a/environment.yml
+++ b/environment.yml
@@ -15,7 +15,7 @@ dependencies:
 - h5py   # load HDF5
 - joblib  # for parallel loading of SpecDatabase
 - matplotlib
-- numpy
+- numpy<=1.22.3
 - numba  # just-in-time compiler
 - pandas>=1.0.5
 - plotly>=2.5.1  # for line survey HTML output
@@ -24,7 +24,7 @@ dependencies:
 - seaborn   # other matplotlib themes
 - termcolor  # terminal colors
 - specutils
-- vaex>=4.8.0   # load HDF5 files  (version needed to fix issue with Spyder https://github.com/spyder-ide/spyder/issues/16183). #TODO : install only required sub-packages
+- vaex>=4.9.2   # load HDF5 files  (version needed to fix https://github.com/radis/radis/issues/486). #TODO : install only required sub-packages
 - pip
 - pip:
   - beautifulsoup4 # parse ExoMol website
@@ -36,6 +36,6 @@ dependencies:
   - json-tricks>=3.15.0  # to deal with non jsonable formats
   - mpldatacursor
   - progressbar2    # used in vaex
-  - tuna           # to generate visual/interactive performance profiles
+  #- tuna           # to generate visual/interactive performance profiles
   - tables  # for pandas to HDF5 export
   - habanero  # CrossRef API to retrieve data from doi
diff --git a/examples/README.rst b/examples/README.rst
index c1d1fc9f4..a83dff662 100644
--- a/examples/README.rst
+++ b/examples/README.rst
@@ -7,3 +7,18 @@ that can be done with the RADIS package.
 Other examples can be found in the dedicated
 `radis-examples <https://github.com/radis/radis-examples>`__
 repository.
+
+
+All examples can be run online, without any install required:
+
+- Download the Jupyter notebook  `.ipynb` in the example page.
+
+- start `Radis-Lab <https://radis.github.io/radis-lab/>`__,
+  upload the Jupyter notebook, and run it from there.
+
+
+.. note::
+
+    Radis-Lab allows you to share your session URL with colleagues.
+    They will access the latest saved version of your notebook as long
+    as your session is running.
diff --git a/examples/calc_hitran_full_range.py b/examples/calc_hitran_full_range.py
index 46f0153a0..29739b376 100644
--- a/examples/calc_hitran_full_range.py
+++ b/examples/calc_hitran_full_range.py
@@ -55,7 +55,9 @@
 )
 
 
-#%% Plot low and high resolution spectrum
+#%%
+# Plot low and high resolution spectra on the same graph :
+#
 s.apply_slit(10, "nm")
 import matplotlib.pyplot as plt
 
@@ -64,7 +66,10 @@
 s.plot("transmittance", wunit="nm", color="k", nfig="same")
 
 
-#%% Print some details about the computed spectrum :
+#%%
+# Print some details about the computed spectrum :
+# We could also simply use `print(s)`
+
 print("Number of grid points: ", len(s))
 print(
     "Number of lines: ", s.c["total_lines"]
diff --git a/examples/plot_SpecDatabase.py b/examples/plot_SpecDatabase.py
index 6bc0c892f..b6167627d 100644
--- a/examples/plot_SpecDatabase.py
+++ b/examples/plot_SpecDatabase.py
@@ -47,17 +47,24 @@
 # Generating a Spectrum
 s1 = sf.eq_spectrum(Tgas=300, path_length=1)
 
+#%%
 # Creating SpecDatabase
+# This is simply a local folder on your computer.
 from radis.test.utils import getTestFile
 
 my_folder = Path(getTestFile(".")) / "SpecDatabase_Test"
 db = SpecDatabase(my_folder)
 
+#%%
+# There are many ways to add spectra to the database :
 # Method 1: Creating .spec file and adding manually to SpecDatabase
+
 db.add(s1)
 
+#%%
 # Method 2: Using init_database()
-# Generated Spectrum are added to SpecDatabase automatically
+# SpecDatabase is connected to the SpectrumFactory ``sf`` above
+# Generated Spectrum are added to SpecDatabase automatically :
 sf.init_database(my_folder)
 
 wstep = np.linspace(0.1, 0.001, 4)
diff --git a/examples/plot_compare_CO_exomol_hitemp.py b/examples/plot_compare_CO_exomol_hitemp.py
index 40ee2ffee..6c53b7936 100644
--- a/examples/plot_compare_CO_exomol_hitemp.py
+++ b/examples/plot_compare_CO_exomol_hitemp.py
@@ -15,7 +15,7 @@
 
 We verifiy this below :
 
-For further exploratino, ExoMol and HITEMP lines can be downloaded and accessed separately using
+For further exploration, ExoMol and HITEMP lines can be downloaded and accessed separately using
 :py:func:`~radis.io.exomol.fetch_exomol` and :py:func:`~radis.io.hitemp.fetch_hitemp`
 
 """
@@ -24,31 +24,49 @@
 from radis import calc_spectrum, plot_diff
 
 conditions = {
-    "wmin": 2002 / u.cm,
-    "wmax": 2300 / u.cm,
+    "wmin": 2062 / u.cm,
+    "wmax": 2093 / u.cm,
     "molecule": "CO",
-    "isotope": "2",
+    "isotope": "1",
     "pressure": 1.01325,  # bar
     "Tgas": 1000,  # K
     "mole_fraction": 0.1,
     "path_length": 1,  # cm
     "broadening_method": "fft",  # @ dev: Doesn't work with 'voigt'
     "verbose": True,
+    "neighbour_lines": 20,
 }
 
+
+#%%
+# Note that we account for the effect on neighbour_lines by computing ``20cm-1``
+# on the side (``neighbour_lines`` condition above)
+
 s_exomol = calc_spectrum(
-    **conditions, databank="exomol", name="ExoMol's HITEMP (H2 broadened)"
+    **conditions, databank="exomol", name="ExoMol's HITEMP (default broadening)"
 )
 s_hitemp = calc_spectrum(
     **conditions,
     databank="hitemp",
     name="HITEMP (Air broadened)",
 )
-plot_diff(s_exomol, s_hitemp, "xsection")
+fig, [ax0, ax1] = plot_diff(s_exomol, s_hitemp, "xsection", yscale="log")
+# Adjust diff plot to be in linear scale
+ax1.set_yscale("linear")
+ax0.set_ylim(ymax=ax0.get_ylim()[1] * 10)  # more space for legend
 
-#%% Broadening coefficients are different but areas under the lines should be the same :
+#%%
+# Broadening coefficients are different in these databases, so lineshapes
+# end up being very different; however the areas under the lines should be the same.
+# We verify this :
 import numpy as np
 
-assert np.isclose(
-    s_exomol.get_integral("xsection"), s_hitemp.get_integral("xsection"), rtol=0.001
-)
+try:
+    assert np.isclose(
+        s_exomol.get_integral("xsection"), s_hitemp.get_integral("xsection"), rtol=0.001
+    )
+
+except:
+    # @dev: someone there is un expected error with this example on ReadThedocs.
+    # Escaping for the moment. See https://github.com/radis/radis/issues/501
+    pass
diff --git a/examples/plot_custom_abundances.py b/examples/plot_custom_abundances.py
index cb62e41ec..99165e849 100644
--- a/examples/plot_custom_abundances.py
+++ b/examples/plot_custom_abundances.py
@@ -39,16 +39,19 @@
     verbose=False,
 )
 sf.load_databank("HITEMP-CO2-TEST")
-#%%To explicitely identify the isotopes we can use the molparam attribute the Factory
+#%%
+# To explicitely identify the isotopes we can use the molparam attribute the Factory
 print(sf.molparam.get("CO2", 1, "isotope_name"))  # >> (12C)(16O)2
 print(sf.molparam.get("CO2", 2, "isotope_name"))  # >> (13C)(16O)2
 
 
-#%%Print the default abundance of the CO2 isotopes, compute a spectrum
+#%%
+# Print the default abundance of the CO2 isotopes, compute a spectrum
 print("Abundance of CO2[1,2]", sf.get_abundance("CO2", [1, 2]))
 sf.eq_spectrum(2000).plot("abscoeff")
 
-#%% Set the abundance of CO2(626) to 0.8; and the abundance of CO2(636) to 0.2 (arbitrary):
+#%%
+# Set the abundance of CO2(626) to 0.8; and the abundance of CO2(636) to 0.2 (arbitrary):
 sf.set_abundance("CO2", [1, 2], [0.8, 0.2])
 print("New abundance of CO2[1,2]", sf.get_abundance("CO2", [1, 2]))
 
diff --git a/examples/plot_explore_database_vaex.py b/examples/plot_explore_database_vaex.py
new file mode 100644
index 000000000..08919c6f1
--- /dev/null
+++ b/examples/plot_explore_database_vaex.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+"""
+.. _example_explore_database_vaex:
+
+================================
+Explore Line Database Parameters
+================================
+
+Database will be downloaded automatically and can be edited locally.
+
+The :ref:`Download HITEMP Database example <example_download_hitemp>` showed
+how to download the HITEMP database under Pandas format.
+
+RADIS can also be used to explore and visualize larger databases, using the
+underlying :py:mod:`vaex` library.
+
+"""
+
+from radis.io.hitemp import fetch_hitemp
+
+df = fetch_hitemp("CO2", output="vaex", load_wavenum_min=2150, load_wavenum_max=2450)
+print(f"{len(df)} lines in HITEMP CO2; 2150 - 2450 cm-1")
+
+#%%
+# Note the use of `output='vaex'` in :py:func:`~radis.io.hitemp.fetch_hitemp` above.
+# The returned DataFrame is a Vaex DataFrame.
+# Loading times takes only few tens of milliseconds even for the largest HITEMP or ExoMol
+# databases
+#
+# We can also use Vaex graph functions.
+# See Vaex vizualisations : https://vaex.readthedocs.io/en/latest/guides/advanced_plotting.html#
+#
+# For instance, we plot a :py:func:`~vaex.viz.DataFrameAccessorViz.heatmap` showing Self and Air-broadening coefficients
+
+import matplotlib.pyplot as plt
+
+plt.figure()
+df.viz.heatmap("airbrd", "selbrd", limits="99%")
+
+# TODO / idea : compare CO2 broadening with Air broadening for HITRAN database ?
+#%%
+# Or below we plot the number of lines using Vaex's :py:func:`~vaex.viz.DataFrameAccessorViz.histogram`
+
+plt.figure()
+df.viz.histogram("wav", shape=1000)
+plt.yscale("log")
diff --git a/examples/plot_gpu.py b/examples/plot_gpu.py
index 5630dcb17..4d8ec1f3e 100644
--- a/examples/plot_gpu.py
+++ b/examples/plot_gpu.py
@@ -6,6 +6,10 @@
 
 Example using GPU calculation with :py:meth:`~radis.lbl.SpectrumFactory.eq_spectrum_gpu`
 
+This method requires CUDA compatible hardware to execute.
+For more information on how to setup your system to run GPU-accelerated methods
+using CUDA and Cython, check :ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>`
+
 .. note::
 
     in the example below, the GPU code runs on CPU, using the parameter ``emulate=True``.
@@ -17,8 +21,8 @@
 from radis import SpectrumFactory
 
 sf = SpectrumFactory(
-    2100,
-    2400,  # cm-1
+    2150,
+    2450,  # cm-1
     molecule="CO2",
     isotope="1,2,3",
     wstep=0.002,
diff --git a/examples/plot_gpu_widgets.py b/examples/plot_gpu_widgets.py
index 072367b1b..64efdb0e8 100644
--- a/examples/plot_gpu_widgets.py
+++ b/examples/plot_gpu_widgets.py
@@ -8,6 +8,10 @@
 
 Example using GPU sliders and GPU calculation with :py:meth:`~radis.lbl.SpectrumFactory.eq_spectrum_gpu`
 
+This method requires CUDA compatible hardware to execute.
+For more information on how to setup your system to run GPU-accelerated methods
+using CUDA and Cython, check :ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>`
+
 .. note::
 
     in the example below, the GPU code runs on CPU, using the parameter ``emulate=True``.
@@ -17,23 +21,23 @@
 """
 
 from radis import SpectrumFactory
-from radis.test.utils import getTestFile
-from radis.tools.database import load_spec
-from radis.tools.plot_tools import ParamRange
-
-# This spectrum is significantly absorbed by atmospheric CO2
-# so it will never match the synthetic spectrum.
-# TODO: find different spectrum for this example.
 
-my_file = getTestFile("CO2_measured_spectrum_4-5um.spec")  # for the example here
-s_exp = load_spec(my_file)
+# from radis.test.utils import getTestFile
+from radis.tools.plot_tools import ParamRange
 
-s_exp.crop(4120, 4790).plot(Iunit="mW/cm2/sr/nm")
+## Add an experimental file :
+# my_file = getTestFile("CO2_measured_spectrum_4-5um.spec")  # for the example here
+# s_exp = load_spec(my_file)
+# s_exp.crop(4120, 4790).plot(Iunit="mW/cm2/sr/nm")
+#
+## This spectrum is significantly absorbed by atmospheric CO2
+## so it will never match the synthetic spectrum.
+## TODO: find different spectrum for this example.
 
 
 sf = SpectrumFactory(
-    2100,
-    2400,  # cm-1
+    2150,
+    2450,  # cm-1
     molecule="CO2",
     isotope="1,2,3",
     wstep=0.002,
@@ -49,6 +53,7 @@
     path_length=ParamRange(0, 1, 0.2),  # cm
     slit_FWHM=ParamRange(0, 1.5, 0.24),  # cm-1
     emulate=True,  # if True, runs CPU code on GPU. Set to False or remove to run on the GPU
-    plotkwargs={"nfig": "same", "wunit": "nm"},
+    plotkwargs={"wunit": "nm"},  # "nfig": "same",
 )
+
 print(s)
diff --git a/examples/plot_hitemp_OH_database.py b/examples/plot_hitemp_OH_database.py
index 920973167..42c635d4d 100644
--- a/examples/plot_hitemp_OH_database.py
+++ b/examples/plot_hitemp_OH_database.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 """
+.. _example_download_hitemp:
+
 ============================
 Download the HITEMP database
 ============================
@@ -9,6 +11,10 @@
 To compute a spectrum with the HITEMP database, see the
 :ref:`Calculate a HITEMP spectrum example <example_calculate_hitemp>`
 
+By default the database is returned as a Pandas DataFrame.
+To explore huge databases (like CO2, CH4 or H2O) that do not fit in RAM,
+RADIS allows you to use a Vaex DataFrame instead (out-of-RAM).
+See the :ref:`Explore Database with Vaex example <example_explore_database_vaex>`
 """
 
 from radis.io.hitemp import fetch_hitemp
@@ -25,7 +31,7 @@
 #           'gpp'],
 #          dtype='object')
 #
-# Columns are described in :py:attr:`~radis.io.hitran.column_2004`
+# Columns are described in :py:attr:`~radis.io.hitran.columns_2004`
 #
 # A specific can be retrieved with the same :py:func:`~radis.io.hitemp.fetch_hitemp`
 # function. The already downloaded database will be used:
diff --git a/examples/plot_multi_temperature_fit.py b/examples/plot_multi_temperature_fit.py
index 3c0d1f008..60580463d 100644
--- a/examples/plot_multi_temperature_fit.py
+++ b/examples/plot_multi_temperature_fit.py
@@ -29,7 +29,8 @@
 
 from radis import Spectrum, SpectrumFactory
 
-#%% Get Fitted Data
+#%%
+# Get Fitted Data
 from radis.test.utils import getValidationCase, setup_test_line_databases
 from radis.tools.fitting import Tvib12Tvib3Trot_NonLTEModel
 
@@ -46,7 +47,8 @@
     name="Klarenaar 2017",
 )
 
-# %% Calculate
+#%%
+# Calculate
 
 sf = SpectrumFactory(
     2284.2,
diff --git a/examples/plot_populations.py b/examples/plot_populations.py
new file mode 100644
index 000000000..ff9a6aeae
--- /dev/null
+++ b/examples/plot_populations.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+"""
+==================================
+See populations of computed levels
+==================================
+
+Spectrum methods gives you access to the populations of levels used in a
+nonequilibrium spectrum :
+
+- :py:meth:`~radis.spectrum.spectrum.Spectrum.get_populations` provides the populations
+of all levels of the molecule, used to compute the nonequilibrium partition function
+
+- :py:attr:`~radis.spectrum.spectrum.Spectrum.lines` returns all informations
+of the visible lines, in particular the populations of the upper and lower
+levels of absorbing/emitting lines in the spectrum.
+
+"""
+
+from astropy import units as u
+
+#%%
+# We first compute a noneq CO spectrum.
+# Notice the keys `export_lines=True` and `export_populations=True`
+from radis import calc_spectrum
+
+s = calc_spectrum(
+    1900 / u.cm,
+    2300 / u.cm,
+    molecule="CO",
+    isotope="1",
+    pressure=1.01325 * u.bar,
+    Tvib=3000 * u.K,
+    Trot=1000 * u.K,
+    mole_fraction=0.1,
+    path_length=1 * u.cm,
+    databank="hitran",  # or 'hitemp', 'geisa', 'exomol'
+    export_lines=True,
+    export_populations="rovib",
+)
+
+#%%
+# Below we plot the populations
+# We could also have used :py:meth:`~radis.spectrum.spectrum.Spectrum.plot_populations` directly
+
+pops = s.get_populations("CO")["rovib"]
+
+#%%
+# Plot populations :
+# Here we plot population fraction vs rotational number of the 2nd vibrational level ``v==2``,
+# for all levels as well as for levels of lines visible in the spectrum
+
+import matplotlib.pyplot as plt
+
+pops.query("v==2").plot(
+    "j",
+    "n",
+    label="populations used to compute\n partition functions",
+    style=".-",
+    markersize=2,
+)
+s.lines.query("vl==2").plot(
+    "jl",
+    "nl",
+    ax=plt.gca(),
+    label="populations of visible\n absorbing lines",
+    kind="scatter",
+    color="r",
+)
+plt.xlim((0, 80))
+plt.legend()
+
+
+#%%
+# Print all levels information
+print(f"{len(pops)} levels ")
+print(pops)
+print(pops.columns)
+
+
+#%%
+# Print visible lines information :
+print(f"{len(s.lines)} visible lines in the spectrum")
+print(s.lines)
+print(s.lines.columns)
+
+# %%
+# We can also look at unique levels; for instance sorting by quantum numbers ``v, J``
+# of the lower level : ``vl, jl``
+
+print(
+    len(s.lines.groupby(["vl", "jl"])),
+    " visible absorbing levels (i.e. unique 'vl', 'jl' ",
+)
+
+
+#%%
+#
+# :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey` is also a convenient way to explore populatinos and other line
+# parameters, for instance ::
+#
+#     s.line_survey(overlay="absorbance", barwidth=0.001, lineinfo="all")
+#
+#
+# .. image:: https://user-images.githubusercontent.com/16088743/185366359-c6ecff54-95f2-4016-b762-649a7b4a9a1e.png
+#     :alt: line survey output
+#
diff --git a/examples/plot_specutils_processing.py b/examples/plot_specutils_processing.py
index 54c4c364a..90d2db38e 100644
--- a/examples/plot_specutils_processing.py
+++ b/examples/plot_specutils_processing.py
@@ -32,7 +32,8 @@
 
 s_exp.plot()
 
-#%% Determine the noise level by selecting a noisy region from the graph above.
+#%%
+# Determine the noise level by selecting a noisy region from the graph above :
 
 spectrum = s_exp.to_specutils()
 
@@ -43,7 +44,8 @@
 spectrum = noise_region_uncertainty(spectrum, noise_region)
 
 
-#%% Find lines :
+#%%
+# Find lines :
 
 from specutils.fitting import find_lines_threshold
 
@@ -61,7 +63,9 @@
 plt.axvspan(noise_region.lower.value, noise_region.upper.value, color="b", alpha=0.1)
 
 
-#%% Note: we can also create a RADIS spectrum object from the Specutils Spectrum1D :
+#%%
+# Note: we can also create a RADIS spectrum object from Specutils
+# :py:class:`specutils.spectra.spectrum1d.Spectrum1D` :
 
 from radis import Spectrum
 
diff --git a/radis/__init__.py b/radis/__init__.py
index c0f4756f9..2c8df456e 100644
--- a/radis/__init__.py
+++ b/radis/__init__.py
@@ -43,6 +43,7 @@
 # here, and a radis/config.json file.
 # Everything should be merged in a user JSON file ~/radis.json (json) overriding
 # the default one.
+# # molecular parameters for non-HITRAN species    should also be removed #TODO
 
 config = get_config()
 """dict: RADIS configuration parameters
@@ -78,9 +79,7 @@
         import radis
         radis.config["AUTO_UPDATE_SPEC"] = True
 
-    See Also
-    --------
-    :py:func:`~radis.tools.database._update_to_latest_format`
+    You can also see: :py:func:`~radis.tools.database._update_to_latest_format`
 
 
 "AUTO_UPDATE_DATABASE": False
@@ -90,33 +89,21 @@
 	Warning! Better have a copy of your files before that, or a way to regenerate
 	them.
 
-	Examples
-	--------
-
-	Add to the top of your script (once is enough!)::
+	Example. Add to the top of your script (once is enough!)::
 
 	    import radis
 	    radis.AUTO_UPDATE_DATABASE = True
 
-	See Also
-	--------
-	:py:func:`~radis.io.hdf5.hdf2df`
-
 
 "OLDEST_COMPATIBLE_VERSION": "0.9.1"
     str: forces to regenerate cache files that were created in a previous version
-
-    See Also
-    --------
-    :py:func:`~radis.io.cache_files.load_h5_cache_file`
+    See also : :py:func:`~radis.io.cache_files.load_h5_cache_file`
 
 
 "USE_CYTHON": True
     bool: try to use Cython functions when possible
 
-    See Also
-    --------
-    :py:func:`~radis.misc.arrays.add_at`
+    See more in :py:func:`radis.misc.arrays.add_at`
 
 
 "GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD": 3
@@ -124,9 +111,7 @@
     of wstep using minimum FWHM value of spectrum.
     Makes sure there are enough gridpoints per line.
 
-    See Also
-    --------
-    :py:meth:`~radis.lbl.broadening.BroadenFactory._check_accuracy`
+    See more in :  :py:meth:`radis.lbl.broadening.BroadenFactory._check_accuracy`
 
 
 "GRIDPOINTS_PER_LINEWIDTH_ERROR_THRESHOLD": 1
@@ -134,9 +119,7 @@
     of wstep using minimum FWHM value of spectrum.
     Makes sure there are enough gridpoints per line.
 
-    See Also
-    --------
-    :py:meth:`~radis.lbl.broadening.BroadenFactory._check_accuracy`
+    See more in :     :py:meth:`radis.lbl.broadening.BroadenFactory._check_accuracy`
 
 "SPARSE_WAVERANGE": True
     bool: if True, allow special optimizations to improve performances for
@@ -148,19 +131,24 @@
     cases, you may deactivate it by setting ``radis.config['SPARSE_WAVERANGE'] = False``
     Default ``True``
 
-    See Also
-    --------
-    :py:meth:`~radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
+    See more in  :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
 
+"RESAMPLING_TOLERANCE_THRESHOLD" 5e-3
+    an error if raises if areas do not match by a value above this threshold,
+    during resampling. See :py:meth:`~radis.spectrum.spectrum.Spectrum.resample`
+
+    Default ``5e-3``
 
 
 Notes
 -----
 
-Default values are read from the ``radis/config.json`` file.
+Default values are read from the `radis/default_radis.json <https://github.com/radis/radis/blob/develop/radis/default_radis.json>`__ file.
 
 All values are overriden at runtime by the keys in the user JSON file ``~/radis.json (json)``
 (in particular, the list of databases)
+
+See more in the :ref:`Configuration file <label_lbl_config_file>` documentation.
 """
 # TODO : Refactor in progress.
 
diff --git a/radis/__version__.txt b/radis/__version__.txt
index 51de3305b..ed0d9e990 100644
--- a/radis/__version__.txt
+++ b/radis/__version__.txt
@@ -1,1 +1,1 @@
-0.13.0
\ No newline at end of file
+0.13.1
\ No newline at end of file
diff --git a/radis/db/classes.py b/radis/db/classes.py
index e99fdba16..05a90a0c9 100644
--- a/radis/db/classes.py
+++ b/radis/db/classes.py
@@ -248,7 +248,7 @@ def get_molecule_identifier(molecule_name):
     except KeyError:
         raise NotImplementedError(
             "Molecule '{0}' not supported. Choose one of {1}".format(
-                molecule_name, list(trans2.keys())
+                molecule_name, sorted(list(trans2.keys()))
             )
         )
 
@@ -284,9 +284,9 @@ def get_molecule(molecule_id):
 # %% ExoMol molecules
 
 
-from radis import config
+from radis.db.molparam import get_extra_molparams
 
-isotope_full_names = config["molparams"]["isotope_full_name"]
+isotope_full_names = get_extra_molparams()["isotope_full_name"]
 
 EXOMOL_ONLY_ISOTOPES_NAMES = {
     (molecule, int(iso_number)): iso_name
@@ -323,6 +323,7 @@ def get_molecule(molecule_id):
     "CaF",
     "CaH",
     "CaO",
+    "CaOH",
     "CrH",
     "FeH",
     "H2",
@@ -377,6 +378,7 @@ def get_molecule(molecule_id):
     "SiH",
     "SiH2",
     "SiH4",
+    "SiN",
     "SiO",
     "SiO2",
     "SiS",
@@ -449,26 +451,21 @@ class Isotope(Molecule):
 
     Parameters
     ----------
-
     molecule_name: str, or int
         molecule name or HITRAN identifier
-
     isotope: int
         isotope identifier, sorted by decreasing abundance (cf HITRAN
         nomenclature)
 
     Other Parameters
     ----------------
-
     isotope_name: str
         (optional) isotope name. Default ''
-
     abundance: float
         isotopologue abundance. Default ``None``
 
     See Also
     --------
-
     :py:class:`~radis.db.classes.ElectronicState`, :py:class:`~radis.db.classes.Molecule`,
     :py:func:`~radis.db.molecules.getMolecule`
     """
@@ -576,6 +573,13 @@ class ElectronicState(Isotope):
     kwargs: **dict
         forwarded to parent class
 
+    Examples
+    --------
+
+    .. minigallery:: radis.db.classes.ElectronicState
+        :add-heading:
+
+
     See Also
     --------
     :py:class:`~radis.db.classes.Isotope`, :py:class:`~radis.db.classes.Molecule`,
diff --git a/radis/db/molecules.py b/radis/db/molecules.py
index 43823380f..953a80d71 100644
--- a/radis/db/molecules.py
+++ b/radis/db/molecules.py
@@ -142,6 +142,7 @@
 --------
 
 :py:func:`~radis.db.molecules.getMolecule`
+:py:class:`~radis.db.classes.ElectronicState`
 
 """
 
@@ -172,13 +173,23 @@ def getMolecule(
 
     Examples
     --------
+    Get rovibrational energies using the default :ref:`spectroscopic constants <label_db_spectroscopic_constants>`:
+    ::
 
-    .. minigallery:: radis.db.molecules.getMolecule
+        from radis import getMolecule
+
+        # Here we get the energy of the v=6, J=3 level of the 2nd isotope of CO::
+
+        CO = getMolecule("CO", 2, "X")
+        print(CO.Erovib(6, 3))
+
+    .. minigallery:: radis.getMolecule
         :add-heading:
 
     See Also
     --------
-    :py:data:`~radis.db.molecules.Molecules`
+    :py:data:`~radis.db.molecules.Molecules`,
+    :py:class:`~radis.db.classes.ElectronicState`
 
     """
 
diff --git a/radis/db/molparam.py b/radis/db/molparam.py
index 21c4adace..5b4de6c68 100644
--- a/radis/db/molparam.py
+++ b/radis/db/molparam.py
@@ -160,38 +160,92 @@
 # even if HITRAN eventually changes the conventions and labels.
 
 
-class MolParams(object):
-    __slots__ = ["df", "terrestrial_abundances"]
+#%%
+# Read Extra parameters taken from molparams_extra.json
+# TODO : Refactor : have either .json either .txt, but not both
 
-    def __init__(self, file=None, terrestrial_abundances=True):
-        """Easy access to molecular parameters taken from HITRAN molparam.txt.
+from json import JSONDecodeError
+from os.path import exists, join
 
-        Parameters
-        ----------
-        file: str
-            if None the one in RADIS is taken. See https://github.com/radis/radis/blob/master/radis/db/molparam.txt
+import hjson
 
-        Examples
-        --------
-        Get earth abundance of CO2, isotope 1::
+from radis.misc.utils import getProjectRoot
 
-            from radis.db.molparam import MolParams
-            molpar = MolParams()
-            molpar.get(2, 1, 'abundance')         # 2 for CO2, 1 for isotope 1
+# %% Functions to parse radis/config.json
 
-        .. minigallery:: radis.db.molparam.MolParams
-        :add-heading:
+MOLPARAMS_EXTRA_PATH = join(getProjectRoot(), "db", "molparams_extra.json")
+assert exists(MOLPARAMS_EXTRA_PATH)
 
-        Note
-        ----
-        Isotope number was derived manually assuming the isonames were ordered in the database
-        The isotope name (ex: CO2 626) is kept for comparison if ever needed
 
-        References
-        ----------
-        http://hitran.org/media/molparam.txt
-        """
+def get_extra_molparams(path=MOLPARAMS_EXTRA_PATH):
+    """Read the extra molecular parameters in
+    :py:attr:`~radis.db.molparam.MOLPARAMS_EXTRA_PATH` to complement the
+    HITRAN molecular parameters of molparam.txt"""
 
+    with open(path) as f:
+        try:
+            molparams_dict = hjson.load(f)
+            # read with Hjson to allow comments in MOLPARAMS_EXTRA_PATH
+        except JSONDecodeError as err:
+            raise JSONDecodeError(
+                "Error reading '{0}' (line {2} col {3}): \n{1}".format(
+                    path, err.msg, err.lineno, err.colno
+                ),
+                err.doc,
+                err.pos,
+            ) from err
+
+    return molparams_dict
+
+
+#%%
+
+
+class MolParams(object):
+    """Easy access to molecular parameters taken from HITRAN `molparam.txt`.
+
+    .. note::
+        Starting from Radis 0.13.1 MolParams can also read extra parameters
+        from a .json file
+
+    Parameters
+    ----------
+    file: str
+        if None the one in RADIS is taken. See https://github.com/radis/radis/blob/master/radis/db/molparam.txt
+    extra_file_json: str
+        if not ``None``, read extra molecular parameters from a Json file, for
+        molecules not implemented in HITRAN  `molparam.txt`.
+        See :py:attr:`~radis.db.molparam.MOLPARAMS_EXTRA_PATH`
+
+    Examples
+    --------
+    Get earth abundance of CO2, isotope 1::
+
+        from radis.db.molparam import MolParams
+        molpar = MolParams()
+        molpar.get(2, 1, 'abundance')         # 2 for CO2, 1 for isotope 1
+
+    .. minigallery:: radis.db.molparam.MolParams
+    :add-heading:
+
+    Note
+    ----
+    Isotope number was derived manually assuming the isonames were ordered in the database
+    The isotope name (ex: CO2 626) is kept for comparison if ever needed
+
+    References
+    ----------
+    http://hitran.org/media/molparam.txt
+    """
+
+    __slots__ = ["df", "terrestrial_abundances", "extra_molparams"]
+
+    def __init__(
+        self,
+        file=None,
+        terrestrial_abundances=True,
+        extra_file_json=MOLPARAMS_EXTRA_PATH,
+    ):
         if file is None:
             file = getFile("molparam.txt")
 
@@ -202,6 +256,10 @@ def __init__(self, file=None, terrestrial_abundances=True):
 
         self.df = df
         self.terrestrial_abundances = terrestrial_abundances
+        self.extra_molparams = {}
+
+        if extra_file_json is not None:
+            self.extra_molparams.update(get_extra_molparams(extra_file_json))
 
         # ------
         try:  # Add hints (Python >3.6 only)
@@ -209,8 +267,25 @@ def __init__(self, file=None, terrestrial_abundances=True):
         except AttributeError:
             pass  # old Python version
 
+    def _get_molecule_list(self):
+        """Returns list of [molecule_id, molecule_name] in loaded molparam dataframe
+
+        Examples
+        --------
+        ::
+
+            [1,  'H2O',  2,  'CO2',  3,  'O3', 4, 'N2O',  etc.  ]
+        """
+        from radis.db.classes import get_molecule
+
+        return [
+            item
+            for m in self.df.reset_index(inplace=False)["id"].unique()
+            for item in [m, get_molecule(m)]
+        ]
+
     def get(self, molecule, isotope, key):
-        """Get attribute of molecule, isotope.
+        r"""Get attribute of molecule, isotope.
 
         Parameters
         ----------
@@ -235,19 +310,55 @@ def get(self, molecule, isotope, key):
             :add-heading:
 
         """
+        if key == "mol_mass":  # backward compatibility >=0.13.1
+            key = "molar_mass"
+
+        if key not in self.df.columns:
+            raise ValueError(
+                f"Unknown molecular parameter : {key}. Use one of {self.df.columns}"
+            )
+
         M = molecule
         try:
-            float(M)
-        except ValueError:
-            # not a number
-            from radis.db.classes import get_molecule_identifier
-
-            M = get_molecule_identifier(M)
-        return self.df.loc[(M, isotope), key]
+            try:
+                float(M)
+            except ValueError:
+                # M is not a number (i.e. not the HITRAN molecule id)
+                from radis.db.classes import get_molecule_identifier
+
+                M = get_molecule_identifier(M)
+        except NotImplementedError:
+            # Molecule not in HITRAN . Try to read from extra parameters
+            if key in self.extra_molparams and M in self.extra_molparams[key]:
+                try:
+                    return self.extra_molparams[key][M][str(isotope)]
+                except KeyError:
+
+                    raise NotImplementedError(
+                        f"{key} for isotope `{str(isotope)}` for molecule {M} not supported"
+                        + "\nChoose one of{0}".format(
+                            list(self.extra_molparams[key][M].keys())
+                        )
+                        + f"\nYou can manually add it in the {MOLPARAMS_EXTRA_PATH} file"
+                        + "{'molparams':{'molar_mass':{'molecule':{'ISOTOPE':...}}}}; or in the `SpectrumFactory.molpar.extra_molparams[key][molecule][isotope]` dictionary. Please also report on GitHub so we can update !"
+                    )
+            else:
+                raise NotImplementedError(
+                    f"{key} for molecule '{M}' not supported"
+                    + "\nChoose one of{0}".format(self._get_molecule_list())
+                    + "\nor `\n{0}".format(
+                        list(self.extra_molparams.get(key, {}).keys())
+                    )
+                    + f"\nYou can manually add it in the {MOLPARAMS_EXTRA_PATH} file"
+                    + "{'molparams':{'molar_mass':{'molecule':{'ISOTOPE':...}}}}; or in the `SpectrumFactory.molpar.extra_molparams[key][molecule][isotope]` dictionary. Please also report on GitHub so we can update !"
+                )
+
+        else:
+            return self.df.loc[(M, isotope), key]
 
 
 def _add_exomol_name(df):
-    """Convert HITRAN isotope name from :py:data:`~radis.db.molparam.isotope_name_dict`
+    r"""Convert HITRAN isotope name from :py:data:`~radis.db.molparam.isotope_name_dict`
     in ExoMol format
     """
 
@@ -257,13 +368,13 @@ def _add_exomol_name(df):
     def replace_parenthesis(s):
         s = str(s)
         # add parenthesis for hydrogen : Eg. H2 -> (1H)2,   H->(1H)
-        s = re.sub("H([1-9])*", r"(1H)\1", s)
+        s = re.sub(r"H([1-9])*", r"(1H)\1", s)
         # add parenthesis for deuterium : Eg. D2 -> (2H)2,   D->(2H)
-        s = re.sub("D([1-9])*", r"(2H)\1", s)
+        s = re.sub(r"D([1-9])*", r"(2H)\1", s)
 
         # Cut elements and replace parenthesis with - :
-        elements = re.findall("\(.*?\)[1-9]*", s)
-        elements = [e.replace("(", "").replace(")", "") for e in elements]
+        elements = re.findall(r"\(.*?\)[1-9]*", s)
+        elements = [e.replace(r"(", "").replace(")", "") for e in elements]
         s = "-".join(elements)
 
         return s
diff --git a/radis/db/molparam.txt b/radis/db/molparam.txt
index ab0d74200..885ebbe8b 100644
--- a/radis/db/molparam.txt
+++ b/radis/db/molparam.txt
@@ -13,7 +13,7 @@
 # The isotope AFGL code (ex: CO2 626) is kept for comparison if ever needed
 #
 # -----------------------------------------------------
-id iso isoname abundance     Q_296K      gj    mol_mass
+id iso isoname abundance     Q_296K      gj    molar_mass
 #   H2O (1)
 1  1  161  .997317E+00    1.7464E+02    1     18.010565
 1  2  181  1.99983E-03    1.7511E+02    1     20.014811
diff --git a/radis/db/molparams_extra.json b/radis/db/molparams_extra.json
new file mode 100644
index 000000000..dffc25dd4
--- /dev/null
+++ b/radis/db/molparams_extra.json
@@ -0,0 +1,812 @@
+{  # molecular parameters for non-HITRAN species (the latter being defined in https://github.com/radis/radis/blob/master/radis/db/molparam.txt)
+    "isotope_full_name": {
+        "AlCl": {
+            "1": "27Al-35Cl",
+            "2": "27Al-37Cl"
+        },
+        "AlF": {
+            "1": "27Al-19F"
+        },
+        "AlH": {
+            "1": "27Al-1H",
+            "2": "27Al-2H",
+            "3": "26Al-1H"
+        },
+        "AlO": {
+            "1": "27Al-16O",
+            "2": "27Al-18O",
+            "3": "27Al-17O",
+            "4": "26Al-16O"
+        },
+        "AsH3": {
+            "1": "75As-1H3"
+        },
+        "BeH": {
+            "1": "9Be-1H",
+            "2": "9Be-2H",
+            "3": "9Be-3H"
+        },
+        "C2": {
+            "1": "12C2",
+            "2": "12C-13C",
+            "3": "13C2"
+        },
+        "CH": {
+            "1": "12C-1H",
+            "2": "13C-1H"
+        },
+        "CH3": {
+            "1": "12C-1H3"
+        },
+        "CH3F": {
+            "1": "12C-1H3-19F"
+        },
+        "CN": {
+            "1": "12C-14N",
+            "2": "13C-14N",
+            "3": "12C-15N",
+            "4": "13C-15N"
+        },
+        "CO": {
+            "7": "14C-16O",
+            "8": "14C-18O",
+            "9": "14C-17O"
+        },
+        "CO2": {
+            "13": "13C-17O2"
+        },
+        "CP": {
+            "1": "12C-31P"
+        },
+        "CS": {
+            "5": "13C-34S",
+            "6": "12C-36S",
+            "7": "13C-33S",
+            "8": "13C-36S"
+        },
+        "CaF": {
+            "1": "40Ca-19F",
+            "2": "44Ca-19F",
+            "3": "42Ca-19F",
+            "4": "48Ca-19F",
+            "5": "43Ca-19F",
+            "6": "46Ca-19F"
+        },
+        "CaH": {
+            "1": "40Ca-1H"
+        },
+        "CaO": {
+            "1": "40Ca-16O"
+        },
+        "CrH": {
+            "1": "52Cr-1H"
+        },
+        "FeH": {
+            "1": "56Fe-1H"
+        },
+        "H2_p": {
+            "1": "1H-2H_p"
+        },
+        "H3O_p": {
+            "1": "1H3-16O_p"
+        },
+        "H3_p": {
+            "1": "1H3_p",
+            "2": "1H2-2H_p"
+        },
+        "HeH_p": {
+            "1": "4He-1H_p",
+            "2": "4He-2H_p",
+            "3": "3He-1H_p",
+            "4": "3He-2H_p"
+        },
+        "KCl": {
+            "1": "39K-35Cl",
+            "2": "39K-37Cl",
+            "3": "41K-35Cl",
+            "4": "41K-37Cl"
+        },
+        "KF": {
+            "1": "39K-19F",
+            "2": "41K-19F"
+        },
+        "KOH": {
+            "1": "39K-16O-1H"
+        },
+        "LiCl": {
+            "1": "7Li-35Cl",
+            "2": "7Li-37Cl",
+            "3": "6Li-35Cl",
+            "4": "6Li-37Cl"
+        },
+        "LiF": {
+            "1": "7Li-19F",
+            "2": "6Li-19F"
+        },
+        "LiH": {
+            "1": "7Li-1H"
+        },
+        "LiH_p": {
+            "1": "7Li-1H_p"
+        },
+        "MgF": {
+            "1": "24Mg-19F",
+            "2": "26Mg-19F",
+            "3": "25Mg-19F"
+        },
+        "MgH": {
+            "1": "24Mg-1H",
+            "2": "26Mg-1H",
+            "3": "25Mg-1H"
+        },
+        "MgO": {
+            "1": "24Mg-16O",
+            "2": "26Mg-16O",
+            "3": "25Mg-16O",
+            "4": "24Mg-18O",
+            "5": "24Mg-17O"
+        },
+        "NH": {
+            "1": "14N-1H"
+        },
+        "NO": {
+            "4": "14N-17O",
+            "5": "15N-18O",
+            "6": "15N-17O"
+        },
+        "NS": {
+            "1": "14N-32S",
+            "2": "14N-34S",
+            "3": "14N-33S",
+            "4": "15N-32S",
+            "5": "14N-36S"
+        },
+        "NaCl": {
+            "1": "23Na-35Cl",
+            "2": "23Na-37Cl"
+        },
+        "NaF": {
+            "1": "23Na-19F"
+        },
+        "NaH": {
+            "1": "23Na-1H",
+            "2": "23Na-2H"
+        },
+        "NaOH": {
+            "1": "23Na-16O-1H"
+        },
+        "NiH": {
+            "1": "58Ni-1H",
+            "2": "60Ni-1H",
+            "3": "62Ni-1H"
+        },
+        "OH_p": {
+            "1": "16O-1H_p"
+        },
+        "PF3": {
+            "1": "31P-19F3"
+        },
+        "PH": {
+            "1": "31P-1H"
+        },
+        "PN": {
+            "1": "31P-14N",
+            "2": "31P-15N"
+        },
+        "PO": {
+            "1": "31P-16O"
+        },
+        "PS": {
+            "1": "31P-32S"
+        },
+        "SH": {
+            "1": "32S-1H",
+            "2": "34S-1H",
+            "3": "33S-1H",
+            "4": "32S-2H",
+            "5": "36S-1H"
+        },
+        "ScH": {
+            "1": "45Sc-1H"
+        },
+        "SiH": {
+            "1": "28Si-1H",
+            "2": "29Si-1H",
+            "3": "30Si-1H",
+            "4": "28Si-2H"
+        },
+        "SiH2": {
+            "1": "28Si-1H2"
+        },
+        "SiH4": {
+            "1": "28Si-1H4"
+        },
+        "SiO": {
+            "1": "28Si-16O",
+            "2": "29Si-16O",
+            "3": "30Si-16O",
+            "4": "28Si-18O",
+            "5": "28Si-17O"
+        },
+        "SiO2": {
+            "1": "28Si-16O2"
+        },
+        "SiS": {
+            "1": "28Si-32S",
+            "2": "29Si-32S",
+            "3": "28Si-34S",
+            "4": "30Si-32S",
+            "5": "28Si-33S",
+            "6": "29Si-34S",
+            "7": "30Si-34S",
+            "8": "29Si-33S",
+            "9": "30Si-33S",
+            "10": "28Si-36S",
+            "11": "29Si-36S",
+            "12": "30Si-36S"
+        },
+        "TiH": {
+            "1": "48Ti-1H"
+        },
+        "TiO": {
+            "1": "48Ti-16O",
+            "2": "46Ti-16O",
+            "3": "47Ti-16O",
+            "4": "49Ti-16O",
+            "5": "50Ti-16O"
+        },
+        "VO": {
+            "1": "51V-16O"
+        },
+        "YO": {
+            "1": "89Y-16O"
+        },
+        "cis-P2H2": {
+            "1": "cis-31P2-1H2"
+        },
+        "trans-P2H2": {
+            "1": "trans-31P2-1H2",
+            "2": "trans-31P2-1H-2H"
+        }
+    },
+    "abundance": {
+        "AlCl": {
+            "1": 0.755,
+            "2": 0.239
+        },
+        "AlF": {
+            "1": 1.0
+        },
+        "AlH": {
+            "1": 0.99972,
+            "2": 0.00028,
+            "3": 9.9972e-07
+        },
+        "AlO": {
+            "1": 0.9973799999999999,
+            "2": 0.0022199999999999998,
+            "3": 0.0004,
+            "4": 9.973799999999998e-07
+        },
+        "AsH3": {
+            "1": 0.9991602351780482
+        },
+        "BeH": {
+            "1": 0.99972,
+            "2": 0.00028,
+            "3": "1e-06"
+        },
+        "C2": {
+            "1": 0.9769345599999999,
+            "2": 0.011465439999999999,
+            "3": 0.00013455999999999999
+        },
+        "CH": {
+            "1": 0.988123248,
+            "2": 0.011596752
+        },
+        "CH3": {
+            "1": 0.9875699764499828
+        },
+        "CH3F": {
+            "1": 0.9875699764499828
+        },
+        "CN": {
+            "1": 0.9842289519999998,
+            "2": 0.011551047999999998,
+            "3": 0.004171048,
+            "4": 4.8952e-05
+        },
+        "CO": {
+            "7": 9.973799999999998e-07,
+            "8": 2.2199999999999998e-09,
+            "9": "4e-10"
+        },
+        "CO2": {
+            "13": 1.856e-09
+        },
+        "CP": {
+            "1": 0.9884
+        },
+        "CS": {
+            "5": 0.0005533199999999999,
+            "6": 0.0001848308,
+            "7": 9.245199999999999e-05,
+            "8": 2.1692e-06
+        },
+        "CaF": {
+            "1": 0.96941,
+            "2": 0.02086,
+            "3": 0.00647,
+            "4": 0.00187,
+            "5": 0.00135,
+            "6": "4e-05"
+        },
+        "CaH": {
+            "1": 0.9691385652000001
+        },
+        "CaO": {
+            "1": 0.9668701458
+        },
+        "CrH": {
+            "1": 0.8376553908
+        },
+        "FeH": {
+            "1": 0.9172830888
+        },
+        "H2_p": {
+            "1": 0.00027992159999999997
+        },
+        "H3O_p": {
+            "1": 0.9965424353618817
+        },
+        "H3_p": {
+            "1": 0.9991602351780482,
+            "2": 0.000279843221952
+        },
+        "HeH_p": {
+            "1": 0.99971800056,
+            "2": 0.00027999943999999994,
+            "3": 1.99944e-06,
+            "4": 5.599999999999999e-10
+        },
+        "KCl": {
+            "1": 0.704098655,
+            "2": 0.222886859,
+            "3": 0.05081301,
+            "4": 0.016085178
+        },
+        "KF": {
+            "1": 0.932581,
+            "2": 0.067302
+        },
+        "KOH": {
+            "1": 0.9298771992414215
+        },
+        "LiCl": {
+            "1": 0.69611,
+            "2": 0.220358,
+            "3": 0.05889,
+            "4": 0.018642
+        },
+        "LiF": {
+            "1": 0.922,
+            "2": 0.078
+        },
+        "LiH": {
+            "1": 0.9217418400000001
+        },
+        "LiH_p": {
+            "1": 0.9217418400000001
+        },
+        "MgF": {
+            "1": 0.7888,
+            "2": 0.1109,
+            "3": 0.10034
+        },
+        "MgH": {
+            "1": 0.788579136,
+            "2": 0.11086894800000001,
+            "3": 0.1003119048
+        },
+        "MgO": {
+            "1": 0.786733344,
+            "2": 0.11060944199999999,
+            "3": 0.10007710919999999,
+            "4": 0.0017511359999999997,
+            "5": 0.00031552
+        },
+        "NH": {
+            "1": 0.9955011815999999
+        },
+        "NO": {
+            "4": 0.00039831199999999995,
+            "5": 9.3684e-06,
+            "6": 1.6880000000000004e-06
+        },
+        "NS": {
+            "1": 0.940115898,
+            "2": 0.047498705999999995,
+            "3": 0.0079363666,
+            "4": 0.003984102000000001,
+            "5": 0.00018621086
+        },
+        "NaCl": {
+            "1": 0.755,
+            "2": 0.239
+        },
+        "NaF": {
+            "1": 1.0
+        },
+        "NaH": {
+            "1": 0.99972,
+            "2": 0.00028
+        },
+        "NaOH": {
+            "1": 0.9971007336
+        },
+        "NiH": {
+            "1": 0.68057838468,
+            "2": 0.26215757532,
+            "3": 0.03633482340000001
+        },
+        "OH_p": {
+            "1": 0.9971007336
+        },
+        "PF3": {
+            "1": 1.0
+        },
+        "PH": {
+            "1": 0.99972
+        },
+        "PN": {
+            "1": 0.9957799999999999,
+            "2": 0.004220000000000001
+        },
+        "PO": {
+            "1": 0.9973799999999999
+        },
+        "PS": {
+            "1": 0.9441
+        },
+        "SH": {
+            "1": 0.9438356520000001,
+            "2": 0.047686644,
+            "3": 0.0079677684,
+            "4": 0.00026434799999999997,
+            "5": 0.00018694764000000004
+        },
+        "ScH": {
+            "1": 0.99972
+        },
+        "SiH": {
+            "1": 0.9216518652000001,
+            "2": 0.0469768428,
+            "3": 0.031091292,
+            "4": 0.0002581348
+        },
+        "SiH2": {
+            "1": 0.9213938026777442
+        },
+        "SiH4": {
+            "1": 0.9208778943855188
+        },
+        "SiO": {
+            "1": 0.9194945957999999,
+            "2": 0.04686688619999999,
+            "3": 0.031018518,
+            "4": 0.0020466402,
+            "5": 0.000368764
+        },
+        "SiO2": {
+            "1": 0.9170855199590039
+        },
+        "SiS": {
+            "1": 0.870375231,
+            "2": 0.044363259,
+            "3": 0.043975107,
+            "4": 0.02936151,
+            "5": 0.0073476227,
+            "6": 0.0022414229999999998,
+            "7": 0.00148347,
+            "8": 0.0003745103,
+            "9": 0.000247867,
+            "10": 0.00017239717000000002,
+            "11": 8.78713e-06,
+            "12": 5.8157e-06
+        },
+        "TiH": {
+            "1": 0.736993584
+        },
+        "TiO": {
+            "1": 0.735268536,
+            "2": 0.08228384999999999,
+            "3": 0.07420507199999998,
+            "4": 0.053958258,
+            "5": 0.051664284
+        },
+        "VO": {
+            "1": 0.99488655
+        },
+        "YO": {
+            "1": 0.9973799999999999
+        },
+        "cis-P2H2": {
+            "1": 0.9994400784000002
+        },
+        "trans-P2H2": {
+            "1": 0.9994400784000002,
+            "2": 0.00027992159999999997
+        }
+    },
+    "molar_mass": {
+        "AlCl": {
+            "1": 61.9503912,
+            "2": 63.9474411
+        },
+        "AlF": {
+            "1": 45.979941663
+        },
+        "AlH": {
+            "1": 27.9893635322,
+            "2": 28.995640278099998,
+            "3": 26.9833635322
+        },
+        "AlO": {
+            "1": 42.97645312,
+            "2": 44.980698113,
+            "3": 43.980670257,
+            "4": 41.97045312
+        },
+        "AsH3": {
+            "1": 77.94507009659999
+        },
+        "BeH": {
+            "1": 10.0200081322,
+            "2": 11.0262848781,
+            "3": 12.0322848781
+        },
+        "C2": {
+            "1": 24.0,
+            "2": 25.003354835,
+            "3": 26.00670967
+        },
+        "CH": {
+            "1": 13.0078250322,
+            "2": 14.0111798672
+        },
+        "CH3": {
+            "1": 15.0234750966
+        },
+        "CH3F": {
+            "1": 34.0218782596
+        },
+        "CN": {
+            "1": 26.003074004,
+            "2": 27.006428839,
+            "3": 27.000108899,
+            "4": 28.003463734
+        },
+        "CO": {
+            "7": 30.004269455,
+            "8": 32.008514448,
+            "9": 31.008486592
+        },
+        "CO2": {
+            "13": 47.001618349
+        },
+        "CP": {
+            "1": 42.973761998
+        },
+        "CS": {
+            "5": 46.971221834999994,
+            "6": 47.967081,
+            "7": 45.974813745000006,
+            "8": 48.970435835
+        },
+        "CaF": {
+            "1": 58.960994063,
+            "2": 62.953885163,
+            "3": 60.957021163,
+            "4": 66.950925963,
+            "5": 61.957169162999996,
+            "6": 64.952093163
+        },
+        "CaH": {
+            "1": 40.970415932200005
+        },
+        "CaO": {
+            "1": 55.95750552
+        },
+        "CrH": {
+            "1": 52.9483310322
+        },
+        "FeH": {
+            "1": 56.9427610322
+        },
+        "H2_p": {
+            "1": 3.0219268103
+        },
+        "H3O_p": {
+            "1": 19.018389716599998
+        },
+        "H3_p": {
+            "1": 3.0234750966,
+            "2": 4.0297518425
+        },
+        "HeH_p": {
+            "1": 5.0104282863,
+            "2": 6.016705032200001,
+            "3": 4.0238543522,
+            "4": 5.0301310981
+        },
+        "KCl": {
+            "1": 73.93255919,
+            "2": 75.92960909,
+            "3": 75.93067796,
+            "4": 77.92772786
+        },
+        "KF": {
+            "1": 57.962109653,
+            "2": 59.960228423
+        },
+        "KOH": {
+            "1": 55.9664461422
+        },
+        "LiCl": {
+            "1": 41.98485614,
+            "2": 43.98190604,
+            "3": 40.983975587,
+            "4": 42.981025487
+        },
+        "LiF": {
+            "1": 26.014406602999998,
+            "2": 25.01352605
+        },
+        "LiH": {
+            "1": 8.0238284722
+        },
+        "LiH_p": {
+            "1": 8.0238284722
+        },
+        "MgF": {
+            "1": 42.983444863,
+            "2": 44.980996163,
+            "3": 43.984240162999996
+        },
+        "MgH": {
+            "1": 24.9928667322,
+            "2": 26.9904180322,
+            "3": 25.9936620322
+        },
+        "MgO": {
+            "1": 39.97995632,
+            "2": 41.97750762,
+            "3": 40.98075162,
+            "4": 41.984201313,
+            "5": 40.984173457
+        },
+        "NH": {
+            "1": 15.0108990362
+        },
+        "NO": {
+            "4": 31.002205761,
+            "5": 32.999268512,
+            "6": 31.999240656
+        },
+        "NS": {
+            "1": 45.975145178,
+            "2": 47.970941004,
+            "3": 46.974532914,
+            "4": 46.972180073000004,
+            "5": 49.970155004
+        },
+        "NaCl": {
+            "1": 57.958621980000004,
+            "2": 59.95567188
+        },
+        "NaF": {
+            "1": 41.988172442999996
+        },
+        "NaH": {
+            "1": 23.9975943122,
+            "2": 25.0038710581
+        },
+        "NaOH": {
+            "1": 39.9925089322
+        },
+        "NiH": {
+            "1": 58.9431670322,
+            "2": 60.9386110322,
+            "3": 62.9361700322
+        },
+        "OH_p": {
+            "1": 17.0027396522
+        },
+        "PF3": {
+            "1": 87.968971487
+        },
+        "PH": {
+            "1": 31.9815870302
+        },
+        "PN": {
+            "1": 44.976836002,
+            "2": 45.973870897
+        },
+        "PO": {
+            "1": 46.968676618
+        },
+        "PS": {
+            "1": 62.945833172
+        },
+        "SH": {
+            "1": 32.9798962062,
+            "2": 34.9756920322,
+            "3": 33.979283942200006,
+            "4": 33.9861729521,
+            "5": 36.9749060322
+        },
+        "ScH": {
+            "1": 45.963733032200004
+        },
+        "SiH": {
+            "1": 28.9847515672,
+            "2": 29.9843196972,
+            "3": 30.9815950422,
+            "4": 29.9910283131
+        },
+        "SiH2": {
+            "1": 29.9925765994
+        },
+        "SiH4": {
+            "1": 32.0082266638
+        },
+        "SiO": {
+            "1": 43.971841155,
+            "2": 44.971409285,
+            "3": 45.96868463,
+            "4": 45.976086148,
+            "5": 44.976058292000005
+        },
+        "SiO2": {
+            "1": 59.966755774999996
+        },
+        "SiS": {
+            "1": 59.948997709,
+            "2": 60.948565839,
+            "3": 61.944793535,
+            "4": 61.945841184,
+            "5": 60.948385445,
+            "6": 62.944361665,
+            "7": 63.941637009999994,
+            "8": 61.947953575,
+            "9": 62.945228920000005,
+            "10": 63.944007535,
+            "11": 64.943575665,
+            "12": 65.94085101
+        },
+        "TiH": {
+            "1": 48.9557670322
+        },
+        "TiO": {
+            "1": 63.94285662,
+            "2": 61.94754261999999,
+            "3": 62.94667362,
+            "4": 64.94278062,
+            "5": 65.93970162
+        },
+        "VO": {
+            "1": 66.93887162
+        },
+        "YO": {
+            "1": 104.90075462
+        },
+        "cis-P2H2": {
+            "1": 63.9631740604
+        },
+        "trans-P2H2": {
+            "1": 63.9631740604,
+            "2": 64.9694508063
+        }
+    }
+}
diff --git a/radis/default_radis.json b/radis/default_radis.json
index 3acb2ac6d..a3bf6f5b1 100644
--- a/radis/default_radis.json
+++ b/radis/default_radis.json
@@ -1,841 +1,32 @@
 # default values for RADIS configuration parameters
-# the values of your ~/radis.json file will overwrite these.
+# the values of your ~/radis.json file will overwrite, these.
 {
-    "database": {},                          # list of registered databases on this computer. See expected format in https://radis.readthedocs.io/en/latest/lbl/lbl.html#label-lbl-config-file
-    "spectroscopic_constants": {             # path to spectroscopic constants for non-LTE calculations. By default look for them in radis/db/[MOLECULE]/*
+    "database": {}                          # list of registered databases on this computer. See expected format in https://radis.readthedocs.io/en/latest/lbl/lbl.html#label-lbl-config-file
+    "spectroscopic_constants": {             # path to spectroscopic constants for non-LTE calculations. By default look for them in radis/db/[MOLECULE],/*
         "CO2": "molecules_data.json",
         "CO": "molecules_data.json"
     },
     "plot": {
-        "plotlib": "publib",                 # library for plot style. "publib", "seaborn".
-        "context": "paper",                  # "paper", "talk", "poster"
+        "plotlib": "publib"                 # library for plot style. "publib", "seaborn".
+        "context": "paper"                  # "paper", "talk", "poster",
         "style": "origin"                    # default style. See Publib (https://github.com/erwanp/publib) or Seaborn (https://seaborn.pydata.org/)
-    },
+    }
     # global parameters
     # -----------------
     "DEBUG_MODE": false,
-    "AUTO_UPDATE_DATABASE": false,           # auto-convert 'pytables' files to 'vaex'
-    "AUTO_UPDATE_SPEC": false,               # auto-update saved Spectrum objects to latest format.
-    "OLDEST_COMPATIBLE_VERSION": "0.9.1",    # automatically regenerate cache files generated with versions anterior to this one
-    "USE_CYTHON": true,                      # use Cython module if available (else, default to Python)
-    "GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD": 3,    # raise a warning if less than THIS number of grid points per lineshape
-    "GRIDPOINTS_PER_LINEWIDTH_ERROR_THRESHOLD": 1,   # raise an error if less than THIS number of grid points per lineshape
-    "MEMORY_MAPPING_ENGINE": "auto",         # "vaex"/"pytables"/"feather". "auto" uses "vaex" in most cases
-    "SPARSE_WAVERANGE": "auto",              # true/false. sparse LDM algorithm. May be smaller on dense spectra. If "auto", a scarcity criterion is used (Nlines/Ngrids > 1)
-    "DEFAULT_DOWNLOAD_PATH": "~/.radisdb",   # default path for downloading databases with databank='hitran'/'hitemp'/'exomol' . You can also specify a local path for each entry of the "database" list.
+    "AUTO_UPDATE_DATABASE": false           # auto-convert 'pytables' files to 'vaex'
+    "AUTO_UPDATE_SPEC": false               # auto-update saved Spectrum objects to latest format.
+    "OLDEST_COMPATIBLE_VERSION": "0.9.1"    # automatically regenerate cache, files generated with versions anterior to this one,
+    "USE_CYTHON": true                      # use Cython module if available (else default to Python)
+    "GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD": 3    # raise a warning if less than THIS number of grid points per lineshape,
+    "GRIDPOINTS_PER_LINEWIDTH_ERROR_THRESHOLD": 1   # raise an error if less than THIS number of grid points per lineshape,
+    "MEMORY_MAPPING_ENGINE": "auto"         # "vaex",/"pytables"/"feather". "auto" uses "vaex" in most cases
+    "SPARSE_WAVERANGE": "auto"              # true,/false. sparse LDM algorithm. May be smaller on dense spectra. If "auto", a scarcity criterion is used (Nlines/Ngrids > 1)
+    "DEFAULT_DOWNLOAD_PATH": "~/.radisdb"   # default path for downloading databases with databank='hitran'/'hitemp'/'exomol' . You can also specify a local path for each entry of the, "database" list.
+    "RESAMPLING_TOLERANCE_THRESHOLD": 5e-3  # an error if raises if areas do not match by a value above, this threshold during resampling. See :py:meth:`~radis.spectrum.spectrum.Spectrum.resample`
+
 
     # molecular parameters
     # --------------------
-    "molparams": {                           # molecular parameters for non-HITRAN species (the latter being defined in https://github.com/radis/radis/blob/master/radis/db/molparam.txt)
-        "isotope_full_name": {
-            "AlCl": {
-                "1": "27Al-35Cl",
-                "2": "27Al-37Cl"
-            },
-            "AlF": {
-                "1": "27Al-19F"
-            },
-            "AlH": {
-                "1": "27Al-1H",
-                "2": "27Al-2H",
-                "3": "26Al-1H"
-            },
-            "AlO": {
-                "1": "27Al-16O",
-                "2": "27Al-18O",
-                "3": "27Al-17O",
-                "4": "26Al-16O"
-            },
-            "AsH3": {
-                "1": "75As-1H3"
-            },
-            "BeH": {
-                "1": "9Be-1H",
-                "2": "9Be-2H",
-                "3": "9Be-3H"
-            },
-            "C2": {
-                "1": "12C2",
-                "2": "12C-13C",
-                "3": "13C2"
-            },
-            "CH": {
-                "1": "12C-1H",
-                "2": "13C-1H"
-            },
-            "CH3": {
-                "1": "12C-1H3"
-            },
-            "CH3F": {
-                "1": "12C-1H3-19F"
-            },
-            "CN": {
-                "1": "12C-14N",
-                "2": "13C-14N",
-                "3": "12C-15N",
-                "4": "13C-15N"
-            },
-            "CO": {
-                "7": "14C-16O",
-                "8": "14C-18O",
-                "9": "14C-17O"
-            },
-            "CO2": {
-                "13": "13C-17O2"
-            },
-            "CP": {
-                "1": "12C-31P"
-            },
-            "CS": {
-                "5": "13C-34S",
-                "6": "12C-36S",
-                "7": "13C-33S",
-                "8": "13C-36S"
-            },
-            "CaF": {
-                "1": "40Ca-19F",
-                "2": "44Ca-19F",
-                "3": "42Ca-19F",
-                "4": "48Ca-19F",
-                "5": "43Ca-19F",
-                "6": "46Ca-19F"
-            },
-            "CaH": {
-                "1": "40Ca-1H"
-            },
-            "CaO": {
-                "1": "40Ca-16O"
-            },
-            "CrH": {
-                "1": "52Cr-1H"
-            },
-            "FeH": {
-                "1": "56Fe-1H"
-            },
-            "H2_p": {
-                "1": "1H-2H_p"
-            },
-            "H3O_p": {
-                "1": "1H3-16O_p"
-            },
-            "H3_p": {
-                "1": "1H3_p",
-                "2": "1H2-2H_p"
-            },
-            "HeH_p": {
-                "1": "4He-1H_p",
-                "2": "4He-2H_p",
-                "3": "3He-1H_p",
-                "4": "3He-2H_p"
-            },
-            "KCl": {
-                "1": "39K-35Cl",
-                "2": "39K-37Cl",
-                "3": "41K-35Cl",
-                "4": "41K-37Cl"
-            },
-            "KF": {
-                "1": "39K-19F",
-                "2": "41K-19F"
-            },
-            "KOH": {
-                "1": "39K-16O-1H"
-            },
-            "LiCl": {
-                "1": "7Li-35Cl",
-                "2": "7Li-37Cl",
-                "3": "6Li-35Cl",
-                "4": "6Li-37Cl"
-            },
-            "LiF": {
-                "1": "7Li-19F",
-                "2": "6Li-19F"
-            },
-            "LiH": {
-                "1": "7Li-1H"
-            },
-            "LiH_p": {
-                "1": "7Li-1H_p"
-            },
-            "MgF": {
-                "1": "24Mg-19F",
-                "2": "26Mg-19F",
-                "3": "25Mg-19F"
-            },
-            "MgH": {
-                "1": "24Mg-1H",
-                "2": "26Mg-1H",
-                "3": "25Mg-1H"
-            },
-            "MgO": {
-                "1": "24Mg-16O",
-                "2": "26Mg-16O",
-                "3": "25Mg-16O",
-                "4": "24Mg-18O",
-                "5": "24Mg-17O"
-            },
-            "NH": {
-                "1": "14N-1H"
-            },
-            "NO": {
-                "4": "14N-17O",
-                "5": "15N-18O",
-                "6": "15N-17O"
-            },
-            "NS": {
-                "1": "14N-32S",
-                "2": "14N-34S",
-                "3": "14N-33S",
-                "4": "15N-32S",
-                "5": "14N-36S"
-            },
-            "NaCl": {
-                "1": "23Na-35Cl",
-                "2": "23Na-37Cl"
-            },
-            "NaF": {
-                "1": "23Na-19F"
-            },
-            "NaH": {
-                "1": "23Na-1H",
-                "2": "23Na-2H"
-            },
-            "NaOH": {
-                "1": "23Na-16O-1H"
-            },
-            "NiH": {
-                "1": "58Ni-1H",
-                "2": "60Ni-1H",
-                "3": "62Ni-1H"
-            },
-            "OH_p": {
-                "1": "16O-1H_p"
-            },
-            "PF3": {
-                "1": "31P-19F3"
-            },
-            "PH": {
-                "1": "31P-1H"
-            },
-            "PN": {
-                "1": "31P-14N",
-                "2": "31P-15N"
-            },
-            "PO": {
-                "1": "31P-16O"
-            },
-            "PS": {
-                "1": "31P-32S"
-            },
-            "SH": {
-                "1": "32S-1H",
-                "2": "34S-1H",
-                "3": "33S-1H",
-                "4": "32S-2H",
-                "5": "36S-1H"
-            },
-            "ScH": {
-                "1": "45Sc-1H"
-            },
-            "SiH": {
-                "1": "28Si-1H",
-                "2": "29Si-1H",
-                "3": "30Si-1H",
-                "4": "28Si-2H"
-            },
-            "SiH2": {
-                "1": "28Si-1H2"
-            },
-            "SiH4": {
-                "1": "28Si-1H4"
-            },
-            "SiO": {
-                "1": "28Si-16O",
-                "2": "29Si-16O",
-                "3": "30Si-16O",
-                "4": "28Si-18O",
-                "5": "28Si-17O"
-            },
-            "SiO2": {
-                "1": "28Si-16O2"
-            },
-            "SiS": {
-                "1": "28Si-32S",
-                "2": "29Si-32S",
-                "3": "28Si-34S",
-                "4": "30Si-32S",
-                "5": "28Si-33S",
-                "6": "29Si-34S",
-                "7": "30Si-34S",
-                "8": "29Si-33S",
-                "9": "30Si-33S",
-                "10": "28Si-36S",
-                "11": "29Si-36S",
-                "12": "30Si-36S"
-            },
-            "TiH": {
-                "1": "48Ti-1H"
-            },
-            "TiO": {
-                "1": "48Ti-16O",
-                "2": "46Ti-16O",
-                "3": "47Ti-16O",
-                "4": "49Ti-16O",
-                "5": "50Ti-16O"
-            },
-            "VO": {
-                "1": "51V-16O"
-            },
-            "YO": {
-                "1": "89Y-16O"
-            },
-            "cis-P2H2": {
-                "1": "cis-31P2-1H2"
-            },
-            "trans-P2H2": {
-                "1": "trans-31P2-1H2",
-                "2": "trans-31P2-1H-2H"
-            }
-        },
-        "abundances": {
-            "AlCl": {
-                "1": 0.755,
-                "2": 0.239
-            },
-            "AlF": {
-                "1": 1.0
-            },
-            "AlH": {
-                "1": 0.99972,
-                "2": 0.00028,
-                "3": 9.9972e-07
-            },
-            "AlO": {
-                "1": 0.9973799999999999,
-                "2": 0.0022199999999999998,
-                "3": 0.0004,
-                "4": 9.973799999999998e-07
-            },
-            "AsH3": {
-                "1": 0.9991602351780482
-            },
-            "BeH": {
-                "1": 0.99972,
-                "2": 0.00028,
-                "3": "1e-06"
-            },
-            "C2": {
-                "1": 0.9769345599999999,
-                "2": 0.011465439999999999,
-                "3": 0.00013455999999999999
-            },
-            "CH": {
-                "1": 0.988123248,
-                "2": 0.011596752
-            },
-            "CH3": {
-                "1": 0.9875699764499828
-            },
-            "CH3F": {
-                "1": 0.9875699764499828
-            },
-            "CN": {
-                "1": 0.9842289519999998,
-                "2": 0.011551047999999998,
-                "3": 0.004171048,
-                "4": 4.8952e-05
-            },
-            "CO": {
-                "7": 9.973799999999998e-07,
-                "8": 2.2199999999999998e-09,
-                "9": "4e-10"
-            },
-            "CO2": {
-                "13": 1.856e-09
-            },
-            "CP": {
-                "1": 0.9884
-            },
-            "CS": {
-                "5": 0.0005533199999999999,
-                "6": 0.0001848308,
-                "7": 9.245199999999999e-05,
-                "8": 2.1692e-06
-            },
-            "CaF": {
-                "1": 0.96941,
-                "2": 0.02086,
-                "3": 0.00647,
-                "4": 0.00187,
-                "5": 0.00135,
-                "6": "4e-05"
-            },
-            "CaH": {
-                "1": 0.9691385652000001
-            },
-            "CaO": {
-                "1": 0.9668701458
-            },
-            "CrH": {
-                "1": 0.8376553908
-            },
-            "FeH": {
-                "1": 0.9172830888
-            },
-            "H2_p": {
-                "1": 0.00027992159999999997
-            },
-            "H3O_p": {
-                "1": 0.9965424353618817
-            },
-            "H3_p": {
-                "1": 0.9991602351780482,
-                "2": 0.000279843221952
-            },
-            "HeH_p": {
-                "1": 0.99971800056,
-                "2": 0.00027999943999999994,
-                "3": 1.99944e-06,
-                "4": 5.599999999999999e-10
-            },
-            "KCl": {
-                "1": 0.704098655,
-                "2": 0.222886859,
-                "3": 0.05081301,
-                "4": 0.016085178
-            },
-            "KF": {
-                "1": 0.932581,
-                "2": 0.067302
-            },
-            "KOH": {
-                "1": 0.9298771992414215
-            },
-            "LiCl": {
-                "1": 0.69611,
-                "2": 0.220358,
-                "3": 0.05889,
-                "4": 0.018642
-            },
-            "LiF": {
-                "1": 0.922,
-                "2": 0.078
-            },
-            "LiH": {
-                "1": 0.9217418400000001
-            },
-            "LiH_p": {
-                "1": 0.9217418400000001
-            },
-            "MgF": {
-                "1": 0.7888,
-                "2": 0.1109,
-                "3": 0.10034
-            },
-            "MgH": {
-                "1": 0.788579136,
-                "2": 0.11086894800000001,
-                "3": 0.1003119048
-            },
-            "MgO": {
-                "1": 0.786733344,
-                "2": 0.11060944199999999,
-                "3": 0.10007710919999999,
-                "4": 0.0017511359999999997,
-                "5": 0.00031552
-            },
-            "NH": {
-                "1": 0.9955011815999999
-            },
-            "NO": {
-                "4": 0.00039831199999999995,
-                "5": 9.3684e-06,
-                "6": 1.6880000000000004e-06
-            },
-            "NS": {
-                "1": 0.940115898,
-                "2": 0.047498705999999995,
-                "3": 0.0079363666,
-                "4": 0.003984102000000001,
-                "5": 0.00018621086
-            },
-            "NaCl": {
-                "1": 0.755,
-                "2": 0.239
-            },
-            "NaF": {
-                "1": 1.0
-            },
-            "NaH": {
-                "1": 0.99972,
-                "2": 0.00028
-            },
-            "NaOH": {
-                "1": 0.9971007336
-            },
-            "NiH": {
-                "1": 0.68057838468,
-                "2": 0.26215757532,
-                "3": 0.03633482340000001
-            },
-            "OH_p": {
-                "1": 0.9971007336
-            },
-            "PF3": {
-                "1": 1.0
-            },
-            "PH": {
-                "1": 0.99972
-            },
-            "PN": {
-                "1": 0.9957799999999999,
-                "2": 0.004220000000000001
-            },
-            "PO": {
-                "1": 0.9973799999999999
-            },
-            "PS": {
-                "1": 0.9441
-            },
-            "SH": {
-                "1": 0.9438356520000001,
-                "2": 0.047686644,
-                "3": 0.0079677684,
-                "4": 0.00026434799999999997,
-                "5": 0.00018694764000000004
-            },
-            "ScH": {
-                "1": 0.99972
-            },
-            "SiH": {
-                "1": 0.9216518652000001,
-                "2": 0.0469768428,
-                "3": 0.031091292,
-                "4": 0.0002581348
-            },
-            "SiH2": {
-                "1": 0.9213938026777442
-            },
-            "SiH4": {
-                "1": 0.9208778943855188
-            },
-            "SiO": {
-                "1": 0.9194945957999999,
-                "2": 0.04686688619999999,
-                "3": 0.031018518,
-                "4": 0.0020466402,
-                "5": 0.000368764
-            },
-            "SiO2": {
-                "1": 0.9170855199590039
-            },
-            "SiS": {
-                "1": 0.870375231,
-                "2": 0.044363259,
-                "3": 0.043975107,
-                "4": 0.02936151,
-                "5": 0.0073476227,
-                "6": 0.0022414229999999998,
-                "7": 0.00148347,
-                "8": 0.0003745103,
-                "9": 0.000247867,
-                "10": 0.00017239717000000002,
-                "11": 8.78713e-06,
-                "12": 5.8157e-06
-            },
-            "TiH": {
-                "1": 0.736993584
-            },
-            "TiO": {
-                "1": 0.735268536,
-                "2": 0.08228384999999999,
-                "3": 0.07420507199999998,
-                "4": 0.053958258,
-                "5": 0.051664284
-            },
-            "VO": {
-                "1": 0.99488655
-            },
-            "YO": {
-                "1": 0.9973799999999999
-            },
-            "cis-P2H2": {
-                "1": 0.9994400784000002
-            },
-            "trans-P2H2": {
-                "1": 0.9994400784000002,
-                "2": 0.00027992159999999997
-            }
-        },
-        "molar_mass": {
-            "AlCl": {
-                "1": 61.9503912,
-                "2": 63.9474411
-            },
-            "AlF": {
-                "1": 45.979941663
-            },
-            "AlH": {
-                "1": 27.9893635322,
-                "2": 28.995640278099998,
-                "3": 26.9833635322
-            },
-            "AlO": {
-                "1": 42.97645312,
-                "2": 44.980698113,
-                "3": 43.980670257,
-                "4": 41.97045312
-            },
-            "AsH3": {
-                "1": 77.94507009659999
-            },
-            "BeH": {
-                "1": 10.0200081322,
-                "2": 11.0262848781,
-                "3": 12.0322848781
-            },
-            "C2": {
-                "1": 24.0,
-                "2": 25.003354835,
-                "3": 26.00670967
-            },
-            "CH": {
-                "1": 13.0078250322,
-                "2": 14.0111798672
-            },
-            "CH3": {
-                "1": 15.0234750966
-            },
-            "CH3F": {
-                "1": 34.0218782596
-            },
-            "CN": {
-                "1": 26.003074004,
-                "2": 27.006428839,
-                "3": 27.000108899,
-                "4": 28.003463734
-            },
-            "CO": {
-                "7": 30.004269455,
-                "8": 32.008514448,
-                "9": 31.008486592
-            },
-            "CO2": {
-                "13": 47.001618349
-            },
-            "CP": {
-                "1": 42.973761998
-            },
-            "CS": {
-                "5": 46.971221834999994,
-                "6": 47.967081,
-                "7": 45.974813745000006,
-                "8": 48.970435835
-            },
-            "CaF": {
-                "1": 58.960994063,
-                "2": 62.953885163,
-                "3": 60.957021163,
-                "4": 66.950925963,
-                "5": 61.957169162999996,
-                "6": 64.952093163
-            },
-            "CaH": {
-                "1": 40.970415932200005
-            },
-            "CaO": {
-                "1": 55.95750552
-            },
-            "CrH": {
-                "1": 52.9483310322
-            },
-            "FeH": {
-                "1": 56.9427610322
-            },
-            "H2_p": {
-                "1": 3.0219268103
-            },
-            "H3O_p": {
-                "1": 19.018389716599998
-            },
-            "H3_p": {
-                "1": 3.0234750966,
-                "2": 4.0297518425
-            },
-            "HeH_p": {
-                "1": 5.0104282863,
-                "2": 6.016705032200001,
-                "3": 4.0238543522,
-                "4": 5.0301310981
-            },
-            "KCl": {
-                "1": 73.93255919,
-                "2": 75.92960909,
-                "3": 75.93067796,
-                "4": 77.92772786
-            },
-            "KF": {
-                "1": 57.962109653,
-                "2": 59.960228423
-            },
-            "KOH": {
-                "1": 55.9664461422
-            },
-            "LiCl": {
-                "1": 41.98485614,
-                "2": 43.98190604,
-                "3": 40.983975587,
-                "4": 42.981025487
-            },
-            "LiF": {
-                "1": 26.014406602999998,
-                "2": 25.01352605
-            },
-            "LiH": {
-                "1": 8.0238284722
-            },
-            "LiH_p": {
-                "1": 8.0238284722
-            },
-            "MgF": {
-                "1": 42.983444863,
-                "2": 44.980996163,
-                "3": 43.984240162999996
-            },
-            "MgH": {
-                "1": 24.9928667322,
-                "2": 26.9904180322,
-                "3": 25.9936620322
-            },
-            "MgO": {
-                "1": 39.97995632,
-                "2": 41.97750762,
-                "3": 40.98075162,
-                "4": 41.984201313,
-                "5": 40.984173457
-            },
-            "NH": {
-                "1": 15.0108990362
-            },
-            "NO": {
-                "4": 31.002205761,
-                "5": 32.999268512,
-                "6": 31.999240656
-            },
-            "NS": {
-                "1": 45.975145178,
-                "2": 47.970941004,
-                "3": 46.974532914,
-                "4": 46.972180073000004,
-                "5": 49.970155004
-            },
-            "NaCl": {
-                "1": 57.958621980000004,
-                "2": 59.95567188
-            },
-            "NaF": {
-                "1": 41.988172442999996
-            },
-            "NaH": {
-                "1": 23.9975943122,
-                "2": 25.0038710581
-            },
-            "NaOH": {
-                "1": 39.9925089322
-            },
-            "NiH": {
-                "1": 58.9431670322,
-                "2": 60.9386110322,
-                "3": 62.9361700322
-            },
-            "OH_p": {
-                "1": 17.0027396522
-            },
-            "PF3": {
-                "1": 87.968971487
-            },
-            "PH": {
-                "1": 31.9815870302
-            },
-            "PN": {
-                "1": 44.976836002,
-                "2": 45.973870897
-            },
-            "PO": {
-                "1": 46.968676618
-            },
-            "PS": {
-                "1": 62.945833172
-            },
-            "SH": {
-                "1": 32.9798962062,
-                "2": 34.9756920322,
-                "3": 33.979283942200006,
-                "4": 33.9861729521,
-                "5": 36.9749060322
-            },
-            "ScH": {
-                "1": 45.963733032200004
-            },
-            "SiH": {
-                "1": 28.9847515672,
-                "2": 29.9843196972,
-                "3": 30.9815950422,
-                "4": 29.9910283131
-            },
-            "SiH2": {
-                "1": 29.9925765994
-            },
-            "SiH4": {
-                "1": 32.0082266638
-            },
-            "SiO": {
-                "1": 43.971841155,
-                "2": 44.971409285,
-                "3": 45.96868463,
-                "4": 45.976086148,
-                "5": 44.976058292000005
-            },
-            "SiO2": {
-                "1": 59.966755774999996
-            },
-            "SiS": {
-                "1": 59.948997709,
-                "2": 60.948565839,
-                "3": 61.944793535,
-                "4": 61.945841184,
-                "5": 60.948385445,
-                "6": 62.944361665,
-                "7": 63.941637009999994,
-                "8": 61.947953575,
-                "9": 62.945228920000005,
-                "10": 63.944007535,
-                "11": 64.943575665,
-                "12": 65.94085101
-            },
-            "TiH": {
-                "1": 48.9557670322
-            },
-            "TiO": {
-                "1": 63.94285662,
-                "2": 61.94754261999999,
-                "3": 62.94667362,
-                "4": 64.94278062,
-                "5": 65.93970162
-            },
-            "VO": {
-                "1": 66.93887162
-            },
-            "YO": {
-                "1": 104.90075462
-            },
-            "cis-P2H2": {
-                "1": 63.9631740604
-            },
-            "trans-P2H2": {
-                "1": 63.9631740604,
-                "2": 64.9694508063
-            }
-        }
-    }
+    # Additional parameters are read from radis.db.molparams_extra.json
 }
diff --git a/radis/io/cache_files.py b/radis/io/cache_files.py
index f745de919..efac7845f 100644
--- a/radis/io/cache_files.py
+++ b/radis/io/cache_files.py
@@ -29,7 +29,7 @@
 # Note: don't import unicode_literals because it breaks the df.to_hdf of
 # save_to_hdf because of a stupid unicode/str error in Python 2.7
 import os
-from os.path import exists, splitext
+from os.path import exists
 from warnings import warn
 
 from packaging.version import parse
@@ -37,9 +37,9 @@
 import radis
 
 try:
-    from .hdf5 import HDF5Manager
+    from .hdf5 import DataFileManager
 except ImportError:  # if file is ran as a module
-    from radis.io.hdf5 import HDF5Manager
+    from radis.io.hdf5 import DataFileManager
 from radis.misc.basics import compare_dict, is_float
 from radis.misc.printer import printm, printr
 from radis.misc.warning import DeprecatedFileWarning, IrrelevantFileWarning
@@ -179,8 +179,8 @@ def load_h5_cache_file(
         printm("Reading cache file ({0})".format(cachefile))
     try:
         # Load file :
-        manager = HDF5Manager(engine)
-        df = manager.load(cachefile, columns=columns, key="df")
+        manager = DataFileManager(engine)
+        df = manager.read(cachefile, columns=columns, key="df")
 
     except KeyError as err:  # An error happened during file reading.
         # Fail safe by deleting cache file (unless we explicitely wanted it
@@ -222,8 +222,8 @@ def get_cache_file(fcache, engine="pytables", verbose=True):
     # TODO @dev: refactor : we probably don't need this function (only used in astroquery)
 
     # Load file
-    manager = HDF5Manager(engine)
-    df = manager.load(fcache, key="df")
+    manager = DataFileManager(engine)
+    df = manager.read(fcache, key="df")
 
     # Check file
     # ... 'object' columns slow everything down (not fixed format strings!)
@@ -368,10 +368,10 @@ def check_not_deprecated(
         which HDF5 library to use. If ``'guess'``, try to guess.
     """
     if engine == "guess":
-        engine = HDF5Manager.guess_engine(file)
+        engine = DataFileManager.guess_engine(file)
 
     # Get metadata :
-    manager = HDF5Manager(engine)
+    manager = DataFileManager(engine)
 
     try:
         file_metadata = manager.read_metadata(file)
@@ -526,10 +526,10 @@ def check_relevancy(
 
     """
     if engine == "guess":
-        engine = HDF5Manager.guess_engine(file)
+        engine = DataFileManager.guess_engine(file)
 
     # Get metadata :
-    manager = HDF5Manager(engine)
+    manager = DataFileManager(engine)
     file_metadata = manager.read_metadata(file, key=key)
 
     for k, v in relevant_if_metadata_above.items():
@@ -607,7 +607,7 @@ def save_to_hdf(
      ``None`` values are not stored
     """
     # Check file
-    assert fname.endswith(".h5") or fname.endswith(".hdf5")
+    assert str(fname).endswith(".h5") or str(fname).endswith(".hdf5")
     assert "version" not in metadata
     # ... 'object' columns slow everything down (not fixed format strings!)
     if verbose >= 2:
@@ -621,7 +621,7 @@ def save_to_hdf(
         raise ValueError("File exist: {0}".format(fname))
 
     # start by exporting dataframe
-    manager = HDF5Manager(engine)
+    manager = DataFileManager(engine)
     manager.write(fname, df, append=False, key=key)
 
     # Add metadata
@@ -686,19 +686,7 @@ def some_function(*args):
 
 
 def cache_file_name(fname, engine="pytables"):
-    """Return the corresponding cache file name for fname.
-
-    Other Parameters
-    ----------------
-    engine: ``'h5py'``, ``'pytables'``, ``'vaex'``
-       which HDF5 library to use. Default ``pytables``
-    """
-    if engine in ["pytables", "pytables-fixed"]:
-        return splitext(fname)[0] + ".h5"
-    elif engine in ["h5py", "vaex"]:
-        return splitext(fname)[0] + ".hdf5"
-    else:
-        raise ValueError(engine)
+    raise DeprecationWarning("Use DataFileManager.cache_file() instead")
 
 
 if __name__ == "__main__":
diff --git a/radis/io/cdsd.py b/radis/io/cdsd.py
index d3518b50a..13cac495c 100644
--- a/radis/io/cdsd.py
+++ b/radis/io/cdsd.py
@@ -23,7 +23,8 @@
 from os.path import exists, getmtime
 
 import radis
-from radis.io.cache_files import cache_file_name, load_h5_cache_file, save_to_hdf
+from radis.io.cache_files import load_h5_cache_file, save_to_hdf
+from radis.io.hdf5 import DataFileManager
 from radis.io.tools import (
     drop_object_format_columns,
     parse_hitran_file,
@@ -241,7 +242,7 @@ def cdsd2df(
         raise ValueError("Unknown CDSD version: {0}".format(version))
 
     # Use cache file if possible
-    fcache = cache_file_name(fname, engine=engine)
+    fcache = str(DataFileManager(engine).cache_file(fname))
     if cache and exists(fcache):
         relevant_if_metadata_above = (
             {"wavenum_max": load_wavenum_min} if load_wavenum_min else {}
@@ -312,6 +313,6 @@ def cdsd2df(
 
 
 if __name__ == "__main__":
-    from radis.test.test_io import test_hitemp
+    from radis.test.io.test_hitran_cdsd import _run_testcases
 
-    print("Testing cdsd: ", test_hitemp())
+    print("Testing cdsd: ", _run_testcases())
diff --git a/radis/io/dbmanager.py b/radis/io/dbmanager.py
index c61e12398..bbe8578b5 100644
--- a/radis/io/dbmanager.py
+++ b/radis/io/dbmanager.py
@@ -16,9 +16,9 @@
 
 try:
     from .cache_files import check_not_deprecated
-    from .hdf5 import HDF5Manager, hdf2df
+    from .hdf5 import DataFileManager
 except ImportError:
-    from radis.io.hdf5 import hdf2df, HDF5Manager
+    from radis.io.hdf5 import DataFileManager
     from radis.io.cache_files import check_not_deprecated
 
 from datetime import date
@@ -91,39 +91,29 @@ def __init__(
         molecule,
         local_databases,
         engine,
+        extra_params=None,
         verbose=False,
         parallel=True,
         nJobs=-2,
         batch_size="auto",
     ):
-        from os import environ
-
         if engine == "default":
             from radis import config
 
             engine = config["MEMORY_MAPPING_ENGINE"]  # 'pytables', 'vaex', 'feather'
-            # Quick fix for #401, #404
             if 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):
-                    engine = "pytables"  # for HITRAN and HITEMP databases
-                    if verbose >= 3:
-                        print(
-                            f"ReadTheDocs environment detected. Memory-mapping-engine set to '{engine}'. See https://github.com/radis/radis/issues/404"
-                        )
-                else:
-                    engine = "vaex"
+                engine = "vaex"
 
         self.name = name
         self.molecule = molecule
         self.local_databases = local_databases
+        self.extra_params = extra_params
         # create folder if needed
         if not exists(local_databases):
             from radis.misc.basics import make_folders
 
-            make_folders(*split(abspath(dirname(local_databases))))
-            make_folders(*split(abspath(local_databases)))
+            make_folders(*split(abspath(dirname(expanduser(local_databases)))))
+            make_folders(*split(abspath(expanduser(local_databases))))
 
         if self.is_registered():
             registered_paths = getDatabankEntries(self.name)["path"]
@@ -324,8 +314,10 @@ def register(self, dict_entries):
             self.verbose,
         )
 
-    def get_hdf5_manager(self):
-        return HDF5Manager(engine=self.engine)
+    def get_datafile_manager(self, engine=None):
+        if engine is None:
+            engine = self.engine
+        return DataFileManager(engine=engine)
 
     def download_and_parse(self, urlnames, local_files):
         all_local_files, _ = self.get_filenames()
@@ -425,10 +417,10 @@ def clean_download_files(self):
     def load(
         self,
         local_files,
-        isotope,
-        columns,
-        load_wavenum_min,
-        load_wavenum_max,
+        columns=None,
+        lower_bound=[],
+        upper_bound=[],
+        within=[],
         output="pandas",
     ):
         """
@@ -439,20 +431,31 @@ def load(
         output: 'pandas', 'vaex', 'jax'
             format of the output DataFrame. If ``'jax'``, returns a dictionary of
             jax arrays.
+        lower_bound: list of tuples [(column, lower_bound), etc.]
+            ::
+
+                lower_bound =[("wav", load_wavenum_min)]
+        upper_bound_bound: list of tuples [(column, upper_bound), etc.]
+            ::
+
+                upper_bound=[("wav", load_wavenum_max)]
+        within: list of tuples [(column, within_list), etc.]
+            ::
+
+                within=[("iso", isotope.split(","))]
         """
         engine = self.engine
-        if engine == "pytables":
+        mgr = self.get_datafile_manager()
+        if engine in ["pytables", "feather"]:
             df_all = []
             for local_file in local_files:
                 df_all.append(
-                    hdf2df(
+                    mgr.load(
                         local_file,
                         columns=columns,
-                        isotope=isotope,
-                        load_wavenum_min=load_wavenum_min,
-                        load_wavenum_max=load_wavenum_max,
-                        verbose=self.verbose,
-                        engine=engine,
+                        lower_bound=lower_bound,
+                        upper_bound=upper_bound,
+                        within=within,
                         output=output,
                     )
                 )
@@ -460,14 +463,12 @@ def load(
 
         elif engine == "vaex":
             # vaex can open several files at the same time:
-            return hdf2df(
+            return mgr.load(
                 local_files,
                 columns=columns,
-                isotope=isotope,
-                load_wavenum_min=load_wavenum_min,
-                load_wavenum_max=load_wavenum_max,
-                verbose=self.verbose,
-                engine=engine,
+                lower_bound=lower_bound,
+                upper_bound=upper_bound,
+                within=within,
                 output=output,
             )
         else:
@@ -503,6 +504,41 @@ def get_nrows(self, local_file):
             raise ValueError(engine)
         return nrows
 
+    def get_columns(self, local_file):
+        """Get all columns using DataFileManager class get_columns function"""
+        return self.get_datafile_manager().get_columns(local_file)
+
+    def add_column(self, df, key, value):
+        """Create column ``key`` in DataFrame or dictionary ``df`` with value ``value``"""
+        from radis.misc.basics import is_number
+
+        if is_number(self.alpha_ref):
+            import vaex
+
+            if isinstance(df, vaex.dataframe.DataFrameLocal):
+                # see https://github.com/vaexio/vaex/pull/1570
+                df[key] = vaex.vconstant(float(value), length=len(df))
+            else:
+                df[key] = value
+        else:
+            df[key] = value
+
+    def rename_columns(self, df, rename_dict):
+        """Example::
+
+        mdb.rename_columns(df, {"nu_lines":"wav"})
+        """
+        import vaex
+
+        if isinstance(df, vaex.dataframe.DataFrameLocal):
+            for k, v in rename_dict.items():
+                df.rename(k, v)
+        elif isinstance(df, pd.DataFrame):
+            df.rename(columns=rename_dict, inplace=True)
+        elif isinstance(df, dict):
+            for k, v in rename_dict.items():
+                df[v] = df.pop(k)
+
 
 def register_database(databank_name, dict_entries, verbose):
     """Add to registered databases in RADIS config file.
diff --git a/radis/io/exomol.py b/radis/io/exomol.py
index 37d367d02..29b865476 100644
--- a/radis/io/exomol.py
+++ b/radis/io/exomol.py
@@ -6,23 +6,26 @@
 code (which you should also have a look at !), by @HajimeKawahara, under MIT License.
 
 """
+import os
 import pathlib
 import warnings
 
 import numpy as np
 
-# from exojax.spec import hapi, exomolapi, exomol
-# from exojax.spec.hitran import gamma_natural as gn
-import pandas as pd
-
 try:
     from . import exomolapi
+    from .dbmanager import DatabaseManager
     from .exomol_utils import e2s
 except ImportError:  # if local import
     from radis.io import exomolapi
+    from radis.io.dbmanager import DatabaseManager
     from radis.io.exomol_utils import e2s
 
-from radis.db.classes import get_molecule_identifier
+from radis.db.classes import (
+    EXOMOL_MOLECULES,
+    EXOMOL_ONLY_ISOTOPES_NAMES,
+    get_molecule_identifier,
+)
 
 EXOMOL_URL = "http://www.exomol.com/db/"
 
@@ -51,7 +54,10 @@ def get_exomol_full_isotope_name(molecule, isotope):
     --------
     :py:func:`~radis.io.exomol.get_exomol_database_list`"""
 
-    from radis.db.classes import EXOMOL_ONLY_ISOTOPES_NAMES
+    if molecule not in EXOMOL_MOLECULES:
+        raise ValueError(
+            f"Molecule {molecule} not available in ExoMol. Change database, or choose one of ExoMol available molecules: {sorted(EXOMOL_MOLECULES)}"
+        )
 
     if (molecule, isotope) in EXOMOL_ONLY_ISOTOPES_NAMES:
         return EXOMOL_ONLY_ISOTOPES_NAMES[(molecule, isotope)]
@@ -63,7 +69,24 @@ def get_exomol_full_isotope_name(molecule, isotope):
         return mp.get(molecule, isotope, "isotope_name_exomol")
 
 
-def get_exomol_database_list(molecule, isotope_full_name):
+def get_list_of_known_isotopes(molecule):
+    """find all isotopes until error ensues"""
+
+    i = 1
+    isotope_list = []
+    while True:
+        try:
+            iso_name = get_exomol_full_isotope_name(molecule, i)
+        except:
+            break
+        else:
+            isotope_list.append(iso_name)
+        finally:
+            i += 1
+    return isotope_list
+
+
+def get_exomol_database_list(molecule, isotope_full_name=None):
     """Parse ExoMol website and return list of available databases, and recommended database
 
     Parameters
@@ -75,11 +98,13 @@ def get_exomol_database_list(molecule, isotope_full_name):
 
     Returns
     -------
+    list of databases, database recommended by ExoMol
 
     Examples
     --------
     Get CH4 from ExoMol :
     ::
+
         databases, recommended = get_exomol_database_list("CH4", "12C-1H4")
         >>> ['xsec-YT10to10', 'YT10to10', 'YT34to10'], 'YT34to10'
 
@@ -99,11 +124,20 @@ def get_exomol_database_list(molecule, isotope_full_name):
     :py:func:`~radis.io.exomol.get_exomol_full_isotope_name`
     """
 
+    if isotope_full_name is None:
+        raise ValueError(
+            f"Give isotope name. List of known isotopes for {molecule} : {get_list_of_known_isotopes(molecule)}"
+        )
+
     url = f"https://exomol.com/data/molecules/{molecule}/{isotope_full_name}"
     try:
         response = urlopen(url).read()
     except HTTPError as err:
-        raise ValueError(f"HTTPError opening url={url}") from err
+        if isotope_full_name not in get_list_of_known_isotopes(molecule):
+            extra = f". Isotope name {isotope_full_name} is not in list of known isotopes : {get_list_of_known_isotopes(molecule)}"
+        else:
+            extra = ""
+        raise ValueError(f"HTTPError opening url={url}" + extra) from err
 
     soup = BeautifulSoup(
         response, features="lxml"
@@ -134,7 +168,12 @@ def get_exomol_database_list(molecule, isotope_full_name):
 
     databases = databases + databases_recommended
 
-    return databases, databases_recommended[0]
+    if len(databases_recommended) > 0:
+        recommended_database = databases_recommended[0]
+    else:
+        recommended_database = False
+
+    return databases, recommended_database
 
 
 # def fetch_exomol_molecule_list():
@@ -226,6 +265,8 @@ def fetch_exomol(
     return_local_path=False,
     return_partition_function=False,
     engine="default",
+    output="pandas",
+    skip_optional_data=True,
 ):
     """Stream ExoMol file from EXOMOL website. Unzip and build a HDF5 file directly.
 
@@ -270,9 +311,9 @@ def fetch_exomol(
 
     Other Parameters
     ----------------
-    cache: bool, or ``'regen'``
+    cache: bool, or ``'regen'`` or ``'force'``
         if ``True``, use existing HDF5 file. If ``False`` or ``'regen'``, rebuild it.
-        Default ``True``.
+        If ``'force'``, crash if not cache file found. Default ``True``.
     verbose: bool
     clean_cache_files: bool
         if ``True`` clean downloaded cache files after HDF5 are created.
@@ -282,6 +323,30 @@ def fetch_exomol(
         if ``True``, also returns a :py:class:`~radis.levels.partfunc.PartFuncExoMol` object.
     engine: 'vaex', 'feather'
         which memory-mapping library to use. 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. If ``'vaex'``, output is a :py:class:`vaex.dataframe.DataFrameLocal`
+
+        .. note::
+            Vaex DataFrames are memory-mapped. They do not take any space in RAM
+            and are extremelly useful to deal with the largest databases.
+
+    skip_optional_data : bool
+        If False, fetch all fields which are marked as available in the ExoMol definition
+        file. If True, load only the first 4 columns of the states file
+        ("i", "E", "g", "J"). The structure of the columns above 5 depend on the
+        the definitions file (*.def) and the Exomol version.
+        If ``skip_optional_data=False``, two errors may occur:
+
+            - a field is marked as present/absent in the *.def field but is
+              absent/present in the *.states file (ie both files are inconsistent).
+            - in the updated version of Exomol, new fields have been added in the
+              states file of some species. But it has not been done for all species,
+              so both structures exist. For instance, the states file of
+              https://exomol.com/data/molecules/HCl/1H-35Cl/HITRAN-HCl/ follows the
+              structure described in [1]_, unlike the states file of
+              https://exomol.com/data/molecules/NO/14N-16O/XABC/ which follows the
+              structure described in [2]_.
 
     Returns
     -------
@@ -306,6 +371,12 @@ def fetch_exomol(
     the expected wavenumber range & isotopes are returned. The .HFD5 parsing uses
     :py:func:`~radis.io.hdf5.hdf2df`
 
+    References
+    ----------
+
+    .. [1] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Barton, E. J., Chubb, K. L., Coles, P. A., … Zak, E. (2016). The ExoMol database: molecular line lists for exoplanet and other hot atmospheres. https://doi.org/10.1016/j.jms.2016.05.002
+    .. [2] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Clark, V. H. J., Chubb, K. L., Conway, E. K., … Yurchenko, O. P. (2020). The 2020 release of the ExoMol database: Molecular line lists for exoplanet and other hot atmospheres. Journal of Quantitative Spectroscopy and Radiative Transfer, 255, 107228. https://doi.org/10.1016/j.jqsrt.2020.107228
+
     See Also
     --------
     :py:func:`~radis.io.hitran.fetch_hitran`, :py:func:`~radis.io.hitemp.fetch_hitemp`
@@ -363,41 +434,115 @@ def fetch_exomol(
         / database
     )
 
-    if load_wavenum_min is None:
-        load_wavenum_min = -np.inf
-    if load_wavenum_max is None:
-        load_wavenum_max = np.inf
-    mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
+    # TODO: add deprecation if missing columns in cache file
+
+    # Init database, download files if needed.
+    mdb = MdbExomol(
+        local_path,
+        molecule=molecule,
+        name=databank_name,
+        local_databases=local_databases,
+        nurange=[
+            load_wavenum_min if load_wavenum_min is not None else 0.0,
+            load_wavenum_max if load_wavenum_max is not None else np.inf,
+        ],
+        engine=engine,
+        cache=cache,
+        skip_optional_data=skip_optional_data,
+    )
 
-    # Attributes of the DataFrame
-    from radis.db.classes import HITRAN_MOLECULES
+    # Get local files
+    local_files = mdb.trans_file
+    if not isinstance(local_files, list):
+        local_files = [local_files]
+    mgr = mdb.get_datafile_manager()
+    local_files = [mgr.cache_file(f) for f in local_files]
+
+    # Specific for RADIS : rename columns
+    radis2exomol_columns = {
+        "wav": "nu_lines",
+        "airbrd": "alpha_ref",
+        "El": "elower",
+        "ju": "jupper",
+        "jl": "jlower",
+        "gp": "gupper",
+        "gpp": "glower",
+        "Tdpair": "n_Texp",
+    }
+    # get column name converting to exomol/exojax format if possible, else use the same
+    if columns is not None:
+        columns_exomol = [radis2exomol_columns.get(c, c) for c in columns] + [
+            "Sij0",
+            "jlower",
+            "jupper",
+        ]  # needed for broadening
+    else:
+        columns_exomol = None
+
+    df = mdb.load(
+        local_files,
+        columns=columns_exomol,
+        lower_bound=([("nu_lines", load_wavenum_min)] if load_wavenum_min else [])
+        + ([("Sij0", mdb.crit)] if not np.isneginf(mdb.crit) else []),
+        upper_bound=([("nu_lines", load_wavenum_max)] if load_wavenum_max else []),
+        output=output,
+    )
 
-    attrs = {}
-    if molecule in HITRAN_MOLECULES:
-        attrs["id"] = get_molecule_identifier(
-            molecule
-        )  # HITRAN id-number (if available)
-    attrs["molecule"] = molecule
-    attrs["iso"] = isotope
+    if "jlower" not in df:
+        raise KeyError(
+            f"jlower not found. Maybe try to delete cache file {local_files} and restart?"
+        )
 
-    df = mdb.to_df(attrs=attrs)
+    # Add broadening
+    mdb.set_broadening(df, output=output)
 
-    # Replace Linestrength with Line Intensity taking into account Terrestrial isotopic abundance
-    from radis.db.molparam import MolParams
+    # Specific for RADIS :
+    # ... Get RADIS column names:
+    exomol2radis_columns = {v: k for k, v in radis2exomol_columns.items()}
+    mdb.rename_columns(df, {k: v for k, v in exomol2radis_columns.items() if k in df})
 
-    try:
-        Ia = MolParams().get(molecule, isotope, "abundance")
-    except NotImplementedError:
-        from radis import config
+    assert "wav" in df
+
+    # ... include isotopic abundance in linestrength :
+    # Note : ExoMol treats isotopes as independant molecules ; linestrength is not
+    # corrected by isotopic abundance.
+    # Below, replace Linestrength with Line Intensity taking into account
+    # Terrestrial isotopic abundance (to be compatible with HITRAN/HITEMP/etc. )
+    from radis.db.molparam import MOLPARAMS_EXTRA_PATH, MolParams
 
-        Ia = config["molparams"]["abundances"][molecule][str(isotope)]
+    Ia = MolParams(extra_file_json=MOLPARAMS_EXTRA_PATH).get(
+        molecule, isotope, "abundance"
+    )
 
-    df["Sij0"] *= Ia
-    df.rename(columns={"Sij0": "int"}, inplace=True)
+    if output == "jax":
+        try:
+            import jax.numpy as jnp
+        except:
+            import numpy as jnp
+        df["logsij0"] += jnp.log(Ia)
+    else:
+        df["Sij0"] *= Ia
+        mdb.rename_columns(df, {"Sij0": "int"})
 
-    # Return:
-    out = [df]
+    # Add Attributes of the DataFrame
+    if output == "pandas":  # no attribtes in "Jax" or "Vaex" mode
+        from radis.db.classes import HITRAN_MOLECULES
 
+        attrs = {}
+        if molecule in HITRAN_MOLECULES:
+            attrs["id"] = get_molecule_identifier(
+                molecule
+            )  # HITRAN id-number (if available)
+        attrs["molecule"] = molecule
+        attrs["iso"] = isotope
+
+        for k, v in attrs.items():
+            df.attrs[k] = v
+
+    # Return:
+    out = df
+    if return_local_path or return_partition_function:
+        out = [out]
     if return_local_path:
         out.append(str(mdb.path))
     if return_partition_function:
@@ -407,13 +552,88 @@ def fetch_exomol(
     return out
 
 
-class MdbExomol(object):
+class MdbExomol(DatabaseManager):
+
     """molecular database of ExoMol
 
     MdbExomol is a class for ExoMol.
 
     Parameters
     ----------
+    path: str
+        path for Exomol data directory/tag. For instance, "/home/CO/12C-16O/Li2015"
+    nurange: array
+        wavenumber range list (cm-1) or wavenumber array
+    margin: float
+        margin for nurange (cm-1)
+    crit: float
+        line strength lower limit for extraction
+    bkgdatm: str
+        background atmosphere for broadening. e.g. H2, He,
+    broadf: bool
+        if False, the default broadening parameters in .def file is used
+
+    Other Parameters
+    ----------------
+    engine : str
+        which memory mapping engine to use : 'vaex', 'pytables' (HDF5), 'feather'
+    skip_optional_data : bool
+        If False, fetch all fields which are marked as available in the ExoMol definition
+        file. If True, load only the first 4 columns of the states file
+        ("i", "E", "g", "J"). The structure of the columns above 5 depend on the
+        the definitions file (*.def) and the Exomol version.
+        If ``skip_optional_data=False``, two errors may occur:
+
+            - a field is marked as present/absent in the *.def field but is
+              absent/present in the *.states file (ie both files are inconsistent).
+            - in the updated version of Exomol, new fields have been added in the
+              states file of some species. But it has not been done for all species,
+              so both structures exist. For instance, the states file of
+              https://exomol.com/data/molecules/HCl/1H-35Cl/HITRAN-HCl/ follows the
+              structure described in [1]_, unlike the states file of
+              https://exomol.com/data/molecules/NO/14N-16O/XABC/ which follows the
+              structure described in [2]_.
+
+    Notes
+    -----
+
+    The trans/states files can be very large. For the first time to read it,
+    we convert it to the feather or hdf5-format. After the second-time,
+    we use the feather/hdf5 format instead.
+
+    Examples
+    --------
+    ::
+
+        # Init database, download files if needed.
+        mdb = MdbExomol(
+            local_path,
+            molecule=molecule,
+            name=databank_name,
+            local_databases=local_databases,
+            # nurange=[load_wavenum_min, load_wavenum_max],
+            engine="vaex",
+        )
+
+        # Get cache files to load :
+        mgr = mdb.get_datafile_manager()
+        local_files = [mgr.cache_file(f) for f in mdb.trans_file]
+
+        # Load files
+        df = mdb.load(
+            local_files,
+            columns=columns_exomol,
+            lower_bound=([('nu_lines', load_wavenum_min)] if load_wavenum_min else []) + ([("Sij0", mdb.crit)] if not np.isneginf(mdb.crit) else []),
+            upper_bound=([('nu_lines', load_wavenum_max)] if load_wavenum_max else []),
+            output="jax", # or "pytables", "vaex"
+        )
+
+    .. minigallery:: radis.fetch_exomol
+
+
+    DataFrame columns
+    -----------------
+
     nu_lines (nd array): line center (cm-1)
     Sij0 (nd array): line strength at T=Tref (cm)
     dev_nu_lines (np array): line center in device (cm-1)
@@ -428,7 +648,11 @@ class MdbExomol(object):
     n_Tref_def: default temperature exponent in .def file, used for jlower not given in .broad
     alpha_ref_def: default alpha_ref (gamma0) in .def file, used for jlower not given in .broad
 
-    .. minigallery:: radis.fetch_exomol
+    References
+    ----------
+
+    .. [1] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Barton, E. J., Chubb, K. L., Coles, P. A., … Zak, E. (2016). The ExoMol database: molecular line lists for exoplanet and other hot atmospheres. https://doi.org/10.1016/j.jms.2016.05.002
+    .. [2] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Clark, V. H. J., Chubb, K. L., Conway, E. K., … Yurchenko, O. P. (2020). The 2020 release of the ExoMol database: Molecular line lists for exoplanet and other hot atmospheres. Journal of Quantitative Spectroscopy and Radiative Transfer, 255, 107228. https://doi.org/10.1016/j.jqsrt.2020.107228
 
     """
 
@@ -441,64 +665,42 @@ class MdbExomol(object):
     def __init__(
         self,
         path,
-        nurange=[-np.inf, np.inf],
+        molecule,
+        database=None,
+        local_databases=None,
+        name="EXOMOL-{molecule}",
+        nurange=[0.0, np.inf],
         margin=1.0,
         crit=-np.inf,
-        bkgdatm="H2",  # TODO: use Air whenever possible, to be consistent with HITRAN/HITEMP
+        bkgdatm="Air",  # TODO: use Air whenever possible, to be consistent with HITRAN/HITEMP
         broadf=True,
         engine="vaex",
         verbose=True,
+        cache=True,
+        skip_optional_data=True,
     ):
-        """Molecular database for Exomol form
-
-        Parameters
-        ----------
-        path: path for Exomol data directory/tag. For instance, "/home/CO/12C-16O/Li2015"
-        nurange: wavenumber range list (cm-1) or wavenumber array
-        margin: margin for nurange (cm-1)
-        crit: line strength lower limit for extraction
-        bkgdatm: background atmosphere for broadening. e.g. H2, He,
-        broadf: if False, the default broadening parameters in .def file is used
-
-        Notes
-        -----
-
-        The trans/states files can be very large. For the first time to read it,
-        we convert it to the feather or hdf5-format. After the second-time,
-        we use the feather/hdf5 format instead.
+        super().__init__(
+            name,
+            molecule,
+            local_databases,
+            engine,
+            verbose=verbose,
+        )
 
-        """
-        from os import environ
+        assert cache  # cache only used for cache='regen' or cache='force' modes, cache=False is not expected
 
         if engine == "default":
             from radis import config
 
             engine = config["MEMORY_MAPPING_ENGINE"]
-            # Quick fix for #401, #404
             if 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):
-                    engine = "feather"
-                    if verbose >= 3:
-                        print(
-                            f"ReadTheDocs environment detected. Memory-mapping-engine set to '{engine}'. See https://github.com/radis/radis/issues/404"
-                        )
-                else:
-                    engine = "vaex"
-
-        if engine == "vaex":
-            import vaex
-        elif engine == "feather":
-            pass
-            # vaex will be the future default, but it still fails on some systems
-            # Ex : on Spyder : See https://github.com/spyder-ide/spyder/issues/16183
-            # We keep "feather" as backup)
-        else:
-            raise NotImplementedError(f"{engine} is not implemented")
-        self.engine = engine  # which memory mapping engine to use : 'vaex', 'pytables' (HDF5), 'feather'
+                engine = "vaex"
 
         self.path = pathlib.Path(path)
+
+        if local_databases is not None:
+            self.path = pathlib.Path(local_databases).expanduser() / self.path
+
         t0 = self.path.parents[0].stem
         molec = t0 + "__" + str(self.path.stem)
         self.bkgdatm = bkgdatm
@@ -508,6 +710,7 @@ def __init__(
         self.crit = crit
         self.margin = margin
         self.nurange = [np.min(nurange), np.max(nurange)]
+        self.wmin, self.wmax = self.nurange
         self.broadf = broadf
         # Where exomol files are
         self.states_file = self.path / pathlib.Path(molec + ".states.bz2")
@@ -528,62 +731,57 @@ def __init__(
         tag = molec.split("__")
         self.isotope_fullname = tag[0]
         self.molecule = e2s(tag[0])
-        self.isotope = 1  # Placeholder. TODO : impement parsing of other isotopes.
+        # self.isotope = 1  # Placeholder. TODO : impement parsing of other isotopes.
 
         # load def
-        # @minou: I feel this next line should just update the self.stuff
         dic_def = exomolapi.read_def(self.def_file)  # approx. 3 ms
         self.n_Texp_def = dic_def["n_Texp"]
         self.alpha_ref_def = dic_def["alpha_ref"]
-        self.molmass = dic_def["molmass"]
 
         #  default n_Texp value if not given
         if self.n_Texp_def is None:
+            self.n_Texp_def = 0.5
             warnings.warn(
                 Warning(
-                    """
-                    No default broadening exponent in def file. Assigned n = 0.5
+                    f"""
+                    No default broadening exponent in def file. Assigned n = {self.n_Texp_def}
                     """
                 )
             )
-            self.n_Texp_def = 0.5
         #  default alpha_ref value if not given
         if self.alpha_ref_def is None:
+            self.alpha_ref_def = 0.07
             warnings.warn(
                 Warning(
-                    """
-                    No default broadening in def file. Assigned alpha_ref = 0.07
+                    f"""
+                    No default broadening in def file. Assigned alpha_ref = {self.alpha_ref_def}
                     """
                 )
             )
-            self.alpha_ref_def = 0.07
 
         # load states
-        if engine == "feather":
-            if self.states_file.with_suffix(".feather").exists():
-                states = pd.read_feather(self.states_file.with_suffix(".feather"))
-            else:
-                print(
-                    "Note: Caching states data to the feather format. After the second time, it will become much faster."
-                )
-                states = exomolapi.read_states(self.states_file, dic_def, engine="csv")
-                states.to_feather(self.states_file.with_suffix(".feather"))
-            ndstates = states.to_numpy()[
-                :, :4
-            ]  # the i, E, g, J are in the 4 first columns
-        elif engine == "vaex":
-            if self.states_file.with_suffix(".bz2.hdf5").exists():
-                states_file_cache = self.states_file.with_suffix(".bz2.hdf5")
-                states = vaex.open(states_file_cache)
-            elif self.states_file.with_suffix(".hdf5").exists():
-                states_file_cache = self.states_file.with_suffix(".hdf5")
-                states = vaex.open(states_file_cache)
-            else:
-                print(
-                    "Note: Caching states data to the hdf5 format with vaex. After the second time, it will become much faster."
+        mgr = self.get_datafile_manager()
+        if cache == "regen" and mgr.cache_file(self.states_file).exists():
+            if verbose:
+                print("Removing existing file ", mgr.cache_file(self.states_file))
+            os.remove(mgr.cache_file(self.states_file))
+        if mgr.cache_file(self.states_file).exists():
+            states = mgr.read(mgr.cache_file(self.states_file))
+        else:
+            if cache == "force":
+                raise ValueError(
+                    f"Cache file {str(mgr.cache_file(self.states_file))} does not exist"
                 )
-                states = exomolapi.read_states(self.states_file, dic_def, engine="vaex")
-            ndstates = vaex.array_types.to_numpy(states)
+            print(
+                f"Note: Caching states data to the {engine} format. After the second time, it will become much faster."
+            )
+            states = exomolapi.read_states(
+                self.states_file,
+                dic_def,
+                engine="vaex" if engine == "vaex" else "csv",
+                skip_optional_data=skip_optional_data,
+            )
+            mgr.write(mgr.cache_file(self.states_file), states)
 
         # load pf
         pf = exomolapi.read_pf(self.pf_file)
@@ -597,354 +795,109 @@ def __init__(
         self.Tref = 296.0
         self.QTref = np.array(self.QT_interp(self.Tref))
 
+        # Download files
+
+        # Generate list of files
+        # ---------------------
         # Case1 : transitions are stored in a single file
         if dic_def["numinf"] is None:
-            self.trans_file = self.path / pathlib.Path(molec + ".trans.bz2")
-            if not self.trans_file.exists():
-                self.download(molec, [".trans.bz2"])
-
-            if engine == "vaex":
-                if self.trans_file.with_suffix(".hdf5").exists():
-                    trans = vaex.open(self.trans_file.with_suffix(".hdf5"))
-                    # Apply filter
-                    cdt = 1
-                    if not np.isneginf(self.nurange[0]):
-                        cdt *= trans.nu_lines > self.nurange[0] - self.margin
-                    if not np.isinf(self.nurange[1]):
-                        cdt *= trans.nu_lines < self.nurange[1] - self.margin
-                    if not np.isneginf(self.crit):
-                        cdt *= trans.Sij0 > self.crit
-                    if cdt != 1:
-                        trans = trans[cdt]
-
-                    # mask has been alraedy applied
-                    mask_needed = False
-
-                else:
-                    print(
-                        "Note: Caching line transition data to the HDF5 format with vaex. After the second time, it will become much faster."
-                    )
-                    trans = exomolapi.read_trans(self.trans_file, engine="vaex")
-
-                    # Check validity
-                    b = trans.nu_lines.isnan()  # TODO: check if can be made faster?
-                    if b.sum() > 0:
-                        raise NotImplementedError(
-                            "transition wavenumber not given. Has to be computed from upper/lower level energy look up"
-                        )
-                    # mask needs to be applied
-                    mask_needed = True
-
-                # TODO Load extra columns (precomputed Sij0, etc.)
-                # trans_file_extra = self.trans_file.with_name(
-                #    self.trans_file.stem + "_extra" + self.trans_file.suffix
-                # )
-                # if trans_file_extra.exists():
-                #    trans_extra = vaex.open(trans_file_extra)
-                #    # trans = trans.join(trans_extra)  # merge without column name will merge on rows basis
-
-                ndtrans = vaex.array_types.to_numpy(trans)
-
-            elif engine == "feather":
-                if self.trans_file.with_suffix(".feather").exists():
-                    trans = pd.read_feather(self.trans_file.with_suffix(".feather"))
-                else:
-                    print(
-                        "Note: Caching line transition data to the feather format. After the second time, it will become much faster."
-                    )
-                    trans = exomolapi.read_trans(self.trans_file, engine="csv")
-                    trans.to_feather(self.trans_file.with_suffix(".feather"))
-                ndtrans = trans.to_numpy()
-                # mask needs to be applied   (in feather mode we don't sleect wavneumbers)
-                mask_needed = True
-
-            # compute extra parameters (Sij0, gup and elower, etc.)
-            (
-                self._A,
-                self.nu_lines,
-                self._elower,
-                self._gpp,
-                self._jlower,
-                self._jupper,
-                mask_zeronu,
-                self._quantumNumbers,
-            ) = exomolapi.pickup_gE(ndstates, ndtrans, self.trans_file, dic_def)
-
-            if engine == "vaex" and self.trans_file.with_suffix(".hdf5").exists():
-                # if engine == 'feather' we recompute all the time
-                # Todo : we should get the column name instead ?
-                self.Sij0 = ndtrans[:, 4]  # TODO: Refactor remove array index lookups.
-                # TODO Refactor. Add extra here:   trans = trans.join(trans_extra)
-            else:
-                ##Line strength:
-                from radis.lbl.base import (  # TODO: move elsewhere
-                    linestrength_from_Einstein,
-                )
-
-                self.Sij0 = linestrength_from_Einstein(
-                    A=self._A,
-                    gu=self._gpp,
-                    El=self._elower,
-                    Ia=1,  #  Sij0 is a linestrength calculated without taking into account isotopic abundance (unlike line intensity parameter of HITRAN. In RADIS this is corrected for in fetch_exomol()  )
-                    nu=self.nu_lines,
-                    Q=self.QTref,
-                    T=self.Tref,
-                )
-
-                trans["nu_positive"] = mask_zeronu
-                if engine == "vaex":
-                    # exclude the lines whose nu_lines evaluated inside exomolapi.pickup_gE (thus sometimes different from the "nu_lines" column in trans) is not positive
-
-                    trans = trans[trans.nu_positive].extract()
-                    trans.drop("nu_positive", inplace=True)
-                else:
-                    if False in mask_zeronu:
-                        raise NotImplementedError(
-                            "some wavenumber is not defined;  masking not impleemtend so far in 'feather' engine"
-                        )
-
-                trans["nu_lines"] = self.nu_lines
-                trans["Sij0"] = self.Sij0
-
-                if engine == "vaex":
-                    trans.export(self.trans_file.with_suffix(".hdf5"))
-                #  TODO : implement masking in 'feather' mode
+            self.trans_file = [self.path / pathlib.Path(molec + ".trans.bz2")]
+            self.num_tag = [None]
 
         # Case2 : Transitions are stored in multiple files:
         else:  # dic_def["numinf"] is not None
-            imin = (
-                np.searchsorted(dic_def["numinf"], nurange[0], side="right") - 1
-            )  # left side
-            imax = (
-                np.searchsorted(dic_def["numinf"], nurange[1], side="right") - 1
-            )  # left side
+            # dic_def["numinf"] contains the limit points ``[w(0), w(1), ..., w(n)]``
+            # (n+1 elements) defining the spectral ranges appearing in the list
+            # dic_def["numtag"] which looks like ``["w(0)-w(1)", "w(1)-w(2)", ..., w(n-1)-w(n)]``
+            # (n elements)
+            # imin :
+            # index i of the "w(i)-w(i+1)" element in dic_def["numtag"] such
+            # that nurange[0]<=w(i)
+            imin = np.searchsorted(dic_def["numinf"], nurange[0], side="right") - 1
+            # imax :
+            # index i of the "w(i)-w(i+1)" element in dic_def["numtag"] such
+            # that w(i+1)<=nurange[1]
+            imax = np.searchsorted(dic_def["numinf"], nurange[1], side="right") - 2
             self.trans_file = []
+            self.num_tag = []
             for k, i in enumerate(range(imin, imax + 1)):
                 trans_file = self.path / pathlib.Path(
                     molec + "__" + dic_def["numtag"][i] + ".trans.bz2"
                 )
-                if not trans_file.exists():
-                    self.download(
-                        molec, extension=[".trans.bz2"], numtag=dic_def["numtag"][i]
-                    )
-                if engine == "feather":
-                    trans_feather_path = trans_file.with_suffix(".bz2.feather")
-                    if trans_feather_path.exists():
-                        trans = pd.read_feather(trans_feather_path)
-                    else:
-                        print(
-                            "Note: Caching line transition data to the feather format. After the second time, it will become much faster."
-                        )
-                        trans = exomolapi.read_trans(trans_file, engine="csv")
-                        trans.to_feather(trans_feather_path)
-                        #!!TODO:restrict NOW the trans size to avoid useless overload of memory and CPU
-                        # trans = trans[(trans['nu'] > self.nurange[0] - self.margin) & (trans['nu'] < self.nurange[1] + self.margin)]
-                    ndtrans = trans.to_numpy()
-                    # mask needs to be applied   (in feather mode we don't sleect wavneumbers)
-                    mask_needed = True
-                elif engine == "vaex":
-                    if trans_file.with_suffix(".hdf5").exists():
-                        trans = vaex.open(trans_file.with_suffix(".hdf5"))
-                        cdt = 1
-                        if not np.isneginf(self.nurange[0]):
-                            cdt *= trans.nu_lines > self.nurange[0] - self.margin
-                        if not np.isinf(self.nurange[1]):
-                            cdt *= trans.nu_lines < self.nurange[1] - self.margin
-                        if not np.isneginf(self.crit):
-                            cdt *= trans.Sij0 > self.crit
-                        if cdt != 1:
-                            trans = trans[cdt]
-
-                        # mask has been already applied
-                        mask_needed = False
-                    else:
-                        print(
-                            "Note: Caching line transition data to the HDF5 format with vaex. After the second time, it will become much faster."
-                        )
-                        trans = exomolapi.read_trans(trans_file, engine="vaex")
-
-                        # Check validity
-                        b = trans.nu_lines.isnan()  # TODO: check if can be made faster?
-                        if b.sum() > 0:
-                            raise NotImplementedError(
-                                "transition wavenumber not given. Has to be computed from upper/lower level energy look up"
-                            )
-
-                        # TODO Load extra columns (precomputed Sij0, etc.)
-                        # trans_file_extra = self.trans_file.with_name(
-                        #    self.trans_file.stem + "_extra" + self.trans_file.suffix
-                        # )
-                        # if trans_file_extra.exists():
-                        #    trans_extra = vaex.open(trans_file_extra)
-                        #    trans = trans.join(
-                        #        trans_extra
-                        #    )  # merge without column name will merge on rows basis
-
-                        # mask needs to be applied
-                        mask_needed = True
-
-                    ndtrans = vaex.array_types.to_numpy(trans)
                 self.trans_file.append(trans_file)
+                self.num_tag.append(dic_def["numtag"][i])
+
+        # Look-up missing parameters and write file
+        # -----------------------------------------
+        for trans_file, num_tag in zip(self.trans_file, self.num_tag):
+
+            if cache == "regen" and mgr.cache_file(trans_file).exists():
+                if verbose:
+                    print("Removing existing file ", mgr.cache_file(trans_file))
+                os.remove(mgr.cache_file(trans_file))
+
+            if not mgr.cache_file(trans_file).exists():
+
+                if cache == "force":
+                    raise ValueError(
+                        f"Cache file {str(mgr.cache_file(trans_file))} does not exist"
+                    )
+
+                if not trans_file.exists():
+                    self.download(molec, extension=[".trans.bz2"], numtag=num_tag)
+                    # TODO: add option to delete file at the end
+
+                print(
+                    f"Note: Caching line transition data to the {engine} format. After the second time, it will become much faster."
+                )
+                trans = exomolapi.read_trans(
+                    trans_file, engine="vaex" if engine == "vaex" else "csv"
+                )
 
                 # Complete transition data with lookup on upper & lower state :
                 # In particular, compute gup and elower
-                if k == 0:
-                    (
-                        self._A,
-                        self.nu_lines,
-                        self._elower,
-                        self._gpp,
-                        self._jlower,
-                        self._jupper,
-                        mask_zeronu,
-                        self._quantumNumbers,
-                    ) = exomolapi.pickup_gE(ndstates, ndtrans, trans_file, dic_def)
-
-                    if engine == "vaex" and trans_file.with_suffix(".hdf5").exists():
-                        # Sij0x already stored in cache file
-                        self.Sij0 = ndtrans[:, 4]
-                    else:
-                        ##Recompute Line strength:
-                        from radis.lbl.base import (  # TODO: move elsewhere
-                            linestrength_from_Einstein,
-                        )
-
-                        self.Sij0 = linestrength_from_Einstein(
-                            A=self._A,
-                            gu=self._gpp,
-                            El=self._elower,
-                            Ia=1,  #  Sij0 is a linestrength calculated without taking into account isotopic abundance (unlike line intensity parameter of HITRAN. In RADIS this is corrected for in fetch_exomol()  )
-                            nu=self.nu_lines,
-                            Q=self.QTref,
-                            T=self.Tref,
-                        )
-
-                        # exclude the lines whose nu_lines evaluated inside exomolapi.pickup_gE (thus sometimes different from the "nu_lines" column in trans) is not positive
-                        if engine == "vaex":
-                            trans["nu_positive"] = mask_zeronu
-                            trans = trans[trans.nu_positive].extract()
-                            trans.drop("nu_positive", inplace=True)
-                        else:
-                            if False in mask_zeronu:
-                                raise NotImplementedError(
-                                    "some wavenumber is not defined;  masking not impleemtend so far in 'feather' engine"
-                                )
-
-                        trans["nu_lines"] = self.nu_lines
-                        trans["Sij0"] = self.Sij0
-                else:  # k!=0
-
-                    (
-                        Ax,
-                        nulx,
-                        elowerx,
-                        gppx,
-                        jlowerx,
-                        jupperx,
-                        mask_zeronu,
-                        quantumNumbersx,
-                    ) = exomolapi.pickup_gE(ndstates, ndtrans, trans_file, dic_def)
-                    if engine == "vaex" and trans_file.with_suffix(".hdf5").exists():
-                        # Sij0x already stored in cache file
-                        Sij0x = ndtrans[:, 4]
-                    else:
-                        ##Recompute Line strength:
-                        from radis.lbl.base import (  # TODO: move elsewhere
-                            linestrength_from_Einstein,
-                        )
-
-                        Sij0x = linestrength_from_Einstein(
-                            A=Ax,
-                            gu=gppx,
-                            El=elowerx,
-                            Ia=1,  #  Sij0 is a linestrength calculated without taking into account isotopic abundance (unlike line intensity parameter of HITRAN. In RADIS this is corrected for in fetch_exomol()  )
-                            nu=nulx,
-                            Q=self.QTref,
-                            T=self.Tref,
-                        )
-
-                        # exclude the lines whose nu_lines evaluated inside exomolapi.pickup_gE (thus sometimes different from the "nu_lines" column in trans) is not positive
-                        if engine == "vaex":
-                            trans["nu_positive"] = mask_zeronu
-                            trans = trans[trans.nu_positive].extract()
-                            trans.drop("nu_positive", inplace=True)
-                        else:
-                            if False in mask_zeronu:
-                                raise NotImplementedError(
-                                    "some wavenumber is not defined;  masking not impleemtend so far in 'feather' engine"
-                                )
-
-                        trans["nu_lines"] = nulx
-                        trans["Sij0"] = Sij0x
-
-                    self._A = np.hstack([self._A, Ax])
-                    self.nu_lines = np.hstack([self.nu_lines, nulx])
-                    self.Sij0 = np.hstack([self.Sij0, Sij0x])
-                    self._elower = np.hstack([self._elower, elowerx])
-                    self._gpp = np.hstack([self._gpp, gppx])
-                    self._jlower = np.hstack([self._jlower, jlowerx])
-                    self._jupper = np.hstack([self._jupper, jupperx])
-                    for key in self._quantumNumbers.keys():
-                        self._quantumNumbers[key] = np.hstack(
-                            [self._quantumNumbers[key], quantumNumbersx[key]]
-                        )
-
-                    #  TODO refactpr : store into "extra" file ; create column if needed ;if it did exist already make sure the value was computed for ALL lines
-                    if (
-                        engine == "vaex"
-                        and not trans_file.with_suffix(".hdf5").exists()
-                    ):
-                        trans.export(trans_file.with_suffix(".hdf5"))
-
-        ### MASKING ###
-        mask = (
-            (self.nu_lines > self.nurange[0] - self.margin)
-            * (self.nu_lines < self.nurange[1] + self.margin)
-            * (self.Sij0 > self.crit)
-        )
 
-        self.masking(mask, mask_needed)
+                exomolapi.pickup_gE(
+                    states,
+                    trans,
+                    trans_file,
+                    dic_def,
+                    skip_optional_data=skip_optional_data,
+                )
 
-    def masking(self, mask, mask_needed=True):
-        """applying mask and (re)generate jnp.arrays
+                ##Recompute Line strength:
+                from radis.lbl.base import (  # TODO: move elsewhere
+                    linestrength_from_Einstein,
+                )
 
-        Args:
-           mask: mask to be applied. self.mask is updated.
-           mask_needed: whether mask needs to be applied or not
+                self.Sij0 = linestrength_from_Einstein(
+                    A=trans["A"],
+                    gu=trans["gup"],
+                    El=trans["elower"],
+                    Ia=1,  #  Sij0 is a linestrength calculated without taking into account isotopic abundance (unlike line intensity parameter of HITRAN. In RADIS this is corrected for in fetch_exomol()  )
+                    nu=trans["nu_lines"],
+                    Q=self.QTref,
+                    T=self.Tref,
+                )
 
-        """
-        # TODO : with Vaex-HDF5, selection should happen on disk.
-
-        # numpy float 64 Do not convert them jnp array
-        if mask_needed:
-            self.nu_lines = self.nu_lines[mask]
-            self.Sij0 = self.Sij0[mask]
-            self._A = self._A[mask]
-            self._elower = self._elower[mask]
-            self._gpp = self._gpp[mask]
-            self._jlower = self._jlower[mask]
-            self._jupper = self._jupper[mask]
-            for key, array in self._quantumNumbers.items():
-                self._quantumNumbers[key] = array[mask]
-
-        # jnp arrays
-        self.dev_nu_lines = np.array(self.nu_lines)
-        self.logsij0 = np.array(np.log(self.Sij0))
-        self.A = np.array(self._A)
-        # self.gamma_natural = gn(self.A)   #  Natural Broadening neglected in RADIS
-        self.elower = np.array(self._elower)
-        self.gpp = np.array(self._gpp)
-        self.jlower = np.array(self._jlower, dtype=int)
-        self.jupper = np.array(self._jupper, dtype=int)
-        ##Broadening parameters
-        self.set_broadening()
-
-    def set_broadening(self, alpha_ref_def=None, n_Texp_def=None):
+                trans["Sij0"] = self.Sij0
+
+                mgr.write(mgr.cache_file(trans_file), trans)
+
+        # Database ready to be loaded.
+        # Proceed with mdb.load()
+
+    def set_broadening(self, df, alpha_ref_def=None, n_Texp_def=None, output=None):
         """setting broadening parameters
 
-        Args:
-           alpha_ref: set default alpha_ref and apply it. None=use self.alpha_ref_def
-           n_Texp_def: set default n_Texp and apply it. None=use self.n_Texp_def
+        Parameters
+        ----------
+        alpha_ref: set default alpha_ref and apply it. None=use self.alpha_ref_def
+        n_Texp_def: set default n_Texp and apply it. None=use self.n_Texp_def
+
+        Returns
+        -------
+        None. Updates ``df`` with ``alpha_ref``, ``n_Texp_def``
         """
         if alpha_ref_def:
             self.alpha_ref_def = alpha_ref_def
@@ -962,38 +915,42 @@ def set_broadening(self, alpha_ref_def=None, n_Texp_def=None):
                         bdat,
                         alpha_ref_default=self.alpha_ref_def,
                         n_Texp_default=self.n_Texp_def,
-                        jlower_max=np.max(self._jlower),
+                        jlower_max=np.max(df["jlower"]),
                     )
-                    self.alpha_ref = np.array(j2alpha_ref[self._jlower])
-                    self.n_Texp = np.array(j2n_Texp[self._jlower])
+                    self.alpha_ref = np.array(j2alpha_ref[df["jlower"]])
+                    self.n_Texp = np.array(j2n_Texp[df["jlower"]])
                 elif codelv == "a1":
                     j2alpha_ref, j2n_Texp = exomolapi.make_j2b(
                         bdat,
                         alpha_ref_default=self.alpha_ref_def,
                         n_Texp_default=self.n_Texp_def,
-                        jlower_max=np.max(self._jlower),
+                        jlower_max=np.max(df["jlower"]),
                     )
                     jj2alpha_ref, jj2n_Texp = exomolapi.make_jj2b(
                         bdat,
                         j2alpha_ref_def=j2alpha_ref,
                         j2n_Texp_def=j2n_Texp,
-                        jupper_max=np.max(self._jupper),
+                        jupper_max=np.max(df["jupper"]),
                     )
-                    self.alpha_ref = np.array(jj2alpha_ref[self._jlower, self._jupper])
-                    self.n_Texp = np.array(jj2n_Texp[self._jlower, self._jupper])
-            except:
+                    self.alpha_ref = np.array(jj2alpha_ref[df["jlower"], df["jupper"]])
+                    self.n_Texp = np.array(jj2n_Texp[df["jlower"], df["jupper"]])
+            except FileNotFoundError:
                 print(
                     "Warning: Cannot load .broad. The default broadening parameters are used."
                 )
                 self.alpha_ref = np.array(
-                    self.alpha_ref_def * np.ones_like(self._jlower)
+                    self.alpha_ref_def * np.ones_like(df["jlower"])
                 )
-                self.n_Texp = np.array(self.n_Texp_def * np.ones_like(self._jlower))
+                self.n_Texp = np.array(self.n_Texp_def * np.ones_like(df["jlower"]))
 
         else:
             print("The default broadening parameters are used.")
-            self.alpha_ref = np.array(self.alpha_ref_def * np.ones_like(self._jlower))
-            self.n_Texp = np.array(self.n_Texp_def * np.ones_like(self._jlower))
+            self.alpha_ref = np.array(self.alpha_ref_def * np.ones_like(df["jlower"]))
+            self.n_Texp = np.array(self.n_Texp_def * np.ones_like(df["jlower"]))
+
+        # Add values
+        self.add_column(df, "alpha_ref", self.alpha_ref)
+        self.add_column(df, "n_Texp", self.n_Texp)
 
     def QT_interp(self, T):
         """interpolated partition function
@@ -1064,88 +1021,24 @@ def download(self, molec, extension, numtag=None):
                 print("Downloading " + pfpath)
                 try:
                     urllib.request.urlretrieve(pfpath, str(self.path / pfname))
-                except:
-                    print("Error: Couldn't download " + ext + " file and save.")
+                except HTTPError:
+                    print(f"Error: Couldn't download {ext} file at {pfpath} and save.")
 
     def to_partition_function_tabulator(self):
+        """Generate a :py:class:`~radis.levels.partfunc.PartFuncExoMol` object"""
         from radis.levels.partfunc import PartFuncExoMol
 
         return PartFuncExoMol(self.isotope_fullname, self.T_gQT, self.gQT)
 
-    def to_df(self, attrs={}):
-        """Export Line Database to a RADIS-friendly Pandas DataFrame
-
-        Parameters
-        ----------
-        attrs: dict
-            add these as attributes of the DataFrame
-        """
-
-        ##Broadening parameters
-        self.set_broadening()
-
-        # TODO : define RADIS database format somewhere else; with description of the column names.
-        df = pd.DataFrame(
-            {
-                "wav": self.nu_lines,
-                "Sij0": self.Sij0,  #  linestrength (not corrected for isotopic abundance)
-                "A": self._A,
-                "airbrd": self.alpha_ref,  # temperature dependance exponent for air broadening
-                # "selbrd": None,                  # self-temperature dependant exponent. Not implementedi in ExoMol
-                "El": self._elower,
-                # "Tdpair": None,
-                # "Pshft": None,
-                "ju": self._jupper,
-                "jl": self._jlower,
-                "gpp": self._gpp,
-                "Tdpair": self.n_Texp,  # temperature dependance exponent. Here we use Tdair; no Tdpsel. TODO
-            }
-        )
-
-        # Check format :
-        for key, array in self._quantumNumbers.items():
-            try:
-                df[key] = pd.to_numeric(array, errors="raise")
-            except:
-                df[key] = array
-                df[key] = df[key].astype("str")
 
-        for k, v in attrs.items():
-            df.attrs[k] = v
-
-        assert self.Tref == 296  # default in RADIS
-
-        return df
-
-    def to_jax(self):
-        """Generate Jax arrays for use in ExoJax"""
-
-        pass  # TODO
+if __name__ == "__main__":
 
+    from radis.test.io.test_exomol import (
+        test_calc_exomol_spectrum,
+        test_calc_exomol_vs_hitemp,
+        test_exomol_parsing_functions,
+    )
 
-if __name__ == "__main__":
-    # mdb=MdbExomol("/home/kawahara/exojax/data/CO/12C-16O/Li2015/")
-    # mdb=MdbExomol("/home/kawahara/exojax/data/CH4/12C-1H4/YT34to10/",nurange=[6050.0,6150.0])
-
-    # %% Test  by overriding Spectrumfactory's DataFrame df0
-    # mdb = MdbExomol(".database/H2O/1H2-16O/POKAZATEL", [4310.0, 4320.0], crit=1.0e-45)
-    # df = mdb.to_df()
-    # from radis import SpectrumFactory
-    # sf = SpectrumFactory(
-    #     4310,
-    #     4320,
-    #     molecule="H2O",
-    #     isotope="1",
-    # )
-    # sf.fetch_databank(
-    #     "hitran"
-    # )  # placeholder. Load lines (will be replaced), load partition function.
-    # sf.df0 = df  # override.
-    # s = sf.eq_spectrum(500, name="ExoMol")
-    # # sf.fetch_databank('hitran')  # placeholder. Load lines (will be replaced), load partition function.
-    # # s_hit = sf.eq_spectrum(500, name='HITRAN')
-
-    #%% Test by direct caclulation
-    import pytest
-
-    print("Testing factory:", pytest.main(["../test/io/test_exomol.py"]))
+    test_exomol_parsing_functions()
+    test_calc_exomol_spectrum()
+    test_calc_exomol_vs_hitemp()
diff --git a/radis/io/exomolapi.py b/radis/io/exomolapi.py
index 3c5fa5235..5456324b4 100644
--- a/radis/io/exomolapi.py
+++ b/radis/io/exomolapi.py
@@ -4,9 +4,19 @@
 code (which you should also have a look at !), by @HajimeKawahara, under MIT License.
 
 """
+import bz2
+import re
+
 import numpy as np
 import pandas as pd
 
+try:
+    from .hdf5 import vaexsafe_colname
+except:
+    from radis.io.hdf5 import vaexsafe_colname
+
+from radis.misc.warning import InconsistentDatabaseError
+
 
 def read_def(deff):
     """Exomol IO for a definition file
@@ -17,16 +27,22 @@ def read_def(deff):
 
     Returns
     -------
-    temperature exponent n_Texp
-    broadening parameter alpha_ref
-    molecular mass
-    numinf: nu minimum for trans
-    numtag: tag for wavelength range
+    n_Texp : float
+        temperature exponent
+    alpha_ref : float
+        broadening parameter
+    molmass : float
+        molecular mass
+    numinf : List[float]
+        limit points (``[w(0), w(1), ..., w(n)]``, n+1 elements) defining
+        the spectral ranges appearing in the name of *.trans.bz2 files
+        (``["w(0)-w(1)", "w(1)-w(2)", ..., w(n-1)-w(n)]``, n elements)
+    numtag : List[str]
+        tag for wavelength ranges.
 
     Note:
        For some molecules, ExoMol provides multiple trans files. numinf and numtag are the ranges and identifiers for the multiple trans files.
 
-
     """
 
     dat = pd.read_csv(deff, sep="#", names=("VAL", "COMMENT"))
@@ -50,10 +66,9 @@ def read_def(deff):
             molmass = float(dat["VAL"][i].split()[0])  # in Da (atomic unit)
 
         elif "Lifetime availability" in com:
-            lifetime = dat["VAL"][i] == 1
+            lifetime = int(dat["VAL"][i]) == 1
         elif "Lande g-factor availability" in com:
-            lande = dat["VAL"][i] == 1
-
+            lande = int(dat["VAL"][i]) == 1
         elif "Quantum label" in com:
             quantum_labels.append(dat["VAL"][i].strip(" "))
 
@@ -177,12 +192,17 @@ def read_trans(transf, engine="vaex"):
                 convert=False,  #  file is created by MdbMol
             )
         except:
-            dat = vaex.read_csv(
-                transf,
-                sep=r"\s+",
-                names=("i_upper", "i_lower", "A", "nu_lines"),
-                convert=False,  #  file is created by MdbMol
-            )
+            try:
+                dat = vaex.read_csv(
+                    transf,
+                    sep=r"\s+",
+                    names=("i_upper", "i_lower", "A", "nu_lines"),
+                    convert=False,  #  file is created by MdbMol
+                )
+            except Exception as err:
+                raise Exception(
+                    f"Error reading {transf}. Maybe the file was corrupted during download ? You can try to delete it."
+                ) from err
     elif engine == "csv":
         try:  # bz2 compression
             dat = pd.read_csv(
@@ -192,14 +212,21 @@ def read_trans(transf, engine="vaex"):
                 names=("i_upper", "i_lower", "A", "nu_lines"),
             )
         except:
-            dat = pd.read_csv(
-                transf, sep=r"\s+", names=("i_upper", "i_lower", "A", "nu_lines")
-            )
+            try:
+                dat = pd.read_csv(
+                    transf, sep=r"\s+", names=("i_upper", "i_lower", "A", "nu_lines")
+                )
+            except Exception as err:
+                raise Exception(
+                    f"Error reading {transf}. Maybe the file was corrupted during download ? You can try to delete it."
+                ) from err
+    else:
+        raise NotImplementedError(engine)
 
     return dat
 
 
-def read_states(statesf, dic_def, engine="vaex"):
+def read_states(statesf, dic_def, engine="vaex", skip_optional_data=True):
     """Exomol IO for a state file
 
     Notes
@@ -216,15 +243,42 @@ def read_states(statesf, dic_def, engine="vaex"):
 
     Parameters
     ----------
-    statesf: state file
-    dic_def: Info from def file to read extra quantum numbers
-    engine: parsing engine to use ('vaex', 'csv')
+    statesf: str
+        state file
+    dic_def: dict
+        Info from def file to read extra quantum numbers
+    engine: str
+        parsing engine to use ('vaex', 'csv')
+    skip_optional_data: bool
+        If False, fetch all fields which are marked as available in the ExoMol definition
+        file. If True, load only the first 4 columns of the states file
+        ("i", "E", "g", "J"). The structure of the columns above 5 depend on the
+        the definitions file (*.def) and the Exomol version.
+        If ``skip_optional_data=False``, two errors may occur:
+
+            - a field is marked as present/absent in the *.def field but is
+              absent/present in the *.states file (ie both files are inconsistent).
+            - in the updated version of Exomol, new fields have been added in the
+              states file of some species. But it has not been done for all species,
+              so both structures exist. For instance, the states file of
+              https://exomol.com/data/molecules/HCl/1H-35Cl/HITRAN-HCl/ follows the
+              structure described in [1]_, unlike the states file of
+              https://exomol.com/data/molecules/NO/14N-16O/XABC/ which follows the
+              structure described in [2]_.
 
     Returns
     -------
     states data in pandas DataFrame
 
     If ``'vaex'``, also writes a local hdf5 file `statesf.with_suffix('.hdf5')`
+
+
+    References
+    ----------
+
+    .. [1] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Barton, E. J., Chubb, K. L., Coles, P. A., … Zak, E. (2016). The ExoMol database: molecular line lists for exoplanet and other hot atmospheres. https://doi.org/10.1016/j.jms.2016.05.002
+    .. [2] Tennyson, J., Yurchenko, S. N., Al-Refaie, A. F., Clark, V. H. J., Chubb, K. L., Conway, E. K., … Yurchenko, O. P. (2020). The 2020 release of the ExoMol database: Molecular line lists for exoplanet and other hot atmospheres. Journal of Quantitative Spectroscopy and Radiative Transfer, 255, 107228. https://doi.org/10.1016/j.jqsrt.2020.107228
+
     """
     # we read first 4 columns for ("i", "E", "g", "J"),
     # skip lifetime, skip Landé g-factor,
@@ -232,24 +286,45 @@ def read_states(statesf, dic_def, engine="vaex"):
     N = np.size(dic_def["quantum_labels"])
     quantum_labels = dic_def["quantum_labels"]
 
-    usecol = np.arange(4)
-    names = ("i", "E", "g", "J")
-    if dic_def["Landé"] and dic_def["lifetime"]:
-        usecol = np.concatenate((usecol, 5 + 2 + np.arange(N)))
-        names = names + ("Lande", "lifetime") + tuple(quantum_labels)
-    elif dic_def["Landé"]:
-        usecol = np.concatenate((usecol, 5 + 1 + np.arange(N)))
-        names = names + ("Lande") + tuple(quantum_labels)
-    elif dic_def["lifetime"]:
-        usecol = np.concatenate((usecol, 5 + 1 + np.arange(N)))
-        names = names + ("lifetime") + tuple(quantum_labels)
-    else:  # no lifetime, nor landé according to the def file
-        usecol = np.concatenate((usecol, 5 + np.arange(N)))
-        names = names + tuple(quantum_labels)
+    mandatory_usecol = np.arange(4)
+    mandatory_fields = ("i", "E", "g", "J")
+    if skip_optional_data:
+        usecol = mandatory_usecol
+        names = mandatory_fields
+    else:
+        if dic_def["Landé"] and dic_def["lifetime"]:
+            usecol = np.concatenate((mandatory_usecol, 5 + 2 + np.arange(N)))
+            names = mandatory_fields + ("Lande", "lifetime") + tuple(quantum_labels)
+        elif dic_def["Landé"]:
+            usecol = np.concatenate((mandatory_usecol, 5 + 1 + np.arange(N)))
+            names = mandatory_fields + ("Lande",) + tuple(quantum_labels)
+        elif dic_def["lifetime"]:
+            usecol = np.concatenate((mandatory_usecol, 5 + 1 + np.arange(N)))
+            names = mandatory_fields + ("lifetime",) + tuple(quantum_labels)
+        else:  # no lifetime, nor landé according to the def file
+            usecol = np.concatenate((mandatory_usecol, 5 + np.arange(N)))
+            names = mandatory_fields + tuple(quantum_labels)
+        # The definitions file (*.def) specifies which fields are available
+        # in the states fiel (*.states). Check the number of available fields
+        # (see *.def) match the numbers of columns in the states file (*.states).
+        # Otherwise, both files are inconsistent.
+        with bz2.open(statesf, "rb") as f:
+            firstline = f.readline().decode("utf-8")  # read 1dt line
+            splitline = [x for x in re.split(r"\s+", firstline) if x != ""]
+            # splitline = re.split(r"\s+", firstline)
+            f.close()
+        if len(splitline) != len(names):
+            raise InconsistentDatabaseError(
+                "The EXOMOL definitions and states files are inconsistent.\n"
+                + "Some data are specified as available by the *.def file, but are absent in the *.bz2 file.\n"
+                + "Set `skip_optional_data=False` in `fetch_exomol()` to load only the required data for a LTE computation.\n"
+                + "The problematic optional data/columns will be ignored."
+            )
 
     if engine == "vaex":
         import vaex
 
+        # TODO Refactor: move in DataFileManager
         try:
             dat = vaex.from_csv(
                 statesf,
@@ -257,12 +332,22 @@ def read_states(statesf, dic_def, engine="vaex"):
                 sep=r"\s+",
                 usecols=usecol,
                 names=names,
-                convert=True,
+                convert=False,  # written in MolDB
             )
         except:
-            dat = vaex.read_csv(
-                statesf, sep=r"\s+", usecols=usecol, names=names, convert=True
-            )
+            try:
+                dat = vaex.read_csv(
+                    statesf,
+                    sep=r"\s+",
+                    usecols=usecol,
+                    names=names,
+                    convert=False,  # written in MolDB
+                )
+
+            except Exception as err:
+                raise Exception(
+                    f"Error reading {statesf}. Maybe the file was corrupted during download ? You can try to delete it."
+                ) from err
     elif engine == "csv":
         try:
             dat = pd.read_csv(
@@ -276,17 +361,20 @@ def read_states(statesf, dic_def, engine="vaex"):
     return dat
 
 
-def pickup_gE(ndstates, ndtrans, trans_file, dic_def, trans_lines=False):
+def pickup_gE(
+    states, trans, trans_file, dic_def, trans_lines=False, skip_optional_data=True
+):
     """extract g_upper (gup), E_lower (elower), and J_lower and J_upper from states
     DataFrame and insert them into the transition DataFrame.
 
     Parameters
     ----------
-    ndstates: states numpy array    - the i, E, g, J are in the 4 first columns
-    ndtrans: transition numpy array
+    states: states DataFrame  - the i, E, g, J are in the 4 first columns
+    trans: transition numpy array
     trans_file: name of the transition file
     trans_lines: By default (False) we use nu_lines computed using the state file, i.e. E_upper - E_lower. If trans_nuline=True, we use the nu_lines in the transition file. Note that some trans files do not this info.
     dic_def: Informations about additional quantum labels
+    add_quantum_labels: bool . If True fetch all quantum labels in dic_def['quantum_labels'] from states into transitions
 
     Returns
     -------
@@ -296,89 +384,69 @@ def pickup_gE(ndstates, ndtrans, trans_file, dic_def, trans_lines=False):
     -----
     We first convert pandas DataFrame to ndarray. The state counting numbers in states DataFrame is used as indices of the new array for states (newstates). We remove the state count numbers as the column of newstate, i.e. newstates[:,k] k=0: E, 1: g, 2: J. Then, we can directly use the state counting numbers as mask.
 
+    States by default has columns ::
+
+        #       i      E             g    J    v
+        0       1      0.0           1    0    0
+        1       2      1.448467      3    1    0
+        2       3      4.345384      5    2    0
+        3       4      8.690712      7    3    0
+
 
     """
     ### Step 1. Essential quantum number for spectra
+    # ----------------------------------------------
+
+    def map_add(col, new_col, trans_key, states_key="i"):
+        """Lookup `key` in states and add it in trans, using the level ``i``
+        for upper and lower state
+
+        Examples
+        --------
+        ::
+
+            map_add("E", "E_lower", "i_lower")
+        """
+        try:  # pytable
+            trans[new_col] = trans[trans_key].map(dict(states[col]))
+        except:  # a priori, vaex version  (TODO : replace with dict() approach in vaex too)
+            col = vaexsafe_colname(col)
+            new_col = vaexsafe_colname(new_col)
+
+            # WIP. First implementation with join(). Use Map() will probably be faster.
+            trans.join(
+                states[states_key, col],
+                left_on=trans_key,
+                right_on=states_key,
+                inplace=True,
+            )
+            trans.drop(states_key, inplace=True)
+            trans.rename(col, new_col)
 
-    iorig = np.array(ndstates[:, 0], dtype=int)
-    maxii = int(np.max(iorig) + 1)
-    newstates = np.zeros((maxii, np.shape(ndstates)[1] - 1), dtype=float)
-    newstates[iorig, :] = ndstates[:, 1:]
+    map_add("g", "gup", "i_upper")
+    map_add("J", "jlower", "i_lower")
+    map_add("J", "jupper", "i_upper")
 
-    i_upper = np.array(ndtrans[:, 0], dtype=int)
-    i_lower = np.array(ndtrans[:, 1], dtype=int)
+    map_add("E", "elower", "i_lower")
 
-    ### Step 2. Extra quantum numbers (e/f parity, vib and rot numbers)
-    #!!! Todo: Awfull management of memory, i know! @minou
-    n0 = (
-        4 + dic_def["Landé"] + dic_def["lifetime"]
-    )  # we need to shift if these quantities exist
-    # ndstates_extra = ndstates.to_numpy()[
-    ndstates_extra = ndstates[:, n0:]  # the i, E, g, J are in the 4 first columns
-    a, b = np.shape(ndstates_extra)
-    dico_quantumNumbers = {}
-    if b != 0:
-        for index, label in enumerate(dic_def["quantum_labels"]):
-            if label == "K":  #!!!TODO: allow users to have other quantum numbers
-                dico_quantumNumbers["{}_l".format(label)] = ndstates_extra[
-                    i_lower, index
-                ]
-                # dico_quantumNumbers['{}_u'.format(label)] = ndstates_extra[index][i_upper]
-
-    # use the state counting numbers (upper and lower) as masking.
-    elower = newstates[i_lower, 0]
-    eupper = newstates[i_upper, 0]
-    gup = newstates[i_upper, 1]
-    jlower = np.array(newstates[i_lower, 2], dtype=int)
-    del i_lower
-    jupper = np.array(newstates[i_upper, 2], dtype=int)
-    del i_upper
-
-    A = ndtrans[:, 2]
-
-    if trans_lines:
-        nu_lines = ndtrans[:, 3]
-    else:
-        nu_lines = eupper - elower
-    del eupper
-
-    # ### MASKING ###
-    mask = nu_lines > 0.0
-    if False in mask:
-        len_org = len(nu_lines)
-
-        A = A[mask]
-        nu_lines = nu_lines[mask]
-        elower = elower[mask]
-        gup = gup[mask]
-        jlower = jlower[mask]
-        jupper = jupper[mask]
-        for key, array in dico_quantumNumbers.items():
-            dico_quantumNumbers[key] = array[mask]
-        print(
-            "WARNING: {0:,} transitions with the wavenumber=zero in {1} have been ignored.".format(
-                len_org - len(nu_lines), trans_file
-            )
-        )
-        if trans_lines:
-            print(
-                "This is because the value for the wavenumber column in the transition file is zero for those transitions."
-            )
-        else:
-            print(
-                "This is because the upper and lower state IDs in the transition file indicate the same energy level when referring to the states file for those transitions."
-            )
+    def has_nan(column):
+        try:  # Vaex
+            return column.countnan() > 0
+        except AttributeError:  # Pandas
+            return column.hasnans
 
-    # See Issue exojax#16
-    # import matplotlib.pyplot as plt
-    # nu_lines_t=ndtrans[:,3]
-    # plt.plot(nu_lines_t-nu_lines,".",alpha=0.03)
-    # plt.ylabel("diff nu from trans and state (cm-1)")
-    # plt.xlabel("wavenuber (cm-1)")
-    # plt.savefig("nudiff.png", bbox_inches="tight", pad_inches=0.0)
-    # plt.show()
+    if not "nu_lines" in trans or has_nan(trans["nu_lines"]):
+        map_add("E", "eupper", "i_upper")
+        trans["nu_lines"] = trans["eupper"] - trans["elower"]
+
+    ### Step 2. Extra quantum numbers (e/f parity, vib and rot numbers)
+    # -----------------------------------------------------------------
+    if not skip_optional_data:
+        for q in dic_def["quantum_labels"]:
+            map_add(q, f"{q}_l", "i_lower")
+            # map_add(q, f"{q}_u", "i_upper")
 
-    return A, nu_lines, elower, gup, jlower, jupper, mask, dico_quantumNumbers
+    return trans
 
 
 # def pickup_gEslow(states, trans):
@@ -559,69 +627,3 @@ def make_jj2b(bdat, j2alpha_ref_def, j2n_Texp_def, jupper_max=None):
 
 if __name__ == "__main__":
     pass
-    # import pathlib
-    # import sys
-    # import time
-
-    # deff = pathlib.Path(
-    #     "/home/kawahara/exojax/examples/luhman16/.database/CO2/12C-16O2/UCL-4000/12C-16O2__UCL-4000.def"
-    # )
-    # n_Texp, alpha_ref, molmass, numinf, numtag = read_def(deff)
-    # print(numtag)
-    # sys.exit()
-    # # various broad file
-    # #    broadf="/home/kawahara/exojax/data/broad/12C-16O__H2.broad"
-    # broadf = "/home/kawahara/exojax/data/broad/1H2-16O__H2.broad"
-    # bdat = read_broad(broadf)
-    # codelv = check_bdat(bdat)
-    # print(codelv)
-    # if codelv == "a0":
-    #     j2alpha_ref, j2n_Texp = make_j2b(bdat, jlower_max=100)
-    # elif codelv == "a1":
-    #     j2alpha_ref, j2n_Texp = make_j2b(bdat, jlower_max=100)
-    #     jj2alpha_ref, jj2n_Texp = make_jj2b(bdat, j2alpha_ref, j2n_Texp, jupper_max=100)
-    #     print(jj2alpha_ref[1, 2])
-    #     print(jj2alpha_ref[1, 15])
-
-    # sys.exit()
-    # # broad file
-    # broadf = "/home/kawahara/exojax/data/CO/12C-16O/12C-16O__H2.broad"
-    # bdat = read_broad(broadf)
-    # j2alpha_ref, j2n_Texp = make_j2b(bdat, jlower_max=100)
-
-    # # partition file
-    # pff = "/home/kawahara/exojax/data/exomol/CO/12C-16O/Li2015/12C-16O__Li2015.pf"
-    # dat = read_pf(pff)
-
-    # check = False
-    # if check:
-    #     print("Checking compution of Elower and gupper.")
-    # statesf = (
-    #     "/home/kawahara/exojax/data/exomol/CO/12C-16O/Li2015/12C-16O__Li2015.states.bz2"
-    # )
-    # states = read_states(statesf)
-    # transf = (
-    #     "/home/kawahara/exojax/data/exomol/CO/12C-16O/Li2015/12C-16O__Li2015.trans.bz2"
-    # )
-    # trans = read_trans(transf)
-
-    # ts = time.time()
-    # A, nu_lines, elower, gup, jlower, jupper = pickup_gE(states, trans)
-    # #    for i in range(0,len(A)):
-    # #        print(jlower[i],"-",jupper[i])
-    # te = time.time()
-
-    # tsx = time.time()
-    # if check:
-    #     A_s, nu_lines_s, elower_s, gup_s = pickup_gEslow(states, trans)
-    # tex = time.time()
-    # print(te - ts, "sec")
-    # if check:
-    #     print(tex - tsx, "sec for the slow version")
-    #     print("CHECKING DIFFERENCES...")
-    #     print(np.sum((A_s - A) ** 2))
-    #     print(np.sum((nu_lines_s - nu_lines) ** 2))
-    #     print(np.sum((elower_s - elower) ** 2))
-    #     print(np.sum((gup_s - gup) ** 2))
-
-    # # computing alpha_ref, n_Texp
diff --git a/radis/io/geisa.py b/radis/io/geisa.py
index 35b8120da..f8049be24 100644
--- a/radis/io/geisa.py
+++ b/radis/io/geisa.py
@@ -19,10 +19,17 @@
 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
 
+try:
+    from .cache_files import load_h5_cache_file, save_to_hdf
+    from .dbmanager import DatabaseManager
+    from .hdf5 import DataFileManager
+    from .tools import drop_object_format_columns, parse_hitran_file
+except ImportError:
+    from radis.io.cache_files import load_h5_cache_file, save_to_hdf
+    from radis.io.dbmanager import DatabaseManager
+    from radis.io.hdf5 import DataFileManager
+    from radis.io.tools import drop_object_format_columns, parse_hitran_file
 # from typing import Union
 
 
@@ -229,7 +236,7 @@ def gei2df(
         print("Last Modification time: {0}".format(metadata["last_modification"]))
 
     # Attempt to use cache file
-    fcache = cache_file_name(fname, engine=engine)
+    fcache = DataFileManager(engine).cache_file(fname)
     if cache and exists(fcache):
         relevant_if_metadata_above = (
             {"wavenum_max": load_wavenum_min} if load_wavenum_min else {}
@@ -388,7 +395,7 @@ def parse_to_local_file(
         # molecule = self.molecule
         # Ntotal_lines_expected = GEISA_MOLECULES_Nlines[molecule.upper()]
 
-        writer = self.get_hdf5_manager()
+        writer = self.get_datafile_manager()
 
         with opener.open(urlname) as gfile:  # locally downloaded file
 
@@ -500,7 +507,12 @@ def fetch_geisa(
         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.
+        jax arrays. If ``'vaex'``, output is a :py:class:`vaex.dataframe.DataFrameLocal`
+
+        .. note::
+            Vaex DataFrames are memory-mapped. They do not take any space in RAM
+            and are extremelly useful to deal with the largest databases.
+
     parallel: bool
         if ``True``, uses joblib.parallel to load database with multiple processes
 
@@ -600,9 +612,10 @@ def fetch_geisa(
     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,
+        within=[("iso", isotope)] if isotope is not None else [],
+        # for relevant files, get only the right range :
+        lower_bound=[("wav", load_wavenum_min)] if load_wavenum_min is not None else [],
+        upper_bound=[("wav", load_wavenum_max)] if load_wavenum_max is not None else [],
         output=output,
     )
 
diff --git a/radis/io/hdf5.py b/radis/io/hdf5.py
index 7824d0129..10bce3353 100644
--- a/radis/io/hdf5.py
+++ b/radis/io/hdf5.py
@@ -6,8 +6,9 @@
 """
 
 import os
+import pathlib
 import sys
-from os.path import exists, expanduser, splitext
+from os.path import abspath, exists, expanduser, splitext
 from time import time
 
 import h5py
@@ -15,6 +16,14 @@
 from tables.exceptions import NoSuchNodeError
 
 
+def vaexsafe_colname(name):
+    """replace '/' (forbidden in HDF5 vaex column names with '_'
+    https://github.com/radis/radis/issues/473
+    https://github.com/vaexio/vaex/issues/1255
+    """
+    return name.replace("/", "_")
+
+
 def update_pytables_to_vaex(fname, remove_initial=False, verbose=True, key="df"):
     """Convert a HDF5 file generated from PyTables to a
     Vaex-friendly HDF5 format, preserving metadata"""
@@ -30,7 +39,7 @@ def update_pytables_to_vaex(fname, remove_initial=False, verbose=True, key="df")
     df = pd.read_hdf(fname)
     df = vaex.from_pandas(df)
 
-    pytables_manager = HDF5Manager(engine="pytables")
+    pytables_manager = DataFileManager(engine="pytables")
 
     # Read metadata
     file_metadata = pytables_manager.read_metadata(fname)
@@ -40,7 +49,7 @@ def update_pytables_to_vaex(fname, remove_initial=False, verbose=True, key="df")
     df.close()  # try to fix file not closed()  TODO: remove?
     del df  # same TODO
 
-    vaex_manager = HDF5Manager(engine="vaex")
+    vaex_manager = DataFileManager(engine="vaex")
     vaex_manager.add_metadata(fname_vaex, file_metadata)
 
     if verbose:
@@ -56,8 +65,13 @@ def update_pytables_to_vaex(fname, remove_initial=False, verbose=True, key="df")
 
 
 class HDF5Manager(object):
+    def __init__(*args, **kwargs):
+        raise DeprecationWarning("HDF5Manager replaced with DataFileManager")
+
+
+class DataFileManager(object):
     def __init__(self, engine=None):
-        """Class to handle all memory-mapping-librairies with one common API
+        """Class to handle all DataFrame-librairies with one common API
 
         All functions may not be fully implemetned, will raise a NotImplementedError
         if that's not the case.
@@ -67,13 +81,14 @@ def __init__(self, engine=None):
             'vaex'     > HDF5,  column-based
             'pytables' > Pandas's HDF5,  row-based
             'h5py'     > HDF5
+            'feather'  > feather
 
         Functions ::
 
             add_metadata
             read_metadata
             write
-            load
+            read
             guess_engine
 
         Examples
@@ -107,13 +122,17 @@ def write(
         self,
         file,
         df,
-        append=True,
+        append=False,
         key="default",
         format="table",
-        data_columns=["iso", "wav"],
+        data_columns=["iso", "wav", "nu_lines"],
     ):
         """Write dataframe ``df`` to ``file``
 
+        Parameters
+        ----------
+        df: DataFrame
+
         Other Parameters
         ----------------
         key: str
@@ -141,9 +160,16 @@ def write(
             # export dataframe
             df.to_hdf(file, key, format="fixed", mode="w", complevel=9, complib="blosc")
         elif self.engine == "vaex":
+            if isinstance(df, pd.DataFrame):
+                import vaex
+
+                df = vaex.from_pandas(df)
+
+            for c in df.columns:  # remove "/" in columns (forbidden)
+                df.rename(c, vaexsafe_colname(c))
+
             if key == "default":
                 key = r"/table"
-            import vaex
 
             if append == True:
                 # In vaex we cannot append. Here we write lots of small files then combine them.
@@ -163,15 +189,38 @@ def write(
                     os.remove(file)
                 self._temp_batch_files.append(file)
             # Write:
-            try:
-                df.export_hdf5(file, group=key, mode="w")
-            except AttributeError:  # case where df is not a Vaex dataFrame but (likely) a Pandas Dataframe
-                vaex.from_pandas(df).export_hdf5(file, group=key, mode="w")
+            df.export_hdf5(file, group=key, mode="w")
+        elif self.engine == "feather":
+            df.to_feather(file)
         else:
             raise NotImplementedError(self.engine)
             # h5py is not designed to write Pandas DataFrames
 
-    def combine_temp_batch_files(self, file, key="default", sort_values=None):
+    def get_columns(self, local_file):
+        """Get all columns (without loading all Dataframe)"""
+        engine = self.engine
+        local_file = expanduser(local_file)
+        if engine == "vaex":
+            import vaex
+
+            # by default vaex does not load everything
+            df = vaex.open(local_file)
+            columns = df.columns
+            df.close()
+
+        elif engine == "pytables":
+            with pd.HDFStore(local_file, "r") as store:
+                columns = store.select("df", start=1, stop=1).columns
+        elif engine in ["h5py"]:
+            raise NotImplementedError
+        else:
+            raise ValueError(engine)
+
+        return columns
+
+    def combine_temp_batch_files(
+        self, file, key="default", sort_values=None, delete_nan_columns=True
+    ):
         """Combine all batch files in ``self._temp_batch_files`` into one.
         Removes all batch files.
         """
@@ -188,6 +237,15 @@ def combine_temp_batch_files(self, file, key="default", sort_values=None):
             import vaex
 
             df = vaex.open(self._temp_batch_files, group=key)
+            # Removing Nan values columns
+            if delete_nan_columns:
+                import numpy as np
+
+                for column in df.columns:
+                    col = df[column].values
+                    if type(col[0]) in [np.int32, np.float64] and np.isnan(np.sum(col)):
+                        del df[column]
+
             if sort_values:
                 df.sort(by=sort_values).export_hdf5(file, group=key, mode="w")
             else:
@@ -213,6 +271,104 @@ def __del__(self):
             self._close_temp_batch_files()
 
     def load(
+        self,
+        fname,
+        columns=None,
+        lower_bound=[],
+        upper_bound=[],
+        within=[],
+        output="pandas",
+        **store_kwargs,
+    ):
+        """
+        Other Parameters
+        ----------------
+        columns: list of str
+            list of columns to load. If ``None``, returns all columns in the file.
+        output: 'pandas', 'vaex', 'jax'
+            format of the output DataFrame. If ``'jax'``, returns a dictionary of
+            jax arrays.
+        lower_bound: list of tuples [(column, lower_bound), etc.]
+            ::
+
+                lower_bound =[("wav", load_wavenum_min)]
+        upper_bound_bound: list of tuples [(column, upper_bound), etc.]
+            ::
+
+                upper_bound=[("wav", load_wavenum_max)]
+        within: list of tuples [(column, within_list), etc.]
+            ::
+
+                within=[("iso", isotope.split(","))]
+        """
+        if not lower_bound and not upper_bound and not within:
+            df = self.read(
+                fname,
+                columns,
+                **store_kwargs,
+            )
+        else:
+            df = self.read_filter(
+                fname,
+                columns,
+                lower_bound=lower_bound,
+                upper_bound=upper_bound,
+                within=within,
+                **store_kwargs,
+            )
+
+        # Load
+        # Convert to output format
+        engine = self.engine
+
+        if engine == output:
+            df = df
+        elif engine == "vaex":
+            # in vaex, column selection has to happen now
+            if columns:  # load only these columns (if they exist)
+                columns = [c for c in columns if c in df.columns]
+            if output == "pandas":
+                df = df.to_pandas_df(column_names=columns)
+            elif output == "jax":
+                if columns == None:
+                    columns = list(df.columns)
+                out = {}
+                try:
+                    import jax.numpy as jnp
+                except ImportError:
+                    print("Jax not found. Using Numpy.")
+                    import numpy as jnp
+                for c in columns:
+                    if (
+                        c == "Sij0"
+                    ):  # (special case for Sij0 : we store logSij0 instead, to store them as jax np.32 arrays)
+                        import numpy as np
+
+                        out["logsij0"] = jnp.array(np.log(df[c].values))
+                    else:
+                        out[c] = jnp.array(df[c].values)
+                df = out
+            else:
+                raise NotImplementedError(f"output {output} for engine {engine}")
+        elif engine == "pytables":
+            if output == "pandas":
+                df = df
+            else:
+                raise NotImplementedError(f"output {output} for engine {engine}")
+        elif engine == "feather":
+            if output == "pandas":
+                df = df
+            else:
+                raise NotImplementedError(f"output {output} for engine {engine}")
+        else:
+            raise NotImplementedError(output)
+
+        if output == "vaex":
+            df = df.extract()  # return DataFrame containing only the filtered rows
+
+        return df
+
+    def read(
         self,
         fname,
         columns=None,
@@ -224,6 +380,7 @@ def load(
         """
         Parameters
         ----------
+        fname: str
         columns: list of str
             list of columns to load. If ``None``, returns all columns in the file.
         where: list of str
@@ -290,7 +447,7 @@ def load(
                 except (FileNotFoundError, OSError) as err:
                     # error message with suggestion on how to convert from existing file
                     for f in fname_list:
-                        if exists(f.replace(".hdf5", ".h5")):
+                        if f.endswith(".hdf5") and exists(f.replace(".hdf5", ".h5")):
                             raise FileNotFoundError(
                                 f"`{f}` not found but `{f.replace('.hdf5', '.h5')}` exists (probably a row-based pytables HDF5 file). Try (1) using engine='pytables' in the calling function (`hdf2df`, `fetch_hitemp`, etc.)  ; (2) delete the file to re-download and re-parse it (this may take a lot of time !) ;  or (3, recommended) set `import radis; radis.config['AUTO_UPDATE_DATABASE']= True` in your script to auto-update to Vaex HDF5 file"
                             ) from err
@@ -322,11 +479,107 @@ def load(
                     out[k] = f[k][()]
             return pd.DataFrame(out)
 
+        elif self.engine == "feather":
+            assert where is None
+            fname = expanduser(fname)
+            return pd.read_feather(fname)
+
         else:
             raise NotImplementedError(self.engine)
 
         return df
 
+    def read_filter(
+        self,
+        fname,
+        columns=None,
+        lower_bound=[],
+        upper_bound=[],
+        within=[],
+        **store_kwargs,
+    ):
+        """
+        Parameters
+        ----------
+        fname: str
+        columns: list of str
+            list of columns to load. If ``None``, returns all columns in the file.
+        lower_bound: list of tuples [(column, lower_bound), etc.]
+            ::
+
+                lower_bound =[("wav", load_wavenum_min)]
+        upper_bound_bound: list of tuples [(column, upper_bound), etc.]
+            ::
+
+                upper_bound=[("wav", load_wavenum_max)]
+        within: list of tuples [(column, within_list), etc.]
+            ::
+
+                within=[("iso", isotope.split(","))]
+
+        """
+
+        # Selection
+        if self.engine == "pytables":
+            # Selection
+            where = []
+            for (column, lbound) in lower_bound:
+                where.append(f"{column} > {lbound}")
+            for (column, ubound) in upper_bound:
+                where.append(f"{column} < {ubound}")
+            for (column, withinv) in within:
+                where.append(f"{column} in {withinv.split(',')}")
+
+        elif self.engine in ["vaex", "feather"]:
+            # Selection is done after opening the file time in vaex
+            # see end of this function
+            where = None
+        else:
+            raise NotImplementedError(self.engine)
+
+        # Load :
+        df = self.read(fname, columns=columns, where=where, **store_kwargs)
+
+        #  Selection in vaex
+        if self.engine in ["vaex", "feather"]:
+            # (note that in Vaex, the selection happens on disk whereas Feather
+            # is already loaded as a Pandas DataFrame in RAM)
+
+            # Selection
+            b = True
+            for (column, lbound) in lower_bound:
+                b *= df[column] > lbound
+            for (column, ubound) in upper_bound:
+                b *= df[column] < ubound
+            for (column, withinv) in within:
+                b2 = False
+                for val in withinv.split(","):
+                    b2 += df[column] == float(val)
+                b *= b2
+            if b is not True and False in b:
+                df = df[
+                    b
+                ]  # note in Vaex mode, this is a vaex Expression, not the DataFrame yet
+
+        return df
+
+    def cache_file(self, fname):
+        """Return the corresponding cache file name for fname.
+
+        Other Parameters
+        ----------------
+        engine: ``'h5py'``, ``'pytables'``, ``'vaex'``
+           which HDF5 library to use. Default ``pytables``
+        """
+        if self.engine in ["pytables", "pytables-fixed"]:
+            return pathlib.Path(fname).with_suffix(".h5")
+        elif self.engine in ["h5py", "vaex"]:
+            return pathlib.Path(fname).with_suffix(".hdf5")
+        elif self.engine == "feather":
+            return pathlib.Path(fname).with_suffix(".feather")
+        else:
+            raise ValueError(self.engine)
+
     def add_metadata(
         self, fname: str, metadata: dict, key="default", create_empty_dataset=False
     ):
@@ -424,6 +677,14 @@ def read_metadata(self, fname: str, key="default") -> dict:
                 except KeyError as err:
                     print(f"Error reading metadata from {fname}")
                     raise err
+                except AttributeError as err:
+                    if "Attribute 'metadata' does not exist" in str(err):
+                        metadata = {}
+                    else:
+                        raise err
+
+        elif self.engine == "feather":
+            return {}  # no metadata
 
         elif self.engine == "h5py":
             fname = expanduser(fname)
@@ -477,6 +738,18 @@ def read_metadata(self, fname: str, key="default") -> dict:
 
         return metadata
 
+    def to_numpy(self, df):
+        """Convert DataFrame to numpy"""
+
+        if self.engine == "vaex":
+            import vaex
+
+            return vaex.array_types.to_numpy(df)
+        elif self.engine == "feather":
+            return df.to_numpy()
+        else:
+            raise NotImplementedError(self.engine)
+
     @classmethod
     def guess_engine(self, file, verbose=True):
         """Guess which HDF5 library ``file`` is compatible with
@@ -496,26 +769,38 @@ def guess_engine(self, file, verbose=True):
             mgr.read_metadata(file)
 
         """
-        # See if it looks like PyTables
-        import tables
-
-        if tables.is_pytables_file(file):
-            engine = "pytables"
+        if file.endswith(".feather"):
+            engine = "feather"
         else:
-            # Try Vaex
-            file = expanduser(file)
-            with h5py.File(file, mode="r") as hf:
-                try:
-                    hf[r"/table"]
-                except KeyError:
-                    engine = "h5py"
-                else:
-                    engine = "vaex"
+            # See if it looks like PyTables
+            import tables
+
+            if tables.is_pytables_file(file):
+                engine = "pytables"
+            else:
+                # Try Vaex
+                file = expanduser(file)
+                with h5py.File(file, mode="r") as hf:
+                    try:
+                        hf[r"/table"]
+                    except KeyError:
+                        engine = "h5py"
+                    else:
+                        engine = "vaex"
         if verbose:
-            print(f"Guessed that {file} was compatible with `{engine}` hdf5 engine")
+            print(f"Guessed that {file} was compatible with `{engine}` engine")
         # raise
         return engine
 
+    def has_nan(self, column):
+        if self.engine == "vaex":
+            b = column.isnan()  # TODO: check if can be made faster?
+            return b.sum() > 0
+        elif self.engine in ["pytables", "feather"]:
+            return column.hasnans
+        else:
+            raise NotImplementedError(self.engine)
+
 
 def hdf2df(
     fname,
@@ -557,7 +842,7 @@ def hdf2df(
 
     Returns
     -------
-    df: pandas Dataframe
+    df: pandas Dataframe, or vaex DataFrameLocal, or dictionary of Jax arrays
         dataframe containing all lines or energy levels
 
     Examples
@@ -587,89 +872,29 @@ def hdf2df(
 
     """
 
+    fname = abspath(expanduser(fname))
+
     if engine == "guess":
-        engine = HDF5Manager.guess_engine(fname)
+        engine = DataFileManager.guess_engine(fname)
 
     t0 = time()
 
-    # Selection
-    selection = False
-    if engine == "pytables":
-        # Selection
-        selection = True
-        where = []
-        if load_wavenum_min is not None:
-            where.append(f"wav > {load_wavenum_min}")
-        if load_wavenum_max is not None:
-            where.append(f"wav < {load_wavenum_max}")
-        if isotope:
-            where.append(f'iso in {isotope.split(",")}')
-
-    elif engine == "vaex":
-        # Selection is done after opening the file time in vaex
-        # see end of this function
-        where = None
-    else:
-        raise NotImplementedError(engine)
-
-    # Load :
-    manager = HDF5Manager(engine)
-    df = manager.load(fname, columns=columns, where=where, **store_kwargs)
+    manager = DataFileManager(engine)
 
-    #  Selection in vaex
-    if engine == "vaex":
-
-        # Selection
-        selection = True
-        b = True
-        if load_wavenum_min is not None:
-            b *= df.wav > load_wavenum_min
-        if load_wavenum_max is not None:
-            b *= df.wav < load_wavenum_max
-        if isotope is not None:
-            from radis.misc.basics import is_float
-
-            if is_float(isotope):
-                b *= df.iso == int(isotope)
-            else:
-                b2 = False
-                for iso in isotope.split(","):
-                    b2 += df.iso == int(iso)
-                b *= b2
-        if b != True and False in b:
-            df = df[b]  # note that this is a vaex Expression, not the DataFrame yet
-
-        # Load
-        if columns:  # load only these columns (if they exist)
-            columns = [c for c in columns if c in df.columns]
-
-    # Convert to output format
-    if output == "pandas" and engine == "vaex":
-        df = df.to_pandas_df(column_names=columns)
-    elif output == "vaex" and engine == "pytables":
-        raise NotImplementedError
-    elif output == "jax":
-        if columns == None:
-            columns = ["wav", "int", "A", "El", "gpp"]
-        out = {}
-        try:
-            import jax as jnp
-        except ImportError:
-            import numpy as jnp
-        for c in columns:
-            out[c] = jnp.array(df[c].values)
-        df = out
-    elif output == engine or (
-        engine == "pytables" and output == "pandas"
-    ):  # pandas > pandas; vaex -> vaex
-        df = df
-    else:
-        raise NotImplementedError(output)
+    df = manager.load(
+        fname,
+        columns,
+        lower_bound=[("wav", load_wavenum_min)] if load_wavenum_min is not None else [],
+        upper_bound=[("wav", load_wavenum_max)] if load_wavenum_max is not None else [],
+        within=[("iso", isotope)] if isotope is not None else [],
+        output=output,
+    )
 
     # Read and add metadata in the DataFrame
     metadata = manager.read_metadata(fname)
 
     # Sanity Checks if loading the full file
+    selection = isotope or load_wavenum_min or load_wavenum_max
     if not selection:
         if "total_lines" in metadata:
             assert len(df) == metadata["total_lines"]
@@ -679,7 +904,7 @@ def hdf2df(
             assert df["wav"].max() == metadata["wavenumber_max"]
 
     # Update metadata
-    if output != "jax":
+    if output != "jax":  # metadata not given in Jax output
         if isinstance(metadata, list):
             metadata_dict = {}
             for k, v in metadata[0].items():
diff --git a/radis/io/hitemp.py b/radis/io/hitemp.py
index c6dddc379..837f4cca1 100644
--- a/radis/io/hitemp.py
+++ b/radis/io/hitemp.py
@@ -219,7 +219,7 @@ def fetch_urlnames(self):
             base_url, Ntotal_lines_expected, _, _ = self.fetch_url_Nlines_wmin_wmax()
             response = urllib.request.urlopen(base_url)
             response_string = response.read().decode()
-            inputfiles = re.findall('href="(\S+.zip)"', response_string)
+            inputfiles = re.findall(r'href="(\S+.zip)"', response_string)
 
             urlnames = [join(base_url, f) for f in inputfiles]
 
@@ -312,7 +312,7 @@ def parse_to_local_file(
         wmin = np.inf
         wmax = 0
 
-        writer = self.get_hdf5_manager()
+        writer = self.get_datafile_manager()
 
         with opener.open(urlname) as gfile:  # locally downloaded file
 
@@ -482,7 +482,12 @@ def fetch_hitemp(
         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.
+        jax arrays. If ``'vaex'``, output is a :py:class:`vaex.dataframe.DataFrameLocal`
+
+        .. note::
+            Vaex DataFrames are memory-mapped. They do not take any space in RAM
+            and are extremelly useful to deal with the largest databases.
+
     parallel: bool
         if ``True``, uses joblib.parallel to load database with multiple processes
 
@@ -622,9 +627,10 @@ def fetch_hitemp(
     df = ldb.load(
         files_loaded,  # 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,
+        within=[("iso", isotope)] if isotope is not None else [],
+        # for relevant files, get only the right range :
+        lower_bound=[("wav", load_wavenum_min)] if load_wavenum_min is not None else [],
+        upper_bound=[("wav", load_wavenum_max)] if load_wavenum_max is not None else [],
         output=output,
     )
 
diff --git a/radis/io/hitran.py b/radis/io/hitran.py
index 9726770a0..82202ef0a 100644
--- a/radis/io/hitran.py
+++ b/radis/io/hitran.py
@@ -50,13 +50,28 @@
     HITRAN_GROUP6,
     get_molecule,
 )
-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,
-    replace_PQR_with_m101,
-)
+
+from ..misc.warning import AccuracyWarning
+
+try:
+    from .cache_files import load_h5_cache_file, save_to_hdf
+    from .dbmanager import DatabaseManager
+    from .hdf5 import DataFileManager
+    from .tools import (
+        drop_object_format_columns,
+        parse_hitran_file,
+        replace_PQR_with_m101,
+    )
+except ImportError:
+    from radis.io.cache_files import load_h5_cache_file, save_to_hdf
+    from radis.io.dbmanager import DatabaseManager
+    from radis.io.hdf5 import DataFileManager
+    from radis.io.tools import (
+        drop_object_format_columns,
+        parse_hitran_file,
+        replace_PQR_with_m101,
+    )
+
 
 # %% Parsing functions
 
@@ -90,6 +105,16 @@
 # fmt: on
 
 
+PARAMETER_GROUPS_HITRAN = {
+    "par_line": "PARLIST_DOTPAR",
+    "id": "PARLIST_ID",
+    "standard": "PARLIST_STANDARD",
+    "labels": "PARLIST_LABELS",
+    "voigt": "PARLIST_VOIGT_ALL",
+    "ht": "PARLIST_HT_ALL",
+}
+
+
 def cast_to_int64_with_missing_values(dg, keys):
     """replace missing values of int64 columns with -1"""
     for c in keys:
@@ -178,7 +203,7 @@ def hit2df(
     columns = columns_2004
 
     # Use cache file if possible
-    fcache = cache_file_name(fname, engine=engine)
+    fcache = DataFileManager(engine).cache_file(fname)
     if cache and exists(fcache):
         relevant_if_metadata_above = (
             {"wavenum_max": load_wavenum_min} if load_wavenum_min else {}
@@ -202,6 +227,8 @@ def hit2df(
         if df is not None:
             return df
 
+    #  %% Start reading the full file
+
     # Detect the molecule by reading the start of the file
     try:
         with open(fname) as f:
@@ -212,10 +239,106 @@ def hit2df(
             + "instead of an HITRAN file"
         ) from err
 
-    # %% Start reading the full file
-
     df = parse_hitran_file(fname, columns)
 
+    df = post_process_hitran_data(
+        df,
+        molecule=mol,
+        parse_quanta=parse_quanta,
+    )
+    # cached file mode but cached file doesn't exist yet (else we had returned)
+    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
+                )
+            )
+        from radis import __version__
+
+        try:
+            save_to_hdf(
+                df,
+                fcache,
+                metadata=new_metadata,
+                version=__version__,
+                overwrite=True,
+                verbose=verbose,
+                engine=engine,
+            )
+        except PermissionError:
+            if verbose:
+                print(sys.exc_info())
+                print("An error occured in cache file generation. Lookup access rights")
+            pass
+
+    # TODO : get only wavenum above/below 'load_wavenum_min', 'load_wavenum_max'
+    # by parsing df.wav.   Completely irrelevant files are discarded in 'load_h5_cache_file'
+    # but files that have partly relevant lines are fully loaded.
+    # Note : cache file is generated with the full line list.
+
+    return df
+
+
+def post_process_hitran_data(
+    df,
+    molecule,
+    verbose=True,
+    drop_non_numeric=True,
+    parse_quanta=True,
+):
+    """Parsing non-equilibrum parameters in HITRAN/HITEMP [1]_ file to and return final Pandas Dataframe
+
+    Parameters
+    ----------
+    df: pandas Dataframe
+      dataframe containing generic parameters
+
+    molecule: str
+       molecule name
+
+    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.
+    parse_quanta: bool
+        if ``True``, parse local & global quanta (required to identify lines
+        for non-LTE calculations ; but sometimes lines are not labelled.)
+
+    Returns
+    -------
+    df: pandas Dataframe
+        dataframe containing all lines and parameters
+
+
+    References
+    ----------
+
+    .. [1] `HITRAN 1996, Rothman et al., 1998 <https://www.sciencedirect.com/science/article/pii/S0022407398000788>`__
+
+
+
+    Notes
+    -----
+
+    Performances: see CDSD-HITEMP parser
+
+
+    See Also
+    --------
+
+    :func:`~radis.io.cdsd.cdsd2df`
+    """
+
     # %% Post processing
 
     # assert one molecule per database only. Else the groupbase data reading
@@ -243,7 +366,7 @@ def hit2df(
     if parse_quanta:
         # Add local quanta attributes, based on the HITRAN group
         try:
-            df = parse_local_quanta(df, mol, verbose=verbose)
+            df = parse_local_quanta(df, molecule, verbose=verbose)
         except ValueError as err:
             # Empty strings (unlabelled lines) have been reported for HITEMP2010-H2O.
             # In this case, do not parse (makes non-equilibrium calculations impossible).
@@ -252,12 +375,12 @@ def hit2df(
                 print(str(err))
                 print("-" * 10)
                 print(
-                    f"Impossible to parse local quanta in {fname}, probably an unlabelled line. Ignoring, but nonequilibrium calculations will not be possible. See details above."
+                    f"Impossible to parse local quanta in {molecule}, probably an unlabelled line. Ignoring, but nonequilibrium calculations will not be possible. See details above."
                 )
 
         # Add global quanta attributes, based on the HITRAN class
         try:
-            df = parse_global_quanta(df, mol, verbose=verbose)
+            df = parse_global_quanta(df, molecule, verbose=verbose)
         except ValueError as err:
             # Empty strings (unlabelled lines) have been reported for HITEMP2010-H2O.
             # In this case, do not parse (makes non-equilibrium calculations impossible).
@@ -266,7 +389,7 @@ def hit2df(
                 print(str(err))
                 print("-" * 10)
                 print(
-                    f"Impossible to parse global quanta in {fname}, probably an unlabelled line. Ignoring, but nonequilibrium calculations will not be possible. See details above."
+                    f"Impossible to parse global quanta in {molecule}, probably an unlabelled line. Ignoring, but nonequilibrium calculations will not be possible. See details above."
                 )
 
     # Remove non numerical attributes
@@ -275,43 +398,6 @@ def hit2df(
             replace_PQR_with_m101(df)
         df = drop_object_format_columns(df, verbose=verbose)
 
-    # cached file mode but cached file doesn't exist yet (else we had returned)
-    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
-                )
-            )
-        from radis import __version__
-
-        try:
-            save_to_hdf(
-                df,
-                fcache,
-                metadata=new_metadata,
-                version=__version__,
-                overwrite=True,
-                verbose=verbose,
-                engine=engine,
-            )
-        except PermissionError:
-            if verbose:
-                print(sys.exc_info())
-                print("An error occured in cache file generation. Lookup access rights")
-            pass
-
-    # TODO : get only wavenum above/below 'load_wavenum_min', 'load_wavenum_max'
-    # by parsing df.wav.   Completely irrelevant files are discarded in 'load_h5_cache_file'
-    # but files that have partly relevant lines are fully loaded.
-    # Note : cache file is generated with the full line list.
-
     return df
 
 
@@ -1062,6 +1148,7 @@ def __init__(
         molecule,
         local_databases,
         engine="default",
+        extra_params=None,
         verbose=True,
         parallel=True,
     ):
@@ -1070,6 +1157,7 @@ def __init__(
             molecule,
             local_databases,
             engine=engine,
+            extra_params=extra_params,
             verbose=verbose,
             parallel=parallel,
         )
@@ -1100,9 +1188,8 @@ def download_and_parse(self, local_file, cache=True, parse_quanta=True):
         opener: an opener with an .open() command
         gfile : file handler. Filename: for info"""
 
-        from hapi import db_begin, fetch
+        from hapi import LOCAL_TABLE_CACHE, db_begin, fetch
 
-        from radis import hit2df
         from radis.db.classes import get_molecule_identifier
 
         if isinstance(local_file, list):
@@ -1114,12 +1201,15 @@ def download_and_parse(self, local_file, cache=True, parse_quanta=True):
         wmin = 1
         wmax = 40000
 
-        def download_all_hitran_isotopes(molecule, directory):
+        def download_all_hitran_isotopes(molecule, directory, extra_params):
             """Blindly try to download all isotpes 1 - 9 for the given molecule
 
             .. warning::
                 this won't be able to download higher isotopes (ex : isotope 10-11-12 for CO2)
                 Neglected for the moment, they're irrelevant for most calculations anyway
+            .. Isotope Missing:
+                When an isotope is missing for a particular molecule then a key error `(molecule_id, isotope_id)
+                is raised.
 
             """
             # create temp folder :
@@ -1147,10 +1237,35 @@ def download_all_hitran_isotopes(molecule, directory):
                         )
                         os.remove(join(directory, file + ".data"))
                 try:
-                    fetch(file, get_molecule_identifier(molecule), iso, wmin, wmax)
-                except KeyError:
-                    # Isotope not defined:
-                    continue
+                    if extra_params == "all":
+                        fetch(
+                            file,
+                            get_molecule_identifier(molecule),
+                            iso,
+                            wmin,
+                            wmax,
+                            ParameterGroups=[*PARAMETER_GROUPS_HITRAN],
+                        )
+                    elif extra_params is None:
+                        fetch(file, get_molecule_identifier(molecule), iso, wmin, wmax)
+                    else:
+                        raise ValueError("extra_params can only be 'all' or None ")
+                except KeyError as err:
+                    list_pattern = ["(", ",", ")"]
+                    import re
+
+                    if (
+                        set(list_pattern).issubset(set(str(err)))
+                        and len(re.findall("\d", str(err))) >= 2
+                        and get_molecule_identifier(molecule)
+                        == int(
+                            re.findall(r"[\w']+", str(err))[0]
+                        )  # The regex are cryptic
+                    ):
+                        # Isotope not defined, go to next isotope
+                        continue
+                    else:
+                        raise KeyError("Error: {0}".format(str(err)))
                 else:
                     isotope_list.append(iso)
                     data_file_list.append(file + ".data")
@@ -1164,23 +1279,47 @@ def download_all_hitran_isotopes(molecule, directory):
         # create database in a subfolder to isolate molecules from one-another
         # (HAPI doesn't check and may mix molecules --> see failure at https://app.travis-ci.com/github/radis/radis/jobs/548126303#L2676)
         tempdir = join(self.tempdir, molecule)
+        extra_params = self.extra_params
 
         # Use HAPI only to download the files, then we'll parse them with RADIS's
         # parsers, and convert to RADIS's fast HDF5 file formats.
         isotope_list, data_file_list, header_file_list = download_all_hitran_isotopes(
-            molecule, tempdir
+            molecule, tempdir, extra_params
         )
 
-        writer = self.get_hdf5_manager()
+        writer = self.get_datafile_manager()
 
         # Create HDF5 cache file for all isotopes
         Nlines = 0
         for iso, data_file in zip(isotope_list, data_file_list):
-            df = hit2df(
-                join(tempdir, data_file),
-                cache=False,  # do not generate cache yet
+            df = pd.DataFrame(LOCAL_TABLE_CACHE[data_file.split(".")[0]]["data"])
+            df.rename(
+                columns={
+                    "molec_id": "id",
+                    "local_iso_id": "iso",
+                    "nu": "wav",
+                    "sw": "int",
+                    "a": "A",
+                    "gamma_air": "airbrd",
+                    "gamma_self": "selbrd",
+                    "elower": "El",
+                    "n_air": "Tdpair",
+                    "delta_air": "Pshft",
+                    "global_upper_quanta": "globu",
+                    "global_lower_quanta": "globl",
+                    "local_upper_quanta": "locu",
+                    "local_lower_quanta": "locl",
+                    "gp": "gp",
+                    "gpp": "gpp",
+                },
+                inplace=True,
+            )
+            df = post_process_hitran_data(
+                df,
+                molecule=molecule,
                 parse_quanta=parse_quanta,
             )
+
             wmin_final = min(wmin_final, df.wav.min())
             wmax_final = max(wmax_final, df.wav.max())
             Nlines += len(df)
@@ -1234,9 +1373,9 @@ def register(self):
             df_full = self.load(
                 local_files,
                 columns=["wav"],
-                isotope=None,
-                load_wavenum_min=None,
-                load_wavenum_max=None,
+                within=[],
+                lower_bound=[],
+                upper_bound=[],
             )
             self.wmin = df_full.wav.min()
             self.wmax = df_full.wav.max()
@@ -1272,6 +1411,7 @@ def register(self):
 # TODO: implement parallel=True for all isotopes ?
 def fetch_hitran(
     molecule,
+    extra_params=None,
     local_databases=None,
     databank_name="HITRAN-{molecule}",
     isotope=None,
@@ -1310,6 +1450,14 @@ def fetch_hitran(
         load only specific wavenumbers.
     columns: list of str
         list of columns to load. If ``None``, returns all columns in the file.
+    extra_params: 'all' or None
+        Downloads all additional columns available in the HAPI database for the molecule including
+        parameters like `gamma_co2`, `n_co2` that are required to calculate spectrum in co2 diluent.
+        For eg:
+        ::
+
+            from radis.io.hitran import fetch_hitran
+            df = fetch_hitran('CO', extra_params='all', cache='regen') # cache='regen' to regenerate new database with additional columns
 
     Other Parameters
     ----------------
@@ -1326,7 +1474,12 @@ def fetch_hitran(
         which HDF5 library to use. 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.
+        jax arrays. If ``'vaex'``, output is a :py:class:`vaex.dataframe.DataFrameLocal`
+
+        .. note::
+            Vaex DataFrames are memory-mapped. They do not take any space in RAM
+            and are extremelly useful to deal with the largest databases.
+
     parallel: bool
         if ``True``, uses joblib.parallel to load database with multiple processes
     parse_quanta: bool
@@ -1386,6 +1539,7 @@ def fetch_hitran(
         molecule=molecule,
         local_databases=local_databases,
         engine=engine,
+        extra_params=extra_params,
         verbose=verbose,
         parallel=parallel,
     )
@@ -1396,6 +1550,27 @@ def fetch_hitran(
     # Delete files if needed:
     if cache == "regen":
         ldb.remove_local_files(local_file)
+    else:
+        # Raising AccuracyWarning if local_file exists and doesn't have extra columns in it
+        if ldb.get_existing_files(local_file) and extra_params == "all":
+            columns = ldb.get_columns(local_file[0])
+            extra_columns = ["y_", "gamma_", "n_"]
+            found = False
+            for key in extra_columns:
+                for column_name in columns:
+                    if key in column_name:
+                        found = True
+                        break
+
+            if not found:
+                import warnings
+
+                warnings.warn(
+                    AccuracyWarning(
+                        "All columns are not downloaded currently, please use cache = 'regen' and extra_params='all' to download all columns."
+                    )
+                )
+
     ldb.check_deprecated_files(
         ldb.get_existing_files(local_file),
         auto_remove=True if cache != "force" else False,
@@ -1417,9 +1592,10 @@ def fetch_hitran(
     df = ldb.load(
         local_file,
         columns=columns,
-        isotope=isotope,
-        load_wavenum_min=load_wavenum_min,  # for relevant files, get only the right range
-        load_wavenum_max=load_wavenum_max,
+        within=[("iso", isotope)] if isotope is not None else [],
+        # for relevant files, get only the right range :
+        lower_bound=[("wav", load_wavenum_min)] if load_wavenum_min is not None else [],
+        upper_bound=[("wav", load_wavenum_max)] if load_wavenum_max is not None else [],
         output=output,
     )
 
diff --git a/radis/io/npy.py b/radis/io/npy.py
index aa6984080..578416d55 100644
--- a/radis/io/npy.py
+++ b/radis/io/npy.py
@@ -14,7 +14,6 @@ def npy2df(keywords, verbose=True):
 
     Parameters
     ----------
-
     keywords: dict
 
         npy2df({'wav':'PATH/TO/v0.npy',
@@ -26,7 +25,6 @@ def npy2df(keywords, verbose=True):
 
     Examples
     --------
-
     ::
 
         npy2df({'wav':'PATH/TO/v0.npy',
@@ -36,7 +34,7 @@ def npy2df(keywords, verbose=True):
           'Tdpair':'PATH/TO/Tdpair.npy',
           'El':'PATH/TO/Tdpair.npy'}
 
-    See definitions for instance in :py:data:`~radis.io.hitran.column_2004`
+    See definitions for instance in :py:data:`~radis.io.hitran.columns_2004`
     """
 
     database = keywords  # this has to change. Now database should be a dictionary (cf Example) # TODO
diff --git a/radis/lbl/base.py b/radis/lbl/base.py
index b43d58b66..1f260edac 100644
--- a/radis/lbl/base.py
+++ b/radis/lbl/base.py
@@ -194,7 +194,6 @@ def print_conditions(self, preprend=None):
 
         Parameters
         ----------
-
         preprend: str
             just to text to display before printing conditions
         """
@@ -206,22 +205,18 @@ def print_conditions(self, preprend=None):
 
         return print_conditions(conditions, self.cond_units, verbose=self.verbose)
 
-    def get_energy_levels(self, molecule, isotope, state, conditions=None):
+    def get_energy_levels(self, molecule, isotope, state="X", conditions=None):
         """Return energy levels database for given molecule > isotope > state
         (look up Factory.parsum_calc[molecule][iso][state])
 
         Parameters
         ----------
-
         molecule: str
             molecule name
-
         isotope: int
             isotope identifier
-
         state: str:
-            electronic state
-
+            electronic state. Default ``'X'`` (ground-state)
         conditions: str, or ``None``
             if not None, add conditions on which energies to retrieve, e.g:
 
@@ -232,14 +227,12 @@ def get_energy_levels(self, molecule, isotope, state, conditions=None):
 
         Returns
         -------
-
         energies: pandas Dataframe
             a view of the energies stored in the Partition Function calculator
             for isotope iso. If conditions are applied, we get a copy
 
         See Also
         --------
-
         :meth:`~radis.lbl.base.BaseFactory.get_populations`
         """
 
@@ -252,7 +245,15 @@ def get_energy_levels(self, molecule, isotope, state, conditions=None):
 
     def plot_linestrength_hist(self, cutoff=None):
         """Plot linestrength distribution (to help determine a cutoff
-        criteria)"""
+        criteria)
+
+        Examples
+        --------
+        ::
+
+            s, sf = calc_spectrum(..., return_factory=True)
+            sf.plot_linestrength_hist()
+        """
         return self.plot_hist("df1", "S", axvline=np.log10(cutoff))
 
     def plot_hist(self, dataframe="df0", what="int", axvline=None):
@@ -1623,6 +1624,19 @@ def _calc_degeneracies(self, df):
         df["gu"] = df.gvibu * df.grotu
         df["gl"] = df.gvibl * df.grotl
 
+        # Check consistency if "gp" already existed
+        # https://github.com/radis/radis/pull/514#issuecomment-1229463074
+        if "gp" in df:
+            if (df["gp"] == df["gu"]).all():
+                self.warn(
+                    "'gu' was recomputed although 'gp' already in DataFrame. All values are equal",
+                    "PerformanceWarning",
+                )
+            else:
+                raise ValueError(
+                    "'gu' was recomputed although 'gp' already in DataFrame. Values are not equal ! Check calculations"
+                )
+
         return None  # dataframe updated directly
 
     def get_lines_abundance(self, df):
@@ -1679,46 +1693,27 @@ def get_molar_mass(self, df):
         -------
         The molar mass of all the isotopes in the dataframe
 
-
-        Notes
-        -----
-
-        If molar mass is unknown, it can be temporarily added in the
-        :py:attr:`radis.lbl.loader.DatabankLoader._EXTRA_MOLAR_MASS` dictionary
         """
         molpar = self.molparam
 
         if "id" in df.columns:
-            raise NotImplementedError(">1 molecule")
-        elif "id" in df.attrs:
-            id = df.attrs["id"]
+            raise NotImplementedError(
+                "'id' still in DataFrame columsn. Is there more than 1 molecule ?"
+            )
+        if "id" in df.attrs:
+            molecule = df.attrs["id"]
         else:
-            # HARDCODED molar mass; for WIP ExoMol implementation, until MolParams
-            # is an attribute and can be updated with definitions from ExoMol.
-            # https://github.com/radis/radis/issues/321
-
-            # see :py:attr:`radis.lbl.loader.DatabankLoader._EXTRA_MOLAR_MASS`
-            try:
-                return self._EXTRA_MOLAR_MASS[df.attrs["molecule"]][
-                    str(df.attrs["iso"])
-                ]
-            except KeyError:
-                raise NotImplementedError(
-                    "Molar mass of {0} (isotope {1}) is unknown.".format(
-                        df.attrs["molecule"], df.attrs["iso"]
-                    )
-                    + " You can manually add it in your radis.json file {'molparams':{'molar_mass':{'molecule':{'ISOTOPE':...}}}}; or in the SpectrumFactory._EXTRA_MOLAR_MASS[molecule][isotope] = M (g/mol) dictionary. Please also report on GitHub so we can update !"
-                )
+            molecule = df.attrs["molecule"]
 
         if "iso" in df.columns:
             iso_set = df.iso.unique()
             molar_mass_dict = {}
             for iso in iso_set:
-                molar_mass_dict[iso] = molpar.get(id, iso, "mol_mass")
+                molar_mass_dict[iso] = molpar.get(molecule, iso, "mol_mass")
             molar_mass = df["iso"].map(molar_mass_dict)
         else:
             iso = df.attrs["iso"]
-            molar_mass = molpar.get(id, iso, "mol_mass")
+            molar_mass = molpar.get(molecule, iso, "mol_mass")
 
         return molar_mass
 
@@ -2015,7 +2010,16 @@ def calc_S0(self):
 
         self.profiler.start("scaled_S0", 2, "... Scaling equilibrium linestrength")
 
-        gp = df0["gp"]
+        try:
+            gp = df0["gp"]
+        except (KeyError):
+
+            if not "gu" in df0:
+                if not "ju" in df0:
+                    self._add_ju(df0)
+                self._calc_degeneracies(df0)
+            gp = df0["gu"]
+
         A = df0["A"]
         wav = df0["wav"]
         Ia = self.get_lines_abundance(df0)
diff --git a/radis/lbl/broadening.py b/radis/lbl/broadening.py
index 554a63e9b..8d3fece75 100644
--- a/radis/lbl/broadening.py
+++ b/radis/lbl/broadening.py
@@ -2126,6 +2126,53 @@ def _broaden_lines(self, df):
             line dataframe
 
         See _calc_lineshape for more information
+
+        Examples
+        ----------
+        s = calc_spectrum(
+            2135,
+            2170,
+            molecule="CO",
+            isotope="1",
+            pressure=3,
+            Tgas=2000,
+            mole_fraction=0.1,
+            path_length=1,
+            databank="hitemp",
+            name="Chunksize=1e7",
+            chunksize=1e7,
+            optimization = "min-RMS",
+        )
+
+        Alternatively, you can also initialize a SpectrumFactory object and
+        include chunksize, as follows:
+
+        sf = SpectrumFactory(
+            wavelength_min=4000,
+            wavelength_max=4500,
+            cutoff=1e-27,
+            pressure=1,
+            isotope="1,2",
+            truncation=5,
+            neighbour_lines=5,
+            path_length=0.1,
+            mole_fraction=1e-3,
+            medium="vacuum",
+            optimization=None,
+            chunksize=1e7,
+            wstep=0.001,
+            verbose=False,
+        )
+        sf.load_databank("HITEMP-CO")
+
+        To plot:
+
+        s.plot("abscoeff")
+        plt.show()
+
+        To iterate over the entire dataframe at once (not recommended for large molecules
+        unless you have large RAM), just pass chunksize = None
+
         """
         # --------------------------
 
@@ -2145,56 +2192,93 @@ def _broaden_lines(self, df):
             self.misc.zero_padding = len(self.wavenumber_calc)
 
         try:
-            if optimization in ("simple", "min-RMS"):
-                self.reftracker.add(doi["DIT-2020"], "algorithm")
-                # Use LDM
+            if chunksize is None:
+                # Deal with all lines directly (usually faster)
+                if optimization is None:
+
+                    # printing estimated time
+                    if self.verbose >= 2:
+                        estimated_time = self.predict_time()
+                        print(
+                            "Estimated time for calculating broadening: {0:.2f}s on 1 CPU".format(
+                                estimated_time
+                            )
+                        )
 
-                line_profile_LDM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_LDM(df)
-                # printing estimated time
-                if self.verbose >= 2:
-                    estimated_time = self.predict_time()
-                    print(
-                        "Estimated time for calculating broadening: {0:.2f}s on 1 CPU".format(
-                            estimated_time
+                    line_profile = self._calc_lineshape(df)  # usually the bottleneck
+                    (wavenumber, abscoeff) = self._apply_lineshape(
+                        df.S.values, line_profile, df.shiftwav.values
+                    )
+                elif optimization in ("simple", "min-RMS"):
+                    self.reftracker.add(doi["DIT-2020"], "algorithm")
+                    (
+                        line_profile_LDM,
+                        wL,
+                        wG,
+                        wL_dat,
+                        wG_dat,
+                    ) = self._calc_lineshape_LDM(df)
+
+                    # printing estimated time
+                    if self.verbose >= 2:
+                        estimated_time = self.predict_time()
+                        print(
+                            "Estimated time for calculating broadening: {0:.2f}s on 1 CPU".format(
+                                estimated_time
+                            )
                         )
+
+                    (wavenumber, abscoeff) = self._apply_lineshape_LDM(
+                        df.S.values,
+                        line_profile_LDM,
+                        df.shiftwav.values,
+                        wL,
+                        wG,
+                        wL_dat,
+                        wG_dat,
+                        self.params.optimization,
+                    )
+                else:
+                    raise ValueError(
+                        "Unexpected value for optimization: {0}".format(optimization)
                     )
-                (wavenumber, abscoeff) = self._apply_lineshape_LDM(
-                    df.S.values,
-                    line_profile_LDM,
-                    df.shiftwav.values,
-                    wL,
-                    wG,
-                    wL_dat,
-                    wG_dat,
-                    self.params.optimization,
-                )
 
-            elif optimization is None:
-                # printing estimated time
-                if self.verbose >= 2:
-                    estimated_time = self.predict_time()
-                    print(
-                        "Estimated time for calculating broadening: {0:.2f}s on 1 CPU".format(
-                            estimated_time
+            elif is_float(chunksize):
+                # Cut lines in smaller bits for better memory handling
+                N = int(len(df) * len(wavenumber) / chunksize) + 1
+                # Too big may be faster but overload memory.
+                # See Performance for more information
+
+                # Raise performance warning if the chunks are bigger
+                # than the number of lines.
+                if N >= len(df):
+                    self.warn(
+                        "We suggest increasing chunksize to"
+                        + " {0:.1e} - {1:.1e}".format(
+                            10000 * len(wavenumber), 100000 * len(wavenumber)
                         )
+                        + " to speed up calculations. Currently, there are more chunks"
+                        + " ({0:.3e}) than lines ({1:.3e}).".format(N, len(df))
+                        + " Hence, calculation times will be extremely slow, since broadening will be"
+                        + " calculated using only one line per iteration. Ideally, 10,000 - 100,000"
+                        + " lines per chunk are recommended.",
+                        "PerformanceWarning",
                     )
-                if chunksize is None:
 
-                    # Deal with all lines directly (usually faster)
-                    line_profile = self._calc_lineshape(df)  # usually the bottleneck
-                    (wavenumber, abscoeff) = self._apply_lineshape(
-                        df.S.values, line_profile, df.shiftwav.values
-                    )
+                abscoeff = zeros_like(self.wavenumber)
+                pb = ProgressBar(N, active=self.verbose)
 
-                elif is_float(chunksize):
-                    # Cut lines in smaller bits for better memory handling
-                    N = int(len(df) * len(wavenumber) / chunksize) + 1
-                    # Too big may be faster but overload memory.
-                    # See Performance for more information
+                if optimization is None:
 
-                    abscoeff = zeros_like(self.wavenumber)
+                    # printing estimated time
+                    if self.verbose >= 2:
+                        estimated_time = self.predict_time()
+                        print(
+                            "Estimated time for calculating broadening: {0:.2f}s on 1 CPU".format(
+                                estimated_time
+                            )
+                        )
 
-                    pb = ProgressBar(N, active=self.verbose)
                     for i, (_, dg) in enumerate(df.groupby(arange(len(df)) % N)):
                         line_profile = self._calc_lineshape(dg)
                         (wavenumber, absorption) = self._apply_lineshape(
@@ -2203,14 +2287,41 @@ def _broaden_lines(self, df):
                         abscoeff += absorption
                         pb.update(i)
                     pb.done()
+
+                elif optimization in ("simple", "min-RMS"):
+
+                    self.reftracker.add(doi["DIT-2020"], "algorithm")
+                    # Iterating over the chunks of the line database
+                    # Using DIT Algorithm calculations for optimized loops
+                    for i, (_, dg) in enumerate(df.groupby(arange(len(df)) % N)):
+                        (
+                            line_profile_LDM,
+                            wL_i,
+                            wG_i,
+                            wL_dat_i,
+                            wG_dat_i,
+                        ) = self._calc_lineshape_LDM(dg)
+                        (wavenumber, absorption) = self._apply_lineshape_LDM(
+                            dg.S.values,
+                            line_profile_LDM,
+                            dg.shiftwav.values,
+                            wL_i,
+                            wG_i,
+                            wL_dat_i,
+                            wG_dat_i,
+                            self.params.optimization,
+                        )
+                        abscoeff += absorption
+                        pb.update(i)
+                    pb.done()
                 else:
                     raise ValueError(
-                        "Unexpected value for chunksize: {0}".format(chunksize)
+                        "Unexpected value for optimization: {0}".format(optimization)
                     )
 
             else:
                 raise ValueError(
-                    "Unexpected value for optimization: {0}".format(optimization)
+                    "Unexpected value for chunksize: {0}".format(chunksize)
                 )
 
         except MemoryError as err:
diff --git a/radis/lbl/calc.py b/radis/lbl/calc.py
index 0a659d577..0786794b6 100644
--- a/radis/lbl/calc.py
+++ b/radis/lbl/calc.py
@@ -55,6 +55,7 @@ def calc_spectrum(
     cutoff=1e-27,
     parsum_mode="full summation",
     optimization="simple",
+    chunksize=None,
     broadening_method="voigt",
     overpopulation=None,
     name=None,
@@ -292,6 +293,9 @@ def calc_spectrum(
         - Or the :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` method to plot it
           directly.
         - See [1]_ to get an overview of all Spectrum methods
+    :py:class:`~radis.lbl.factory.SpectrumFactory` : if using ``return_factory=True``
+        if multiple molecules, a dictionary of factories is returned
+
 
     References
     ----------
@@ -532,6 +536,7 @@ def _check_molecules_are_consistent(
             cutoff=cutoff,
             parsum_mode=parsum_mode,
             optimization=optimization,
+            chunksize=chunksize,
             broadening_method=broadening_method,
             name=name,
             use_cached=use_cached,
@@ -560,6 +565,8 @@ def _check_molecules_are_consistent(
         s.store(path=save_to, if_exists_then="replace", verbose=verbose)
 
     if return_factory:
+        if len(factory_dict) == 1:  # return unique element directly
+            factory_dict = factory_dict[list(factory_dict.keys())[0]]
         return s, factory_dict
     else:
         return s
@@ -586,6 +593,7 @@ def _calc_spectrum_one_molecule(
     cutoff,
     parsum_mode,
     optimization,
+    chunksize,
     broadening_method,
     name,
     use_cached,
@@ -628,8 +636,8 @@ def _calc_spectrum_one_molecule(
         # factory is used once only
         kwargs["save_memory"] = True
 
-    if "chunksize" in kwargs:
-        raise DeprecationWarning("use optimization= instead of chunksize=")
+    # if "chunksize" in kwargs:
+    #     raise DeprecationWarning("use optimization= instead of chunksize=")
 
     def _is_at_equilibrium():
         try:
@@ -665,6 +673,7 @@ def _is_at_equilibrium():
         parsum_mode=parsum_mode,
         verbose=verbose,
         optimization=optimization,
+        chunksize=chunksize,
         broadening_method=broadening_method,
         export_lines=export_lines,
         **kwargs,
diff --git a/radis/lbl/factory.py b/radis/lbl/factory.py
index 287deaaef..cd1e3239b 100644
--- a/radis/lbl/factory.py
+++ b/radis/lbl/factory.py
@@ -936,7 +936,7 @@ def eq_spectrum_gpu(
         .. note::
             This method requires CUDA compatible hardware to execute.
             For more information on how to setup your system to run GPU-accelerated methods
-            using CUDA and Cython, check `GPU Spectrum Calculation on RADIS <https://radis.readthedocs.io/en/latest/lbl/gpu.html>`
+            using CUDA and Cython, check :ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>`
 
         Parameters
         ----------
@@ -1060,11 +1060,12 @@ def eq_spectrum_gpu(
         molarmass_arr = np.empty_like(
             iso_list, dtype=np.float32
         )  # molar mass of each isotope
+
         Q_interp_list = []
         for iso in iso_list:
             if iso in iso_set:
                 params = molpar.df.loc[(mol_id, iso)]
-                molarmass_arr[iso] = params.mol_mass
+                molarmass_arr[iso] = params.molar_mass
                 parsum = self.get_partition_function_interpolator(molecule, iso, state)
                 Q_interp_list.append(parsum.at)
             else:
@@ -1084,8 +1085,13 @@ def eq_spectrum_gpu(
 
         # load the data
         df = self.df0
-        iso = df["iso"].to_numpy(dtype=np.uint8)
         v0 = df["wav"].to_numpy(dtype=np.float32)
+
+        if len(iso_set) > 1:
+            iso = df["iso"].to_numpy(dtype=np.uint8)
+        elif len(iso_set) == 1:
+            iso = np.full(len(v0), iso_set[0], dtype=np.uint8)
+
         da = df["Pshft"].to_numpy(dtype=np.float32)
         El = df["El"].to_numpy(dtype=np.float32)
         na = df["Tdpair"].to_numpy(dtype=np.float32)
@@ -1341,6 +1347,13 @@ def eq_spectrum_gpu_interactive(
         fig = line.figure
 
         def update_plot(val):
+            # TODO : refactor this function and the update() mechanism. Ensure conditions are correct.
+            # Update conditions
+            # ... at equilibirum, temperatures remain equal :
+            s.conditions["Tvib"] = s.conditions["Tgas"]
+            s.conditions["Trot"] = s.conditions["Tgas"]
+            s.conditions["slit_function"] = s.conditions["slit_FWHM"]
+
             abscoeff, transmittance, iter_params = gpu_iterate(
                 s.conditions["pressure"],
                 s.conditions["Tgas"],
@@ -1351,46 +1364,21 @@ def update_plot(val):
                 gpu=(not s.conditions["emulate_gpu"]),
             )
 
-            # s._q["abscoeff"] = abscoeff
-            # s.update(var)    # adds required spectral array
-            # _, new_y = s.get(var)   # can also use wunits, Iunits, etc.
-
-            # TODO: the following should obviously be a self contained function..
-
-            if var == "abscoeff":
-                new_y = abscoeff
-            elif var == "transmittance":
-                new_y = transmittance
-            elif var in ["emissivity", "radiance"]:
-                emissivity = 1 - transmittance
-                if var == "emissivity":
-                    new_y = emissivity
-                else:  # var = 'radiance':
-                    new_y = calc_radiance(
-                        s.get_wavenumber(),
-                        emissivity,
-                        s.conditions["Tgas"],
-                        unit=self.units["radiance"],
-                    )
-            else:
-                absorbance = abscoeff * s.conditions["path_length"]
-                if var == "absorbance":
-                    new_y = absorbance
+            # This happen inside a Spectrum() method
+            for k in list(s._q.keys()):  # reset all quantities
+                if k in ["wavespace", "wavelength", "wavenumber"]:
+                    pass
+                elif k == "abscoeff":
+                    s._q["abscoeff"] = abscoeff
                 else:
-                    transmittance_noslit = exp(-absorbance)
-                    if var == "transmittance_noslit":
-                        new_y = transmittance_noslit
-                    else:
-                        emissivity_noslit = 1 - transmittance_noslit
-                        if var == "emissivity_noslit":
-                            new_y = emissivity_noslit
-                        else:
-                            new_y = calc_radiance(
-                                s.get_wavenumber(),
-                                emissivity_noslit,
-                                s.conditions["Tgas"],
-                                unit=self.units["radiance_noslit"],
-                            )
+                    del s._q[k]
+
+            _, new_y = s.get(
+                var,
+                copy=False,  # copy = False saves some time & memory, it's a pointer/reference to the real data, which is fine here as data is just plotted
+                wunit=plotkwargs.get("wunit", "default"),
+                Iunit=plotkwargs.get("Iunit", "default"),
+            )
 
             line.set_ydata(new_y)
             fig.canvas.draw_idle()
@@ -1481,18 +1469,38 @@ def non_eq_spectrum(
         Examples
         --------
         ::
+
             from radis import SpectrumFactory
             sf = SpectrumFactory(
             wavenum_min=2000,
             wavenum_max=3000,
             molecule="CO",
-            wstep=0.1,
+            isotope="1,2,3",
             )
-            sf.fetch_databank("hitemp")
+            sf.fetch_databank("hitemp", load_columns='noneq')
 
             s1 = sf.non_eq_spectrum(Tvib=2000, Trot=600, path_length=1, pressure=0.1)
             s2 = sf.non_eq_spectrum(Tvib=2000, Trot=600, path_length=1, pressure=0.1)
 
+        Multi-vibrational temperature. Below we compare non-LTE spectra of CO2 where all
+        vibrational temperatures are equal, or where the bending & symmetric modes are in
+        equilibrium with rotation ::
+
+            from radis import SpectrumFactory
+            sf = SpectrumFactory(
+            wavenum_min=2000,
+            wavenum_max=3000,
+            molecule="CO2",
+            isotope="1,2,3",
+            )
+            sf.fetch_databank("hitemp", load_columns='noneq')
+            # nonequilibrium between bending+symmetric and asymmetric modes :
+            s1 = sf.non_eq_spectrum(Tvib=(600, 600, 2000), Trot=600, path_length=1, pressure=1)
+            # all vibrational temperatures are equal :
+            s2 = sf.non_eq_spectrum(Tvib=(2000, 2000, 2000), Trot=600, path_length=1, pressure=1)
+
+
+
         .. minigallery:: radis.lbl.SpectrumFactory.non_eq_spectrum
             :add-heading:
 
@@ -2331,10 +2339,14 @@ def print_perf_profile(self, number_format="{:.3f}", precision=16):
 
     def generate_perf_profile(self):
         """Generate a visual/interactive performance profile diagram for
-        the last calculated spectrum
+        the last calculated spectrum, with ``tuna``.
+
+        Requires a `profiler` key with in Spectrum.conditions, and ``tuna``
+        installed.
 
-        .. note:
-            requires a `profiler` key with in Spectrum.conditions
+        .. note::
+            :py:meth:`~radis.spectrum.spectrum.Spectrum.print_perf_profile` is an
+            ascii-version which does not require ``tuna``.
 
         Examples
         --------
diff --git a/radis/lbl/gpu.cpp b/radis/lbl/gpu.cpp
index a63cd204b..49f06d8fb 100644
--- a/radis/lbl/gpu.cpp
+++ b/radis/lbl/gpu.cpp
@@ -138,9 +138,9 @@ __global__ void fillLDM(
                         // ... scale linestrengths under equilibrium
                         float Si = iter_d.N * S0[i] * (expf(iter_d.c2T * El[i]) - expf(iter_d.c2T * (El[i] + v0[i]))) / iter_d.Q[iso[i]];
 
-                        float avi = ki - k0i;
-                        float aGi = li - l0i;
-                        float aLi = mi - m0i;
+                        float avi = ki - (float)k0i;
+                        float aGi = li - (float)l0i;
+                        float aLi = mi - (float)m0i;
 
                         float aV00i = (1 - aGi) * (1 - aLi);
                         float aV01i = (1 - aGi) * aLi;
@@ -150,14 +150,14 @@ __global__ void fillLDM(
                         float Sv0i = Si * (1 - avi);
                         float Sv1i = Si * avi;
 
-                        agnostic_add(&S_klm[m0i + l0i * N_L + k0i * N_G * N_L], aV00i * Sv0i);
-                        agnostic_add(&S_klm[m0i + l0i * N_L + k1i * N_G * N_L], aV00i * Sv1i);
-                        agnostic_add(&S_klm[m0i + l1i * N_L + k0i * N_G * N_L], aV01i * Sv0i);
-                        agnostic_add(&S_klm[m0i + l1i * N_L + k1i * N_G * N_L], aV01i * Sv1i);
-                        agnostic_add(&S_klm[m1i + l0i * N_L + k0i * N_G * N_L], aV10i * Sv0i);
-                        agnostic_add(&S_klm[m1i + l0i * N_L + k1i * N_G * N_L], aV10i * Sv1i);
-                        agnostic_add(&S_klm[m1i + l1i * N_L + k0i * N_G * N_L], aV11i * Sv0i);
-                        agnostic_add(&S_klm[m1i + l1i * N_L + k1i * N_G * N_L], aV11i * Sv1i);
+                        agnostic_add(&S_klm[k0i * N_G * N_L + l0i * N_L + m0i], Sv0i * aV00i);
+                        agnostic_add(&S_klm[k0i * N_G * N_L + l0i * N_L + m1i], Sv0i * aV01i);
+                        agnostic_add(&S_klm[k0i * N_G * N_L + l1i * N_L + m0i], Sv0i * aV10i);
+                        agnostic_add(&S_klm[k0i * N_G * N_L + l1i * N_L + m1i], Sv0i * aV11i);
+                        agnostic_add(&S_klm[k1i * N_G * N_L + l0i * N_L + m0i], Sv1i * aV00i);
+                        agnostic_add(&S_klm[k1i * N_G * N_L + l0i * N_L + m1i], Sv1i * aV01i);
+                        agnostic_add(&S_klm[k1i * N_G * N_L + l1i * N_L + m0i], Sv1i * aV10i);
+                        agnostic_add(&S_klm[k1i * N_G * N_L + l1i * N_L + m1i], Sv1i * aV11i);
                     }
                 }
             }
diff --git a/radis/lbl/gpu.py b/radis/lbl/gpu.py
index a4bc9559e..a7a96e442 100644
--- a/radis/lbl/gpu.py
+++ b/radis/lbl/gpu.py
@@ -607,6 +607,7 @@ def gpu_iterate(p, T, mole_fraction, l=1.0, slit_FWHM=0.0, verbose=0, gpu=False)
         set_iter_params = constant_memory_setter(cuda_module, "iter_d")
 
     else:
+        # isort: off
         from numpy import complex64, float32, zeros
         from numpy.fft import irfft, rfft
         from radis_cython_extensions import (
@@ -617,6 +618,8 @@ def gpu_iterate(p, T, mole_fraction, l=1.0, slit_FWHM=0.0, verbose=0, gpu=False)
             set_iter_params,
         )
 
+        # isort: on
+
         asnumpy = lambda arr: arr
 
     if verbose >= 2:
diff --git a/radis/lbl/loader.py b/radis/lbl/loader.py
index e99a64093..27ca734c1 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, expanduser, join
+from os.path import exists, expanduser, join, splitext
 from time import time
 from uuid import uuid1
 
@@ -61,9 +61,8 @@
 from radis import config
 from radis.db.classes import get_molecule
 from radis.db.molecules import getMolecule
-from radis.db.molparam import MolParams
+from radis.db.molparam import MOLPARAMS_EXTRA_PATH, MolParams
 from radis.db.references import doi
-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
@@ -718,35 +717,12 @@ def __init__(self):
         self._autoretrieveignoreconditions = []  # HACK. See _retrieve_from_database
 
         # Molecular parameters
-        self.molparam = MolParams()
+        self.molparam = MolParams(extra_file_json=MOLPARAMS_EXTRA_PATH)
         """MolParam: contains information about molar mass; isotopic abundance.
 
         See :py:class:`~radis.db.molparam.MolParams`"""
         # TODO @dev : Refactor : turn it into a Dictinoary? (easier to store as JSON Etc.)
 
-        # Extra paramaters :
-        # HARDCODED molar mass; for WIP ExoMol implementation, until MolParams
-        # is an attribute and can be updated with definitions from ExoMol.
-        # https://github.com/radis/radis/issues/321
-
-        self._EXTRA_MOLAR_MASS = config["molparams"]["molar_mass"]
-        """Extra molar mass when not found in HITRAN molecular parameter database
-        ::
-            self._EXTRA_MOLAR_MASS[molecule][isotope] = M (g/mol)
-
-        See :py:func:`radis.lbl.base.BaseFactory.get_molar_mass`
-        """
-
-        # HARDCODED isotopic abundance; for WIP ExoMol implementation, until MolParams
-        # is an attribute and can be updated with definitions from ExoMol.
-        # https://github.com/radis/radis/issues/321
-        self._EXTRA_ABUNDANCES = config["molparams"]["abundances"]
-        """Extra isotopic abundances when not found in HITRAN molecular parameter database
-        ::
-            self._EXTRA_ABUNDANCES[molecule][isotope] = Ia
-
-        """
-
         # Profiler
         self.profiler = None
 
@@ -932,15 +908,16 @@ def fetch_databank(
         memory_mapping_engine="default",
         load_columns="equilibrium",
         parallel=True,
+        extra_params=None,
     ):
-        """Fetch the latest databank files from HITRAN or HITEMP with the
-        https://hitran.org/ API.
+        """Fetch the latest files from [HITRAN-2020]_, [HITEMP-2010]_ (or newer),
+        [ExoMol-2020]_  or [GEISA-2020] , and store them locally in memory-mapping
+        formats for extremelly fast access.
 
         Parameters
         ----------
-        source: ``'hitran'``, ``'hitemp'``, ``'exomol'``
-            [Download database lines from the latest HITRAN (see [HITRAN-2020]_),
-            HITEMP (see [HITEMP-2010]_  )] or EXOMOL see [ExoMol-2020]_  ) databases.
+        source: ``'hitran'``, ``'hitemp'``, ``'exomol'``, ``'geisa'``
+            which database to use.
         database: ``'full'``, ``'range'``, name of an ExoMol database, or ``'default'``
             if fetching from HITRAN, ``'full'`` download the full database and register
             it, ``'range'`` download only the lines in the range of the molecule.
@@ -958,8 +935,9 @@ def fetch_databank(
             to use. Keep ``'default'`` to use the recommended one. See all available databases
             with :py:func:`radis.io.exomol.get_exomol_database_list`
 
-            By default, databases are download in `~/.radisdb`.
-            Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in ~/radis.json config file
+            By default, databases are download in ``~/.radisdb``.
+            Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in
+            ``~/radis.json`` config file
 
 
         Other Parameters
@@ -985,7 +963,7 @@ def fetch_databank(
             If ``None``, non equilibrium calculations are not possible. Default ``'radis'``.
         load_energies: boolean
             if ``False``, dont load energy levels. This means that nonequilibrium
-            spectra cannot be calculated, but it saves some memory. Default ``True``
+            spectra cannot be calculated, but it saves some memory. Default ``False``
         include_neighbouring_lines: bool
             if ``True``, includes off-range, neighbouring lines that contribute
             because of lineshape broadening. The ``neighbour_lines``
@@ -1025,14 +1003,14 @@ def fetch_databank(
 
         Notes
         -----
-        HITRAN is fetched with Astroquery [1]_  and HITEMP with
+        HITRAN is fetched with Astroquery [1]_ or [HAPI]_,  and HITEMP with
         :py:func:`~radis.io.hitemp.fetch_hitemp`
         HITEMP files are generated in a ~/.radisdb database.
 
         See Also
         --------
-        - Load from local files: :meth:`~radis.lbl.loader.DatabankLoader.load_databank`
-        - Load when needed: :meth:`~radis.lbl.loader.DatabankLoader.init_databank`
+        :meth:`~radis.lbl.loader.DatabankLoader.load_databank`,
+        :meth:`~radis.lbl.loader.DatabankLoader.init_databank`
 
         References
         ----------
@@ -1129,8 +1107,6 @@ def fetch_databank(
         # ---------------------
         self._reset_references()  # bibliographic references
 
-        from os import environ
-
         if source == "hitran":
             self.reftracker.add(doi["HITRAN-2020"], "line database")  # [HITRAN-2020]_
 
@@ -1138,16 +1114,7 @@ def fetch_databank(
                 self.reftracker.add(doi["HAPI"], "data retrieval")  # [HAPI]_
 
                 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"
+                    memory_mapping_engine = "vaex"
 
                 if isotope == "all":
                     isotope_list = None
@@ -1166,6 +1133,7 @@ def fetch_databank(
                     return_local_path=True,
                     engine=memory_mapping_engine,
                     parallel=parallel,
+                    extra_params=extra_params,
                 )
                 self.params.dbpath = ",".join(local_paths)
 
@@ -1224,16 +1192,7 @@ def fetch_databank(
             self.reftracker.add(doi["HITEMP-2010"], "line database")  # [HITEMP-2010]_
 
             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"
+                memory_mapping_engine = "vaex"
 
             if database != "full":
                 raise ValueError(
@@ -1272,16 +1231,7 @@ def fetch_databank(
             self.reftracker.add(doi["ExoMol-2020"], "line database")  # [ExoMol-2020]
 
             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 = "feather"
-                    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"
+                memory_mapping_engine = "vaex"
 
             if database in ["full", "range"]:
                 raise ValueError(
@@ -1289,7 +1239,7 @@ def fetch_databank(
                 )
 
             # Download, setup local databases, and fetch (use existing if possible)
-            if memory_mapping_engine not in ["vaex", "feather"]:
+            if memory_mapping_engine not in ["vaex", "feather", "pytables"]:
                 raise NotImplementedError(
                     f"{memory_mapping_engine} with ExoMol files. Define radis.config['MEMORY_MAPPING_ENGINE'] = 'vaex' or 'feather'"
                 )
@@ -1358,16 +1308,7 @@ def fetch_databank(
             self.reftracker.add(doi["GEISA-2020"], "line database")
 
             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"
+                memory_mapping_engine = "vaex"
 
             if database != "full":
                 raise ValueError(
@@ -1779,8 +1720,14 @@ def _check_database_params(
 
             filtered_path = [fname for fname in path]
             for fname in path:
-                if cache_file_name(fname) in path and cache_file_name(fname) != fname:
-                    filtered_path.remove(cache_file_name(fname))
+                for likely_fname_cache in [
+                    splitext(fname)[0] + ".h5",
+                    fname + ".h5",
+                    splitext(fname)[0] + ".hdf5",
+                    fname + ".hdf5",
+                ]:
+                    if likely_fname_cache in path and likely_fname_cache != fname:
+                        filtered_path.remove(likely_fname_cache)
             new_paths += filtered_path
 
             # Raise errors if no file / print which files were selected.
diff --git a/radis/levels/partfunc.py b/radis/levels/partfunc.py
index 73f270704..962beed03 100644
--- a/radis/levels/partfunc.py
+++ b/radis/levels/partfunc.py
@@ -1099,6 +1099,24 @@ class PartFuncExoMol(RovibParFuncTabulator):
     ----------
     name: str
         exomol isotope full name
+    T_range, Q_range: array
+        initial references values to be tabulated.
+
+    Examples
+    --------
+
+    One can use :py:func:`~radis.io.exomol.fetch_exomol` which has the option to
+    return a :py:class:`~radis.levels.partfunc.PartFuncExoMol` object ::
+
+        from radis.io.exomol import fetch_exomol, get_exomol_database_list
+
+        print(get_exomol_database_list("SiO", "28Si-16O"))  # 'EBJT" is one database
+        _, _, Z_exomol = fetch_exomol("SiO", "EBJT", return_local_path=True, return_partition_function=True)
+
+
+        # Get temperature at 2000 K
+        print(Z_exomol.at(2000))
+
 
     See Also
     --------
@@ -1145,6 +1163,8 @@ class PartFuncTIPS(RovibParFuncTabulator):
 
     See :ref:`online examples <label_examples_partition_functions>` for more.
 
+    .. minigallery:: radis.levels.partfunc.PartFuncTIPS
+
     References
     ----------
     Partition function are retrieved from [TIPS-2020]_ through [HAPI]_  (Hitran Python Interface) using
@@ -1328,6 +1348,7 @@ class PartFunc_Dunham(RovibParFuncCalculator):
     Examples
     --------
     Calculate partition function of CO using default spectroscopic constants::
+
         from radis.db.molecules import Molecules
         from radis.levels.partfunc import PartFunc_Dunham
 
diff --git a/radis/los/slabs.py b/radis/los/slabs.py
index cd3ec601f..c644211dd 100644
--- a/radis/los/slabs.py
+++ b/radis/los/slabs.py
@@ -218,7 +218,7 @@ def SerialSlabs(*slabs, **kwargs) -> Spectrum:
                 Iunit=unitsn["transmittance_noslit"],
                 copy=False,
             )[1]
-        except ValueError:
+        except (ValueError, KeyError):
             Tn = None
         try:
             In = sn.get(
@@ -227,7 +227,7 @@ def SerialSlabs(*slabs, **kwargs) -> Spectrum:
                 Iunit=unitsn["radiance_noslit"],
                 copy=False,
             )[1]
-        except ValueError:
+        except (ValueError, KeyError):
             In = None
         # ... get s quantities
         try:
@@ -237,7 +237,7 @@ def SerialSlabs(*slabs, **kwargs) -> Spectrum:
                 Iunit=unitsn["transmittance_noslit"],
                 copy=False,
             )[1]
-        except ValueError:
+        except (ValueError, KeyError):
             T = None
         try:
             I = s.get(
@@ -246,7 +246,7 @@ def SerialSlabs(*slabs, **kwargs) -> Spectrum:
                 Iunit=unitsn["radiance_noslit"],
                 copy=False,
             )[1]
-        except ValueError:
+        except (ValueError, KeyError):
             I = None
 
         # Solve radiative transfer equation
@@ -301,7 +301,7 @@ def SerialSlabs(*slabs, **kwargs) -> Spectrum:
             units=unitsn,
             name=name,
             references=references,
-            warnings=False,  # we already know waveranges are properly spaced, etc.
+            check_wavespace=False,  # we already know waveranges are properly spaced, etc.
         )
 
 
@@ -419,8 +419,9 @@ def same_wavespace(wl):
             return True
 
     # Work on copies
+    # TODO : this is done all the time, and slow ; we could avoid it if same_wavespace()
     if not modify_inputs:
-        slabs = [s.copy(copy_lines=copy_lines) for s in slabs]
+        slabs = [s.copy(copy_lines=copy_lines, copy_arrays=True) for s in slabs]
 
     # Get all keys
     keys = merge_lists([s.get_vars() for s in slabs])
@@ -431,6 +432,9 @@ def same_wavespace(wl):
         # first iteration. However, it also deals with cases where quantities
         # have different wavespaces (ex: radiance and radiance_noslit are not
         # defined on the same range)
+        # TODO EP 15/08/22 : this is no longer the case. ; slit & no_slit arrays
+        # now share the same wavespace. Can be removed.
+
         slabsk = [s for s in slabs if k in s.get_vars()]  # note that these are
         # references to the actual Spectrum copy
         wl = [s.get(k, wunit=waveunit, Iunit=s.units[k], copy=False)[0] for s in slabsk]
@@ -767,8 +771,8 @@ def MergeSlabs(*slabs, **kwargs) -> Spectrum:
                 printdbg("... merge: calculating abscoeff k=sum(k_i)")
             abscoeff_eq = np.sum(
                 [
-                    s.take("abscoeff").get(
-                        "abscoeff", wunit=waveunit, Iunit=units0["abscoeff"]
+                    s.take("abscoeff", copy_arrays=False).get(
+                        "abscoeff", wunit=waveunit, Iunit=units0["abscoeff"], copy=False
                     )[1]
                     for s in slabs
                 ],
@@ -783,8 +787,11 @@ def MergeSlabs(*slabs, **kwargs) -> Spectrum:
                 printdbg("... merge: calculating emisscoeff j=sum(j_i)")
             emisscoeff_eq = np.sum(
                 [
-                    s.take("emisscoeff").get(
-                        "emisscoeff", wunit=waveunit, Iunit=units0["emisscoeff"]
+                    s.take("emisscoeff", copy_arrays=False).get(
+                        "emisscoeff",
+                        wunit=waveunit,
+                        Iunit=units0["emisscoeff"],
+                        copy=False,
                     )[1]
                     for s in slabs
                 ],
diff --git a/radis/misc/arrays.py b/radis/misc/arrays.py
index 04a1cfdee..c609f771d 100644
--- a/radis/misc/arrays.py
+++ b/radis/misc/arrays.py
@@ -507,6 +507,8 @@ def numpy_add_at(LDM, k, l, m, I):
     import radis_cython_extensions as rcx
 except (ModuleNotFoundError, ValueError):
     add_at = numpy_add_at
+    """Wrapper to :py:func`radis.misc.arrays.numpy_add_at` or to the Radis
+    Cython version  (here, the numpy version was loaded)"""
     #  or use radis.misc.utils.NotInstalled() ?
 # EP: Also got (after switching back to former Cython version) :
 #     File "radis\cython\radis_cython_extensions.pyx", line 1, in init radis_cython_extensions
diff --git a/radis/misc/basics.py b/radis/misc/basics.py
index 91640fe27..e81054d7a 100644
--- a/radis/misc/basics.py
+++ b/radis/misc/basics.py
@@ -500,9 +500,10 @@ def is_number(s):
 
     try:
         float(s)
-        return True
-    except ValueError:
+    except (ValueError, TypeError):
         return False
+    else:
+        return True
 
 
 def to_str(a):
diff --git a/radis/misc/config.py b/radis/misc/config.py
index 0ee71508f..5679fec7d 100644
--- a/radis/misc/config.py
+++ b/radis/misc/config.py
@@ -126,7 +126,7 @@ def get_config(configpath=CONFIG_PATH_JSON):
                                  # energies. Default 0 (if not given)
 
 --------------------------"""
-"""str: Typical expected format of a ~/.radis entry::
+r"""str: Typical expected format of a ~/.radis entry::
 
     --------------------------
 
@@ -221,7 +221,7 @@ def get_config(configpath=CONFIG_PATH_JSON):
 }
 
 --------------------------"""
-"""str: Typical expected format of a ~/radis.json entry in "database" key::
+r"""str: Typical expected format of a ~/radis.json entry in "database" key::
 
     --------------------------
 
@@ -279,7 +279,7 @@ def get_config(configpath=CONFIG_PATH_JSON):
 
 
 def get_user_config_configformat():
-    """Read config file and returns it.
+    r"""Read config file and returns it.
 
     Config file name is harcoded: :ref:`~/.radis <label_lbl_config_file>`
     """
@@ -302,7 +302,7 @@ def get_user_config_configformat():
 
 
 def convertRadisToJSON(config_path_json, config_path_old=CONFIG_PATH_OLD):
-    """Converts the ~/.radis file into json formatted file ~/radis.json
+    r"""Converts the ~/.radis file into json formatted file ~/radis.json
 
     Example
     -------
@@ -375,7 +375,7 @@ def convertRadisToJSON(config_path_json, config_path_old=CONFIG_PATH_OLD):
 
 
 def init_radis_json(config_path_json, config_path_old=CONFIG_PATH_OLD):
-    """Checks whether ``config_path_json`` (usually `~/radis.json`) exists.
+    r"""Checks whether ``config_path_json`` (usually `~/radis.json`) exists.
 
     If not then we use
     :func:`~radis.misc.config.convertRadisToJSON`
@@ -445,7 +445,7 @@ def init_radis_json(config_path_json, config_path_old=CONFIG_PATH_OLD):
 
 
 def get_user_config(configpath=CONFIG_PATH_JSON):
-    """Read config file and returns it."""
+    r"""Read config file and returns it."""
 
     # First checking `~radis.json` exist or not, if not create it
     init_radis_json(configpath)
@@ -578,7 +578,7 @@ def getDatabankEntries_configformat(dbname, get_extra_keys=[]):
 
 
 def getDatabankList_configformat():
-    """Get all databanks available in :ref:`~/.radis <label_lbl_config_file>`."""
+    r"""Get all databanks available in :ref:`~/.radis <label_lbl_config_file>`."""
     warnings.warn(DeprecationWarning("config format changed to JSON in radis 0.9.29"))
 
     config = get_user_config_configformat()
@@ -601,7 +601,7 @@ def getDatabankList_configformat():
 
 
 def addDatabankEntries_configformat(dbname, dict_entries, verbose=True):
-    """Add database dbname with entries from dict_entries.
+    r"""Add database dbname with entries from dict_entries.
 
     If database already exists in :ref:`~/.radis <label_lbl_config_file>`, raises an error
 
@@ -688,7 +688,7 @@ def addDatabankEntries_configformat(dbname, dict_entries, verbose=True):
 
 
 def diffDatabankEntries(dict_entries1, dict_entries2, verbose=True):
-    """Compare two Databank entries under dict format (i.e: output of
+    r"""Compare two Databank entries under dict format (i.e: output of
     getDatabankEntries)
 
     Returns None if no differences are found, or the first different key
@@ -743,7 +743,7 @@ def diffDatabankEntries(dict_entries1, dict_entries2, verbose=True):
 
 
 def printDatabankEntries_configformat(dbname, crop=200):
-    """Print databank info.
+    r"""Print databank info.
 
     Parameters
     ----------
@@ -916,13 +916,12 @@ def getDatabankList(configpath=CONFIG_PATH_JSON):
 
 
 def addDatabankEntries(dbname, dict_entries, verbose=True, configpath=CONFIG_PATH_JSON):
-    """Add database dbname with entries from dict_entries.
+    r"""Add database dbname with entries from dict_entries.
 
     If database already exists in :ref:`~/radis.json <label_lbl_config_file>`, raises an error
 
     Examples
     --------
-
     ::
 
         addDatabankEntries("HITEMP2010-CO2",
@@ -1010,7 +1009,7 @@ def addDatabankEntries(dbname, dict_entries, verbose=True, configpath=CONFIG_PAT
 
 
 def printDatabankEntries(dbname, crop=200, configpath=CONFIG_PATH_JSON):
-    """Print databank info.
+    r"""Print databank info.
 
     Parameters
     ----------
diff --git a/radis/misc/printer.py b/radis/misc/printer.py
index e3a5903ee..18c980b03 100644
--- a/radis/misc/printer.py
+++ b/radis/misc/printer.py
@@ -91,3 +91,148 @@ def get_print_full(x):
         pd.reset_option("display.max_rows")
 
     return string[:-1]
+
+
+# %% UTF8 Upper script/ lower script
+# https://stackoverflow.com/a/58612677/5622825
+
+superscript_map = {
+    "0": "⁰",
+    "1": "¹",
+    "2": "²",
+    "3": "³",
+    "4": "⁴",
+    "5": "⁵",
+    "6": "⁶",
+    "7": "⁷",
+    "8": "⁸",
+    "9": "⁹",
+    "a": "ᵃ",
+    "b": "ᵇ",
+    "c": "ᶜ",
+    "d": "ᵈ",
+    "e": "ᵉ",
+    "f": "ᶠ",
+    "g": "ᵍ",
+    "h": "ʰ",
+    "i": "ᶦ",
+    "j": "ʲ",
+    "k": "ᵏ",
+    "l": "ˡ",
+    "m": "ᵐ",
+    "n": "ⁿ",
+    "o": "ᵒ",
+    "p": "ᵖ",
+    "q": "۹",
+    "r": "ʳ",
+    "s": "ˢ",
+    "t": "ᵗ",
+    "u": "ᵘ",
+    "v": "ᵛ",
+    "w": "ʷ",
+    "x": "ˣ",
+    "y": "ʸ",
+    "z": "ᶻ",
+    "A": "ᴬ",
+    "B": "ᴮ",
+    "C": "ᶜ",
+    "D": "ᴰ",
+    "E": "ᴱ",
+    "F": "ᶠ",
+    "G": "ᴳ",
+    "H": "ᴴ",
+    "I": "ᴵ",
+    "J": "ᴶ",
+    "K": "ᴷ",
+    "L": "ᴸ",
+    "M": "ᴹ",
+    "N": "ᴺ",
+    "O": "ᴼ",
+    "P": "ᴾ",
+    "Q": "Q",
+    "R": "ᴿ",
+    "S": "ˢ",
+    "T": "ᵀ",
+    "U": "ᵁ",
+    "V": "ⱽ",
+    "W": "ᵂ",
+    "X": "ˣ",
+    "Y": "ʸ",
+    "Z": "ᶻ",
+    "+": "⁺",
+    "-": "⁻",
+    "=": "⁼",
+    "(": "⁽",
+    ")": "⁾",
+}
+
+
+subscript_map = {
+    "0": "₀",
+    "1": "₁",
+    "2": "₂",
+    "3": "₃",
+    "4": "₄",
+    "5": "₅",
+    "6": "₆",
+    "7": "₇",
+    "8": "₈",
+    "9": "₉",
+    "a": "ₐ",
+    "b": "♭",
+    "c": "꜀",
+    "d": "ᑯ",
+    "e": "ₑ",
+    "f": "բ",
+    "g": "₉",
+    "h": "ₕ",
+    "i": "ᵢ",
+    "j": "ⱼ",
+    "k": "ₖ",
+    "l": "ₗ",
+    "m": "ₘ",
+    "n": "ₙ",
+    "o": "ₒ",
+    "p": "ₚ",
+    "q": "૧",
+    "r": "ᵣ",
+    "s": "ₛ",
+    "t": "ₜ",
+    "u": "ᵤ",
+    "v": "ᵥ",
+    "w": "w",
+    "x": "ₓ",
+    "y": "ᵧ",
+    "z": "₂",
+    "A": "ₐ",
+    "B": "₈",
+    "C": "C",
+    "D": "D",
+    "E": "ₑ",
+    "F": "բ",
+    "G": "G",
+    "H": "ₕ",
+    "I": "ᵢ",
+    "J": "ⱼ",
+    "K": "ₖ",
+    "L": "ₗ",
+    "M": "ₘ",
+    "N": "ₙ",
+    "O": "ₒ",
+    "P": "ₚ",
+    "Q": "Q",
+    "R": "ᵣ",
+    "S": "ₛ",
+    "T": "ₜ",
+    "U": "ᵤ",
+    "V": "ᵥ",
+    "W": "w",
+    "X": "ₓ",
+    "Y": "ᵧ",
+    "Z": "Z",
+    "+": "₊",
+    "-": "₋",
+    "=": "₌",
+    "(": "₍",
+    ")": "₎",
+}
diff --git a/radis/misc/profiler.py b/radis/misc/profiler.py
index c7a2a540d..9182bee41 100644
--- a/radis/misc/profiler.py
+++ b/radis/misc/profiler.py
@@ -18,7 +18,7 @@
 """
 
 from collections import OrderedDict
-from time import time
+from time import perf_counter
 
 from radis.misc.printer import printg
 
@@ -62,7 +62,7 @@ def add_entry(self, dictionary, key, verbose, count):
     def start(self, key, verbose_level, optional=""):
         if __debug__:
             self.initial[key] = {
-                "start_time": time(),
+                "start_time": perf_counter(),
                 "verbose_level": verbose_level,
             }
 
@@ -109,7 +109,7 @@ def add_time(self, dictionary, key, verbose, count, time_calculated):
     def stop(self, key, details):
         if __debug__:
             items = self.initial.pop(key)
-            time_calculated = time() - items["start_time"]
+            time_calculated = perf_counter() - items["start_time"]
 
             if items["verbose_level"] == 1:
                 # Profiler ends; Deserializing to Dictionary format
diff --git a/radis/misc/signal.py b/radis/misc/signal.py
index 9f09db311..e3dec544c 100644
--- a/radis/misc/signal.py
+++ b/radis/misc/signal.py
@@ -14,7 +14,10 @@
 
 """
 
+import math
+
 import numpy as np
+import scipy.linalg as LA
 from numpy import abs, isnan, linspace, nan, trapz, zeros_like
 from scipy.interpolate import splev, splrep
 from scipy.linalg import solveh_banded
@@ -35,7 +38,7 @@ def resample(
     xspace_new,
     k=1,
     ext="error",
-    energy_threshold=5e-3,
+    energy_threshold="default",
     print_conservation=True,
 ):
     """Resample (xspace, vector) on a new space (xspace_new) of evenly
@@ -66,10 +69,11 @@ def resample(
         defined by xspace. If 'error', raise a ValueError. If 'extrapolate', well,
         extrapolate. If '0' or 0, then fill with 0. If 1, fills with 1.
         Default 'error'.
-    energy_threshold: float or ``None``
+    energy_threshold: float or ``None`` or ``'default'
         if energy conservation (integrals on the intersecting range) is above
-        this threshold, raise an error. If ``None``, dont check for energy conservation
-        Default 5e-3 (0.5%)
+        this threshold, raise an error. If ``None``, dont check for energy conservation.
+        If ``'default'``, look up the value in :py:attr:`radis.config` ["RESAMPLING_TOLERANCE_THRESHOLD"]
+        Default ``'default'``
     print_conservation: boolean
         if True, prints energy conservation
 
@@ -101,6 +105,10 @@ def resample(
             "vector and xspace should have the same length. "
             + "Got {0}, {1}".format(len(vector), len(xspace))
         )
+    if energy_threshold == "default":
+        import radis
+
+        energy_threshold = radis.config["RESAMPLING_TOLERANCE_THRESHOLD"]
 
     # Check reversed (interpolation requires objects are sorted)
     if is_sorted(xspace):
@@ -213,11 +221,11 @@ def resample(
             plt.legend()
             raise ValueError(
                 "Error in resampling: "
-                + "energy conservation ({0:.5g}%) below tolerance level ({1:.5g}%)".format(
+                + "difference in areas (i.e. energy conservation ) ({0:.5g}%) is above the tolerance level ({1:.5g}%)".format(
                     (1 - energy_ratio) * 100, energy_threshold * 100
                 )
-                + ". Check graph 101. "
-                + "Increasing energy_threshold is possible but not recommended"
+                + ". Check graph 101. If resampling a low resolution spectrum on a high resolution spectrum, try the other way around. "
+                + "Increasing the tolerance threshold is possible but may result in accuracy errors. To increase the tolerance, set `resample(..., energy_threshold=...)`, or change the default global value in `radis.config['RESAMPLING_TOLERANCE_THRESHOLD']`"
             )
     if print_conservation:
         print("Resampling - Energy conservation: {0:.5g}%".format(energy_ratio * 100))
@@ -388,6 +396,73 @@ def als_baseline(
     return z
 
 
+def baseline(y, deg=None, max_it=None, tol=None):
+    """
+    Computes the baseline of a given data.
+
+    Iteratively performs a polynomial fitting in the data to detect its
+    baseline. At every iteration, the fitting weights on the regions with
+    peaks are reduced to identify the baseline only.
+
+    Parameters
+    ----------
+    y : ndarray
+        Data to detect the baseline.
+    deg : int (default: 3)
+        Degree of the polynomial that will estimate the data baseline. A low
+        degree may fail to detect all the baseline present, while a high
+        degree may make the data too oscillatory, especially at the edges.
+    max_it : int (default: 100)
+        Maximum number of iterations to perform.
+    tol : float (default: 1e-3)
+        Tolerance to use when comparing the difference between the current
+        fit coefficients and the ones from the last iteration. The iteration
+        procedure will stop when the difference between them is lower than
+        *tol*.
+
+    Returns
+    -------
+    ndarray
+        Array with the baseline amplitude for every original point in *y*
+
+    References
+    -------
+    This function has been taken from the PeakUtils package.
+    Their source code can be found here: https://bitbucket.org/lucashnegri/peakutils
+    More info: https://peakutils.readthedocs.io/en/latest/
+    """
+    # for not repeating ourselves in `envelope`
+    if deg is None:
+        deg = 3
+    if max_it is None:
+        max_it = 100
+    if tol is None:
+        tol = 1e-3
+
+    order = deg + 1
+    coeffs = np.ones(order)
+
+    # try to avoid numerical issues
+    cond = math.pow(abs(y).max(), 1.0 / order)
+    x = np.linspace(0.0, cond, y.size)
+    base = y.copy()
+
+    vander = np.vander(x, order)
+    vander_pinv = LA.pinv(vander)
+
+    for _ in range(max_it):
+        coeffs_new = np.dot(vander_pinv, y)
+
+        if LA.norm(coeffs_new - coeffs) / LA.norm(coeffs) < tol:
+            break
+
+        coeffs = coeffs_new
+        base = np.dot(vander, coeffs)
+        y = np.minimum(y, base)
+
+    return base
+
+
 class WhittakerSmoother(object):
     """
 
diff --git a/radis/misc/utils.py b/radis/misc/utils.py
index fd76262a4..7cbf2eeef 100644
--- a/radis/misc/utils.py
+++ b/radis/misc/utils.py
@@ -181,6 +181,8 @@ def get_files_from_regex(path):
     """Returns a list of absolute paths of all the files whose names match the
     input regular expression."""
     directory_name = dirname(path)
+    if directory_name == "":
+        directory_name = "."
     regex = basename(path)
 
     file_names = []
diff --git a/radis/misc/warning.py b/radis/misc/warning.py
index a3818acc4..bce18936c 100644
--- a/radis/misc/warning.py
+++ b/radis/misc/warning.py
@@ -75,6 +75,10 @@ class EmptyDatabaseError(ValueError):
     pass
 
 
+class InconsistentDatabaseError(ValueError):
+    pass
+
+
 # Warnings
 
 
diff --git a/radis/phys/constants.py b/radis/phys/constants.py
index 06cb9f999..5aa231879 100644
--- a/radis/phys/constants.py
+++ b/radis/phys/constants.py
@@ -11,33 +11,52 @@
 
 # %% SI units
 
-eV = 1.60217657e-19  # J
-"""float: electron volt (J)"""
+eV = 1.602176634e-19  # J
+"""float: electron volt (J)
 
-h = 6.62606957e-34
-"""float: Planck constant (m2.kg.s-1 = J.s)"""
+https://physics.nist.gov/cgi-bin/cuu/Value?e
+"""
+
+h = 6.62607015e-34
+"""float: Planck constant (m2.kg.s-1 = J.s)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?h|search_for=planck"""
 
-k_b = 1.3806488e-23
-"""float: Boltzmann constant (m2.kg.s-2.K-1)"""
+k_b = 1.3806490e-23
+"""float: Boltzmann constant (m2.kg.s-2.K-1)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?k|search_for=boltzmann
+"""
 
 c = 2.99792458e8
-"""float: light velocity (m/s)"""
+"""float: light velocity (m/s)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?c
+"""
 
 a_0 = 5.29177e-11
 """float: First Bohr radius (m)"""
 
-m_e = 9.10938291e-31
-"""float: Electron mass (kg)"""
+m_e = 9.1093837015e-31
+"""float: Electron mass (kg)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?me|search_for=electron+mass"""
+
+Ry = 13.605693122994
+"""float: Rydberg unit of energy (eV)
 
-Ry = 13.60569253
-"""float: Rydberg unit of energy (eV)"""
+https://physics.nist.gov/cgi-bin/cuu/Value?rydhcev|search_for=rydberg"""
 
-Av = 6.02214129e23
-"""float: Avogadro constant (molecules / mol)"""
+Av = 6.02214076e23
+"""float: Avogadro constant (molecules / mol)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?na|search_for=avogadro"""
 Na = Av  # just an alias
 
-eps_0 = 8.854187817e-12
-"""float: Vacuum permittivity (Farads / m)"""
+eps_0 = 8.8541878128e-12
+"""float: Vacuum permittivity (Farads / m)
+
+https://physics.nist.gov/cgi-bin/cuu/Value?ep0|search_for=vacuum+permittivity"""
 
 hc_k = h * c / k_b * 100  #
 """float:  cm to K conversion (~ 1.44 K/cm), used in Boltman factors"""
diff --git a/radis/spectrum/compare.py b/radis/spectrum/compare.py
index 490f22c4f..3432472ac 100644
--- a/radis/spectrum/compare.py
+++ b/radis/spectrum/compare.py
@@ -813,13 +813,15 @@ def plot_diff(
         ax0.set_yscale("log")
         ax0.set_ybound(ylim0)
 
+    .. minigallery:: radis.plot_diff
+
     See Also
     --------
-    :func:`~radis.spectrum.compare.get_diff`,
-    :func:`~radis.spectrum.compare.get_ratio`,
-    :func:`~radis.spectrum.compare.get_distance`,
-    :func:`~radis.spectrum.compare.get_residual`,
-    :meth:`~radis.spectrum.spectrum.compare_with`
+    :py:func:`~radis.spectrum.compare.get_diff`,
+    :py:func:`~radis.spectrum.compare.get_ratio`,
+    :py:func:`~radis.spectrum.compare.get_distance`,
+    :py:func:`~radis.spectrum.compare.get_residual`,
+    :py:meth:`~radis.spectrum.spectrum.Spectrum.compare_with`
     """
     import matplotlib.pyplot as plt
     from matplotlib import gridspec
diff --git a/radis/spectrum/equations.py b/radis/spectrum/equations.py
index 7f2a8f238..46fb1d84a 100644
--- a/radis/spectrum/equations.py
+++ b/radis/spectrum/equations.py
@@ -19,7 +19,7 @@
 
 
 def calc_radiance(wavenumber, emissivity, Tgas, unit="mW/sr/cm2/nm"):
-    """Derive radiance (:math:`mW/cm^2/sr/nm`) from the emissivity.
+    r"""Derive radiance (:math:`mW/cm^2/sr/nm`) from the emissivity.
 
     .. math::
         I(\lambda) = \epsilon(\lambda) \cdot L_0(\lambda, T)
@@ -45,7 +45,7 @@ def calc_radiance(wavenumber, emissivity, Tgas, unit="mW/sr/cm2/nm"):
 
 
 def abscoeff2xsection(abscoeff_cm1, Tgas_K, pressure_Pa, mole_fraction):
-    """Convert Absorption coefficient (cm-1) to Cross-section  (cm2)
+    r"""Convert Absorption coefficient (cm-1) to Cross-section  (cm2)
 
     Parameters
     ----------
@@ -60,3 +60,9 @@ def abscoeff2xsection(abscoeff_cm1, Tgas_K, pressure_Pa, mole_fraction):
     """
 
     return abscoeff_cm1 * (k_b * Tgas_K / mole_fraction / pressure_Pa) * 1e6  # cm2
+
+
+if __name__ == "__main__":
+    from radis.test.spectrum.test_equations import test_equations
+
+    test_equations()
diff --git a/radis/spectrum/models.py b/radis/spectrum/models.py
index 232159efd..f4aff0a98 100644
--- a/radis/spectrum/models.py
+++ b/radis/spectrum/models.py
@@ -79,14 +79,14 @@ def calculated_spectrum(
 
     Examples
     --------
-
     ::
 
         # w, I are numpy arrays for wavelength and radiance
         from radis import calculated_spectrum
         s = calculated_spectrum(w, I, wunit='nm', Iunit='W/cm2/sr/nm')     # creates 'radiance_noslit'
 
-
+    .. minigallery:: radis.calculated_spectrum
+        :add-heading:
 
     See Also
     --------
@@ -149,6 +149,9 @@ def transmittance_spectrum(
         from radis import transmittance_spectrum
         s2 = transmittance_spectrum(w, T, wunit='nm')                       # creates 'transmittance_noslit'
 
+    .. minigallery:: radis.transmittance_spectrum
+        :add-heading:
+
 
     See Also
     --------
@@ -212,6 +215,8 @@ def experimental_spectrum(
         s = experimental_spectrum(w, I, Iunit='mW/cm2/sr/nm')             # creates 'radiance'
         s.plot()
 
+    .. minigallery:: radis.experimental_spectrum
+        :add-heading:
 
     See Also
     --------
diff --git a/radis/spectrum/operations.py b/radis/spectrum/operations.py
index c91b98905..822e6c88e 100644
--- a/radis/spectrum/operations.py
+++ b/radis/spectrum/operations.py
@@ -791,7 +791,7 @@ def add_spectra(s1, s2, var=None, force=False):
 
     name = s1.get_name() + "+" + s2.get_name()
 
-    sub = Spectrum.from_array(w1, I1 + I2, var, wunit=wunit1, unit=Iunit1, name=name)
+    sub = Spectrum.from_array(w1, I1 + I2, var, wunit=wunit1, Iunit=Iunit1, name=name)
     #    warn("Conditions of the left spectrum were copied in the substraction.", Warning)
     return sub
 
@@ -855,7 +855,7 @@ def substract_spectra(s1, s2, var=None):
 
     name = s1.get_name() + "-" + s2.get_name()
 
-    sub = Spectrum.from_array(w1, I1 - I2, var, wunit=wunit1, unit=Iunit1, name=name)
+    sub = Spectrum.from_array(w1, I1 - I2, var, wunit=wunit1, Iunit=Iunit1, name=name)
     #    warn("Conditions of the left spectrum were copied in the substraction.", Warning)
     return sub
 
@@ -1089,11 +1089,11 @@ def get_baseline(
     w1, I1 = s.get(var=var, wunit=wunit, Iunit=Iunit)
 
     if algorithm == "polynomial":
-        import peakutils
+        from radis.misc.signal import baseline
 
         polyargs = {"deg": 1, "max_it": 500}
         polyargs.update(kwargs)
-        baseline = peakutils.baseline(I1, **polyargs)
+        baseline = baseline(I1, **polyargs)
     elif algorithm == "als":
         from radis.misc.signal import als_baseline
 
@@ -1102,7 +1102,7 @@ def get_baseline(
         baseline = als_baseline(I1, **alsargs)
 
     baselineSpectrum = Spectrum.from_array(
-        w1, baseline, var, wunit=wunit, unit=Iunit, name=s.get_name() + "_baseline"
+        w1, baseline, var, wunit=wunit, Iunit=Iunit, name=s.get_name() + "_baseline"
     )
     return baselineSpectrum
 
diff --git a/radis/spectrum/rescale.py b/radis/spectrum/rescale.py
index 6d5408033..d37036c57 100644
--- a/radis/spectrum/rescale.py
+++ b/radis/spectrum/rescale.py
@@ -100,7 +100,9 @@ def _build_update_graph(
     Returns
     -------
     derivation: dict
-        {spectral_quantity: [list of combinations of spectral quantities needed to calculate it]}
+        Format::
+
+            {spectral_quantity: [list of combinations of spectral quantities needed to calculate it]}
 
     Examples
     --------
@@ -149,6 +151,7 @@ def _build_update_graph(
         _build_update_graph(s)
 
     Outputs ::
+
         {'transmittance_noslit': [['absorbance'],
           ['abscoeff'],
           ['absorbance'],
@@ -228,7 +231,7 @@ def _build_update_graph(
         and "norm_by" in spec.conditions
     )
 
-    all_keys = [
+    all_usual_keys = [
         "abscoeff",
         "absorbance",
         "emisscoeff",
@@ -239,7 +242,10 @@ def _build_update_graph(
         "transmittance_noslit",
         "xsection",
     ]
-    assert all_in(all_keys, CONVOLUTED_QUANTITIES + NON_CONVOLUTED_QUANTITIES)
+    all_usual_keys_nonconvoluted = [
+        k for k in all_usual_keys if k in NON_CONVOLUTED_QUANTITIES
+    ]
+    assert all_in(all_usual_keys, CONVOLUTED_QUANTITIES + NON_CONVOLUTED_QUANTITIES)
 
     # Build edges of relationships
     derivation = {  # {keys, [list of keys]}
@@ -253,7 +259,6 @@ def derives_from(what, *from_keys):
 
         Examples
         --------
-
         Radiance can be infered from emisscoeff if optically thin::
 
             derives_from('radiance_noslit', 'emisscoeff')
@@ -284,7 +289,7 @@ def derives_from(what, *from_keys):
     # no change: case where we are rescaling without changing path length nor
     # mole fractions, i.e, all initial quantities can be recomputed from themselves...
     if no_change:
-        for k in all_keys:
+        for k in all_usual_keys:
             derives_from(k, [k])
 
     # Deal with case where we know path_length:
@@ -308,14 +313,15 @@ def derives_from(what, *from_keys):
         derives_from("radiance", ["radiance_noslit"])
         derives_from("transmittance", ["transmittance_noslit"])
         derives_from("emissivity", ["emissivity_noslit"])
+
+    # At equilibrium, any usual spectral array can be recomputed from any other
     if equilibrium == True:
         if __debug__:
             printdbg("... build_graph: equilibrium > all keys derive from one")
-        # Anything can be recomputed from anything
-        for key in all_keys:
+        for key in all_usual_keys:
             if key in NON_CONVOLUTED_QUANTITIES and key != "xsection":
                 # except from xsection, where we still need P & T
-                all_but_k = [[k] for k in all_keys if k != key]
+                all_but_k = [[k] for k in all_usual_keys_nonconvoluted if k != key]
                 derives_from(key, *all_but_k)
 
     # ------------------------------------------------------------
@@ -354,9 +360,6 @@ def get_redundant(spec):
     for key in ordered_keys[::-1]:  # roots
         if key in derivation_graph:
             for from_keys in derivation_graph[key]:
-                #                if [key] == from_keys:
-                #                    # that you can be recomputed from yourself doesnt make you redundant
-                #                    continue
                 if all([activated[k] and not redundant[k] for k in from_keys]):
                     redundant[key] = True  # declare redundant
                     continue
@@ -467,17 +470,17 @@ def get_recompute(spec, wanted, no_change=False, true_path_length=None):
         spec, path_length=true_path_length, no_change=no_change
     )
 
-    #    activated = dict().fromkeys(ordered_keys, False)
-    # Store two dictionaries, that characterize, at a given instant, all quantities
-    # that we had to recompute, and all quantities that can be recomputed from these
+    # Create dictionary which contains all quantities
+    # that we have to recompute
     recompute = dict().fromkeys(ordered_keys, False)
     for k in spec.get_vars():  # start from all quantities we have
         recompute[k] = True
     for k in wanted:  # add all quantities we want
         recompute[k] = True
-    #    reachable = dict().fromkeys(ordered_keys, False)
-    #    for k in spec.get_vars():   # start from all quantities we have
-    #        reachable[k] = True
+    # Add non-slit quantities if the convoluted array is wanted
+    for k in CONVOLUTED_QUANTITIES:
+        if k in wanted:
+            recompute[k + "_noslit"] = True
 
     def parse_tree(recompute):
         for key in ordered_keys:
@@ -498,20 +501,6 @@ def parse_tree(recompute):
                                 key, [k for k in from_keys if recompute[k]]
                             )
                         )
-                    #                    # cant recompute this quantity. Let's force recomputation
-                    #                    def get_best_path():
-                    #                        new_recompute_set = recompute.copy()
-                    #                        for k in ordered_keys:
-                    #                            # let's add a new quantity to recompute
-                    #                            if new_recompute_set[k]:
-                    #                                # we already have this quantity
-                    #                                continue
-                    #                            elif not any([k in from_keys for from_keys in derivation_graph[key]]):
-                    #                                # this quantity (k) doesnt participate in calculating from_keys
-                    #                                continue
-                    #                            else:
-                    #                                # let's calculate it
-                    #
                     # cant recompute this quantity. Let's force recomputation
                     # of a given path. We'll arbitrary use the one will the fewer
                     # amount of not already recomputed quantities
@@ -825,15 +814,26 @@ def rescale_abscoeff(
 # ... all, if equilibrium and abscoeff was rescaled
 
 
-def _recompute_all_at_equilibrium(
-    spec, rescaled, wavenumber, Tgas, new_path_length_cm, true_path_length, units
+def _recompute_from_abscoeff_at_equilibrium(
+    spec,
+    rescaled,
+    wavenumber,
+    Tgas,
+    new_path_length_cm,
+    true_path_length,
+    units,
+    recompute,
 ):
     """
 
     Parameters
     ----------
     rescaled: dict
-        abscoeff must be rescaled already
+        abscoeff must be rescaled
+
+    Other Parameters
+    ----------------
+    recompute: list of arrays that needs to be recomputed.
     """
 
     def get_unit_radiance():
@@ -850,43 +850,77 @@ def get_unit_emisscoeff(unit_radiance):
         if "/cm2" in unit_radiance:
             return unit_radiance.replace("/cm2", "/cm3")
         else:
-            return unit_radiance + "/cm"  # will be simplified by Pint afterwards
+            return (
+                unit_radiance + "/cm"
+            )  # will be simplified by Pint/Astropy.units afterwards
 
     assert true_path_length
 
+    # ----------------------------------------------------------------------
+
     abscoeff = rescaled["abscoeff"]
-    path_length_cm = new_path_length_cm
+    # Store:
+    units["abscoeff"] = "cm-1"
 
-    absorbance = abscoeff * path_length_cm
+    if (
+        "absorbance" in recompute
+        or "transmittance_noslit" in recompute
+        or "emissivity_noslit" in recompute
+        or "radiance_noslit" in recompute
+        or "emisscoeff" in recompute
+    ):
+        # Calculate absorbance
+        absorbance = abscoeff * new_path_length_cm
+        # Store:
+        rescaled["absorbance"] = absorbance
+        units["absorbance"] = ""
 
-    # Generate output quantities
-    Iunit_radiance = get_unit_radiance()
-    transmittance_noslit = exp(-absorbance)
-    emissivity_noslit = 1 - transmittance_noslit
-    radiance_noslit = calc_radiance(
-        wavenumber, emissivity_noslit, Tgas, unit=Iunit_radiance
-    )
-    b = transmittance_noslit == 1  # optically thin mask
-    emisscoeff = np.empty_like(abscoeff)
-    emisscoeff[b] = radiance_noslit[b] / path_length_cm  # recalculate (opt thin)
-    emisscoeff[~b] = (
-        radiance_noslit[~b] / (1 - transmittance_noslit[~b]) * abscoeff[~b]
-    )  # recalculate (non opt thin)
+    if (
+        "transmittance_noslit" in recompute
+        or "emissivity_noslit" in recompute
+        or "radiance_noslit" in recompute
+        or "emisscoeff" in recompute
+    ):
+        # Calculate transmittance
+        transmittance_noslit = exp(-absorbance)
+        # Store:
+        rescaled["transmittance_noslit"] = transmittance_noslit
+        units["transmittance_noslit"] = ""
 
-    # ----------------------------------------------------------------------
+    if (
+        "emissivity_noslit" in recompute
+        or "radiance_noslit" in recompute
+        or "emisscoeff" in recompute
+    ):
+        # Calculate emissivity
+        emissivity_noslit = 1 - transmittance_noslit
+        # Store:
+        rescaled["emissivity_noslit"] = emissivity_noslit
+        units["emissivity_noslit"] = ""
 
-    rescaled["absorbance"] = absorbance
-    rescaled["transmittance_noslit"] = transmittance_noslit
-    rescaled["emissivity_noslit"] = emissivity_noslit
-    rescaled["radiance_noslit"] = radiance_noslit
-    rescaled["emisscoeff"] = emisscoeff
+    if "radiance_noslit" in recompute or "emisscoeff" in recompute:
+        # Calculate radiance
+        Iunit_radiance = get_unit_radiance()
+        radiance_noslit = calc_radiance(
+            wavenumber, emissivity_noslit, Tgas, unit=Iunit_radiance
+        )
+        # Store:
+        rescaled["radiance_noslit"] = radiance_noslit
+        units["radiance_noslit"] = Iunit_radiance
 
-    units["abscoeff"] = "cm-1"
-    units["absorbance"] = ""
-    units["transmittance_noslit"] = ""
-    units["emissivity_noslit"] = ""
-    units["radiance_noslit"] = Iunit_radiance
-    units["emisscoeff"] = get_unit_emisscoeff(units["radiance_noslit"])
+    if "emisscoeff" in recompute:
+        # Calculate emission coefficient
+        b = transmittance_noslit == 1  # optically thin mask
+        emisscoeff = np.empty_like(abscoeff)
+        emisscoeff[b] = (
+            radiance_noslit[b] / new_path_length_cm
+        )  # recalculate (opt thin)
+        emisscoeff[~b] = (
+            radiance_noslit[~b] / (1 - transmittance_noslit[~b]) * abscoeff[~b]
+        )  # recalculate (non opt thin)
+        # Store:
+        rescaled["emisscoeff"] = emisscoeff
+        units["emisscoeff"] = get_unit_emisscoeff(units["radiance_noslit"])
 
     return rescaled, units
 
@@ -1936,7 +1970,7 @@ def _recalculate(
 
     # list of quantities that are needed to recompute what we want
     # ... (we're just analysing how to compute them here, the actual calculation
-    # ... will be done laters)
+    # ... will be done later)
     try:
         recompute = get_recompute(
             spec, wanted, no_change, true_path_length=true_path_length
@@ -1972,6 +2006,10 @@ def _recalculate(
     # If no_change, just set everyone as rescaled already
     if no_change:
         for k in initial:
+            try:
+                spec.units[k]
+            except KeyError:
+                raise KeyError(f"Units for {k} must be defined")
             rescaled[k] = spec.get(k, wunit=spec.get_waveunit(), Iunit=spec.units[k])[
                 1
             ]  # note: creates a copy
@@ -2009,7 +2047,7 @@ def _recalculate(
             )
         wavenumber = spec.get_wavenumber()
         Tgas = spec.conditions["Tgas"]
-        rescaled, rescaled_units = _recompute_all_at_equilibrium(
+        rescaled, rescaled_units = _recompute_from_abscoeff_at_equilibrium(
             spec,
             rescaled,
             wavenumber,
@@ -2017,6 +2055,7 @@ def _recalculate(
             new_path_length_cm,
             true_path_length,
             rescaled_units,
+            recompute,
         )
         apply_slit = "radiance" in recompute or "transmittance" in recompute
 
@@ -2143,10 +2182,13 @@ def _recalculate(
                 del spec._q[k]
                 del spec.units[k]
 
-    # Save (only) the ones that we want, unless we want everything ('greedy')
-    for q in rescaled:
-        if q in wanted:  # or greedy:
+    # Save (only) the ones that we want
+    for q in wanted:
+        if q in rescaled:
             spec._q[q] = rescaled[q]
+            # # also add non-convoluted array until we apply slit
+        elif apply_slit and q in CONVOLUTED_QUANTITIES and q + "_noslit" in rescaled:
+            spec._q[q + "_noslit"] = rescaled[q + "_noslit"]
 
     # Update units
     spec.units.update(rescaled_units)
@@ -2192,6 +2234,11 @@ def _recalculate(
                 verbose=verbose,
             )
 
+        # Remove unwanted non-convoluted arrays
+        for q in rescaled:
+            if q in CONVOLUTED_QUANTITIES and q + "_noslit" not in wanted:
+                del spec._q[q + "_noslit"]
+
     # Final checks
 
     # ... "everyone is here": check we didnt miss anyone
@@ -2475,3 +2522,33 @@ def rescale_mole_fraction(
     from radis.test.spectrum.test_rescale import _run_all_tests
 
     print(("Test rescale.py: ", _run_all_tests(verbose=True)))
+
+    def test_recompute_transmittance():
+
+        import radis
+
+        s = radis.test_spectrum()
+        s.update("all")
+        assert set(s.get_vars()) == set(
+            [
+                "abscoeff",
+                "radiance_noslit",
+                "absorbance",
+                "transmittance_noslit",
+                "emissivity_noslit",
+                "emisscoeff",
+                "xsection",
+            ]
+        )
+
+        for k in list(s._q.keys()):
+            if k == "wavespace":
+                pass
+            elif k == "abscoeff":
+                pass
+            else:
+                del s._q[k]
+
+        s.get("transmittance_noslit")
+        print(s._q.keys())
+        assert set(s.get_vars()) == set(("abscoeff", "transmittance_noslit"))
diff --git a/radis/spectrum/spectrum.py b/radis/spectrum/spectrum.py
index be46c8db6..8e5bddc4f 100644
--- a/radis/spectrum/spectrum.py
+++ b/radis/spectrum/spectrum.py
@@ -501,7 +501,22 @@ def __init__(
         any metadata you need to store with the Spectrum object.
         """
         self.populations = populations
+        """ populations of rovibrational levels used to compute partition
+        functions of the spectrum
+
+        See also :py:meth:`~radis.spectrum.spectrum.Spectrum.get_populations`,
+        :py:meth:`~radis.spectrum.spectrum.Spectrum.plot_populations`
+
+        .. minigallery:: radis.spectrum.spectrum.Spectrum.populations
+        """
         self.lines = lines
+        """ informations on emitting or absorbing lines that contribute to the spectrum.
+
+        See also :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`
+
+        .. minigallery:: radis.spectrum.spectrum.Spectrum.lines
+
+        """
         self.units = units
         """ dict: units for spectral quantities.
         """
@@ -540,7 +555,7 @@ def from_array(
         quantity: str
             spectral quantity name
         wunit: ``'nm'``, ``'cm-1'``, ``'nm_vac'``
-            unit of waverange:         wavelength in air (``'nm'``), wavenumber
+            unit of waverange:         wavelength in air (``'nm'`` or ``'nm_air'``), wavenumber
             (``'cm-1'``), or wavelength in vacuum (``'nm_vac'``).
             If ``None``, then ``w`` must be a dimensionned array.
         Iunit: str
@@ -654,10 +669,7 @@ class directly. Ex::
                     "``w`` must be a dimensionned array, or ``wunit=`` must be given."
                 )
         else:
-            if wunit is not None:
-                raise ValueError(
-                    f"``w`` is a dimensionned array (in {wunit.unit.to_string()}), therefore ``wunit`` cannot be given too (got {wunit}). Set ``wunit=None``"
-                )
+            wunit_input = wunit
             wunit = w.unit.to_string()
             if wunit not in WAVELEN_UNITS + WAVENUM_UNITS:
                 # Convert to something we know, for instance 'nm' :
@@ -670,6 +682,29 @@ class directly. Ex::
                 # else, an error will be raised anyway on Spectrum creation.
             w = w.value
 
+            # Check units.
+            # In particular, deal with confusions arising from "nm" being either "nm_vac" or "nm_air"  # TODO: generalize to "µm" too
+            if wunit_input is None:
+                if wunit == "nm":
+                    warn(
+                        "Input wunit not given, risk of confusion : RADIS assumes wavelengths are given as seen in air, not vacuum. To remove this warning be explicit by writing `Spectrum.from_array(..., wunit='nm_air')`"
+                    )
+                else:
+                    pass
+            else:
+                # Check units are the same
+                if wunit == "nm" and (
+                    wunit_input == "nm_vac" or wunit_input == "nm_air"
+                ):
+                    # extra precision given by the user, not an error. Use the input value
+                    wunit = wunit_input
+                elif wunit != wunit_input:
+                    raise ValueError(
+                        f"``w`` is a dimensionned array (in {wunit}), therefore ``wunit`` should not be given ( `wunit=None`), or be the same (got `wunit={wunit_input}`). Set ``wunit=None``"
+                    )
+                else:
+                    pass
+
         quantities = {quantity: (w, I)}
         units = {quantity: Iunit}
 
@@ -861,6 +896,12 @@ def from_specutils(self, spectrum, var="radiance"):
         """Convert a ``specutils`` :py:class:`specutils.spectra.spectrum1d.Spectrum1D`
         to a ``radis`` :py:class:`~radis.spectrum.spectrum.Spectrum` object.
 
+        Parameters
+        ----------
+        spectrum: a ``specutils`` :py:class:`specutils.spectra.spectrum1d.Spectrum1D`
+        var: str
+            spectral array, default ``"radiance"``
+
         Examples
         --------
 
@@ -904,14 +945,31 @@ def from_specutils(self, spectrum, var="radiance"):
 
         assert isinstance(spectrum, specutils.Spectrum1D)
 
+        if "waveunit" in spectrum.meta and spectrum.meta["waveunit"] in WAVELEN_UNITS:
+            raise ValueError(
+                f"Specutils only handles wavelengths in vacuum. Expected `spectrum.meta['waveunit']` to be one of {WAVELENVAC_UNITS}, got  {spectrum.meta['waveunit']}`"
+            )
+
+        conditions = spectrum.meta.copy()
+
+        if spectrum.spectral_axis.unit == "nm":
+            # note : in Specutils wavelengths are given as wavelength in vacuum (1e7/X conversion
+            # to wavenumbers)
+            waverange = spectrum.wavelength
+            waveunit = "nm_vac"
+            if "waveunit" in conditions:
+                conditions["waveunit"] = waveunit
+        else:
+            waverange = spectrum.spectral_axis
+            waveunit = None  # use default
+
         # Convert to radis Spectrum
-        # note : in Specutils wavelengths are given as wavelength in vacuum (1e7/X conversion
-        # to wavenumbers), so here we give the frequency unit.
         return Spectrum.from_array(
-            spectrum.wavelength.to("1 / cm"),
+            waverange,
             spectrum.flux,
             quantity=var,
-            conditions=spectrum.meta,
+            wunit=waveunit,
+            conditions=conditions,
         )
 
     # Public functions
@@ -1010,7 +1068,7 @@ def get(
             else:
                 # Try to compute it automatically :
                 try:
-                    self.update(var)
+                    self.update(var, verbose=False)
                 except ValueError as err:
                     raise ValueError(
                         f"{var} not in Spectrum arrays {self.get_vars()}. An error occured while trying to recompute it from the available arrays and conditions. See above"
@@ -1054,7 +1112,7 @@ def get(
         if Iunit == "default":
             # Add unit consistent with the namespace
             # https://github.com/radis/radis/issues/456
-            if wunit in ["nm", "nm_air"]:
+            if wunit in ["nm", "nm_air", "nm_vac"]:
                 if var in [
                     "radiance",
                     "radiance_noslit",
@@ -1074,6 +1132,8 @@ def get(
                     Iunit = Iunit0.replace("/nm", "/cm-1")
                 else:
                     Iunit = Iunit0
+            else:
+                raise ValueError(wunit)
 
         # Retrieve data (with correct unit)
         if Iunit != "default" and Iunit != Iunit0:
@@ -1119,6 +1179,8 @@ def get(
         if return_units == "as_str":  # only used internally
             return w, I, wunit, Iunit
         elif return_units:
+            if wunit == "nm_vac":
+                wunit = "nm"  # fix for Astropy units
             return w * Unit(wunit), I * Unit(Iunit)
         else:
             return w, I
@@ -1609,7 +1671,7 @@ def crop(self, wmin=None, wmax=None, wunit="default", inplace=True):
             s.crop(s_exp.get_wavelength.min(), s_exp.get_wavelength.max(), 'nm')
             plot_diff(s_exp, s)
 
-        .. minigallery:: radis.spectrum.spectrum.Spectrum.crop
+        .. minigallery:: radis.Spectrum.crop
 
 
         See Also
@@ -1895,6 +1957,9 @@ def plot(
     ):
         """Plot a :py:class:`~radis.spectrum.spectrum.Spectrum` object.
 
+        .. note::
+            default plotting library and templates can be edited in :py:attr:`radis.config` ["plot"]
+
         Parameters
         ----------
         var: variable (`absorbance`, `transmittance`, `transmittance_noslit`, `xsection`, etc.)
@@ -1943,7 +2008,8 @@ def plot(
             show figure. Default ``False``. Will still show the figure in
             interactive mode, e.g, `%matplotlib inline` in a Notebook.
         show_ruler: bool
-            if `True`, add a ruler tool to the Matplotlib toolbar.
+            if `True`, add a ruler tool to the Matplotlib toolbar. Convenient
+            to measure distances between peaks, etc.
 
             .. warning::
                 still experimental in 0.9.30 ! Try it, feedback welcome !
@@ -2112,7 +2178,9 @@ def clean_error_msg(string):
 
         return line
 
-    def get_populations(self, molecule=None, isotope=None, electronic_state=None):
+    def get_populations(
+        self, molecule=None, isotope=None, electronic_state=None, show_warning=True
+    ):
         """Return populations that are featured in the spectrum, either as
         upper or lower levels.
 
@@ -2126,6 +2194,8 @@ def get_populations(self, molecule=None, isotope=None, electronic_state=None):
         electronic_state: str
             if None, only one electronic state must be defined. Else, an error
             is raised
+        show_warning: bool
+            if False, turns off warning about meaning of populations, see Notes and discussion on https://github.com/radis/radis/issues/508.
 
         Returns
         -------
@@ -2143,12 +2213,54 @@ def get_populations(self, molecule=None, isotope=None, electronic_state=None):
 
         (If Spectrum generated with RADIS, structure should match that of
         SpectrumFactory.get_populations())
+
+        Examples
+        --------
+
+        An example on how different are populations used for partition function and spectrum calculations ::
+
+            #%% CO2 example
+            # For instance, plot populations of a given vibrational level, v1,v2,v3=(0,1,0)
+            import radis
+            s = radis.test_spectrum(molecule="CO2", Tvib=3000, Trot=1000,
+                                    export_lines=True,
+                                    export_populations="rovib",
+                                    isotope=1)
+            pops = s.get_populations("CO2")["rovib"]
+
+            import matplotlib.pyplot as plt
+            pops.query("v1==0 & v2==1 & v3==0").plot("j", "n",
+                                                     label="pops. used to compute partition functions")
+            s.lines.query("v1l==0 & v2l==1 & v3l==0").plot("jl", "nl", ax=plt.gca(), kind="scatter", color="r",
+                                                           label="pops. of visible absorbing lines")
+            plt.legend()
+            plt.xlim((0,70))
+
+
+        .. minigallery:: radis.spectrum.spectrum.Spectrum.get_populations
+
+        See Also
+        --------
+        :py:meth:`~radis.spectrum.spectrum.Spectrum.plot_populations`,
+        :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`
+
         """
+        # Warn user on the meaning of these population
+        warn(
+            UserWarning(
+                """Populations valid for partition function calculation but sometimes NOT for spectra calculations, e.g. CO2.
+                         See help on how to use 's.lines.query' instead, for instance in https://github.com/radis/radis/issues/508.
+                         Turn off warning with 'show_warning=False'
+                         """
+            )
+        )
 
         # Check inputs, get default values
         populations = self.populations
         if populations is None or populations == {}:
-            raise ValueError("Populations not defined")
+            raise ValueError(
+                "Populations not defined. Use export_populations='vib' or 'rovib' in calc_spectrum"
+            )
         if type(populations) != dict:
             raise TypeError("Method defined for populations as dictionary")
         if molecule is None:
@@ -2316,6 +2428,17 @@ def plot_populations(
             (as it is done during the calculation of emission integral)
         kwargs: **dict
             are forwarded to the plot
+
+        Examples
+        --------
+
+        .. minigallery:: radis.spectrum.spectrum.Spectrum.plot_populations
+
+        See Also
+        --------
+        :py:meth:`~radis.spectrum.spectrum.Spectrum.get_populations`,
+        :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`,
+
         """
         import matplotlib.pyplot as plt
 
@@ -2893,21 +3016,19 @@ def plot_slit(self, wunit=None, waveunit=None):
 
         Parameters
         ----------
-
         wunit: ``'nm'``, ``'cm-1'``, or ``None``
             plot slit in wavelength or wavenumber. If ``None``, use the unit
             the slit in which the slit function was given. Default ``None``
 
         Returns
         -------
-
         fix, ax: matplotlib objects
             figure and ax
 
+        .. minigallery:: radis.spectrum.spectrum.Spectrum.plot_slit
 
         See Also
         --------
-
         :ref:`the Spectrum page <label_spectrum>`
         """
         # Deprecated inputs
@@ -3015,50 +3136,54 @@ def plot_slit(self, wunit=None, waveunit=None):
     def line_survey(
         self,
         overlay=None,
-        wunit="default",
-        writefile=None,
+        wunit="cm-1",
+        Iunit="hitran",
+        medium="air",
         cutoff=None,
+        plot="S",
+        lineinfo=["int", "A", "El"],
+        barwidth="hwhm_voigt",  # 0.01,
+        yscale="log",
+        writefile=None,
         *args,
         **kwargs,
     ):
         """Plot Line Survey (all linestrengths used for calculation) Output in
-        Plotly (html)
+            Plotly (html)
 
         Parameters
         ----------
-        spec: Spectrum
-            result from SpectrumFactory calculation (see spectrum.py)
-        overlay: 'absorbance', 'transmittance', 'radiance', etc... or list of the above, or None
-            overlay Linestrength with specified variable calculated
-            in `spec`.
-            Get the full list with the :meth:`~radis.spectrum.spectrum.Spectrum.get_vars`
-            method. Default ``None`` ::
-
-                s.lineSurvey(overlay='abscoeff')
-        wunit: ``'default'``, ``'nm'``, ``'cm-1'``, ``'nm_vac'``,
-            wavelength air, wavenumber, or wavelength vacuum. If ``'default'``,
-            Spectrum :py:meth:`~radis.spectrum.spectrum.Spectrum.get_waveunit` is used.
-        medium: {'air', 'vacuum', 'default'}
-            Choose whether wavelength are shown in air or vacuum. If ``'default'``
-            lines are shown as stored in the spectrum.
+        overlay: tuple (w, I, [name], [units]), or list or tuples
+            plot (w, I) on a secondary axis. Useful to compare linestrength with
+            calculated / measured data::
+
+                LineSurvey(overlay='abscoeff')
+        wunit: ``'nm'``, ``'cm-1'``
+            wavelength / wavenumber units
+        Iunit: ``'hitran'``, ``'splot'``
+            Linestrength output units:
+
+            - ``'hitran'``: (cm-1/(molecule/cm-2))
+            - ``'splot'`` : (cm-1/atm)   (Spectraplot units [2]_)
+
+            Note: if not None, cutoff criteria is applied in this unit.
+            Not used if plot is not 'S'
+        medium: ``'air'``, ``'vacuum'``
+            show wavelength either in air or vacuum. Default ``'air'``
+        plot: str
+            what to plot. Default ``'S'`` (scaled line intensity). But it can be
+            any key in the lines, such as population (``'nu'``), or Einstein coefficient (``'Aul'``)
+        lineinfo: list, or ``'all'``
+            extra line information to plot. Should be a column name in the databank
+            (s.lines). For instance: ``'int'``, ``'selbrd'``, etc... Default [``'int'``]
 
         Other Parameters
         ----------------
         writefile: str
             if not ``None``, a valid filename to save the plot under .html format.
             If ``None``, use the ``fig`` object returned to show the plot.
-        kwargs:: dict
-            Other inputs are passed to :func:`~radis.tools.line_survey.LineSurvey`.
-            Example below (see :py:func:`~radis.tools.line_survey.LineSurvey`
-            documentation for more details):
-        Iunit: `hitran`, `splot`
-            Linestrength output units:
-
-            - `hitran`: (cm-1/(molecule/cm-2))
-            - `splot`: (cm-1/atm)   (Spectraplot units [2]_)
-
-            Note: if not None, cutoff criteria is applied in this unit.
-            Not used if plot is not 'S'
+        yscale: ``'log'``, ``'linear'``
+            Default ``'log'``
         barwidth: float or str
             if float, width of bars, in ``wunit``, as a fraction of full-range; i.e. ::
 
@@ -3069,50 +3194,48 @@ def line_survey(
 
                 barwidth = 'hwhm_voigt'
 
+            Returns
+            -------
+            fig: a Plotly figure.
+                If using a Jupyter Notebook, the plot will appear. Else, use ``writefile``
+                to export to an html file.
 
+            Plot in Plotly. See Output in [1]_
 
-        Returns
-        -------
-        fig: a Plotly figure.
-            If using a Jupyter Notebook, the plot will appear. Else, use ``writefile``
-            to export to an html file.
 
-        Plot in Plotly. See Output in [1]_
+            Examples
+            --------
+            An example using the :class:`~radis.lbl.factory.SpectrumFactory` to generate a spectrum::
 
+                from radis import SpectrumFactory
+                sf = SpectrumFactory(
+                                     wavenum_min=2380,
+                                     wavenum_max=2400,
+                                     mole_fraction=400e-6,
+                                     path_length=100,  # cm
+                                     isotope=[1],
+                                     export_lines=True,    # required for LineSurvey!
+                                     db_use_cached=True)
+                sf.load_databank('HITRAN-CO2-TEST')
+                s = sf.eq_spectrum(Tgas=1500)
+                s.apply_slit(0.5)
+                s.line_survey(overlay='radiance_noslit', barwidth=0.01, lineinfo="all")  # or barwidth='hwhm_voigt'
 
-        Examples
-        --------
-        An example using the :class:`~radis.lbl.factory.SpectrumFactory` to generate a spectrum::
-
-            from radis import SpectrumFactory
-            sf = SpectrumFactory(
-                                 wavenum_min=2380,
-                                 wavenum_max=2400,
-                                 mole_fraction=400e-6,
-                                 path_length=100,  # cm
-                                 isotope=[1],
-                                 export_lines=True,    # required for LineSurvey!
-                                 db_use_cached=True)
-            sf.load_databank('HITRAN-CO2-TEST')
-            s = sf.eq_spectrum(Tgas=1500)
-            s.apply_slit(0.5)
-            s.line_survey(overlay='radiance_noslit', barwidth=0.01)  # or barwidth='hwhm_voigt'
-
-        See the output in :ref:`Examples <label_examples>`
-
-        .. minigallery:: radis.spectrum.spectrum.Spectrum.line_survey
-
-        References
-        ----------
-        .. [1] `RADIS Online Documentation (LineSurvey) <https://radis.readthedocs.io/en/latest/tools/line_survey.html>`__
+            See the output in :ref:`Examples <label_examples>`
 
-        .. [2] `SpectraPlot <http://www.spectraplot.com/survey>`__
+            .. minigallery:: radis.spectrum.spectrum.Spectrum.line_survey
 
+            References
+            ----------
+            .. [1] `RADIS Online Documentation (LineSurvey) <https://radis.readthedocs.io/en/latest/tools/line_survey.html>`__
 
-        See Also
-        --------
-        :func:`~radis.tools.line_survey.LineSurvey`,
-        :ref:`the Spectrum page <label_spectrum>`
+            .. [2] `SpectraPlot <http://www.spectraplot.com/survey>`__
+
+
+            See Also
+            --------
+            :func:`~radis.tools.line_survey.LineSurvey`,
+            :ref:`the Spectrum page <label_spectrum>`
         """
 
         from radis.tools.line_survey import LineSurvey
@@ -3146,20 +3269,21 @@ def get_overlay(overlay):
 
             overlay = [get_overlay(ov) for ov in overlay]
 
-            return LineSurvey(
-                self,
-                overlay=overlay,
-                wunit=wunit,
-                writefile=writefile,
-                cutoff=cutoff,
-                *args,
-                **kwargs,
-            )
-
-        else:
-            return LineSurvey(
-                self, wunit=wunit, writefile=writefile, cutoff=cutoff, *args, **kwargs
-            )
+        return LineSurvey(
+            self,
+            overlay=overlay,
+            wunit=wunit,
+            Iunit=Iunit,
+            medium=medium,
+            cutoff=cutoff,
+            plot=plot,
+            lineinfo=lineinfo,
+            barwidth=barwidth,
+            yscale=yscale,
+            writefile=writefile,
+            *args,
+            **kwargs,
+        )
 
     def get_conditions(self):
         """Get all physical / computational parameters.
@@ -3373,12 +3497,19 @@ def to_specutils(self, var=None, wunit="default", Iunit="default"):
         ----------
         var: 'radiance', 'radiance_noslit', etc.
             which spectral array to convert. If ``None`` and only one spectral
-            arry is defined, use it
+            arry is defined, use it.
         wunit: ``'nm'``, ``'cm'``, ``'nm_vac'``.
             wavespace unit: wavelength in air (``'nm'``), wavenumber
             (``'cm-1'``), or wavelength in vacuum (``'nm_vac'``).
             if ``"default"``, default unit for waveunit is used. See
             :py:meth:`~radis.spectrum.spectrum.Spectrum.get_waveunit`.
+
+            .. note::
+                ``specutils`` handles wavelengths in vacuum only. If using
+                ``'nm'`` wavelengths will be converted to wavelengths in vacuum.
+                We recommend using ``'nm_air'`` or ``'nm_vac'`` to avoid any
+                confusion.
+
         Iunit: unit for variable ``var``
             if ``"default"``, default unit for quantity `var` is used. See the
             :py:attr:`~radis.spectrum.spectrum.Spectrum.units` attribute.
@@ -3430,22 +3561,37 @@ def to_specutils(self, var=None, wunit="default", Iunit="default"):
                 "   pip install specutils"
             ) from err
 
+        meta = self.get_conditions().copy()
+
         if var is None:
             var = self._get_unique_var(operation_name="to_specutils")
         if wunit == "default":
             wunit = self.get_waveunit()
+            if wunit in WAVELEN_UNITS:  # wavlength units in air
+                # AFAIK, specutil's Spectrum1D will only handle wavelengths as seen in vacuum
+                # so below we request wavelengths in vac :
+                wunit = "nm_vac"
+        if wunit in WAVELEN_UNITS:  # wavlength units in air
+            raise ValueError(
+                f"specutil's Spectrum1D will only handle wavelengths as seen in vacuum. Use one of `s.to_specutils(..., wunit={WAVELENVAC_UNITS}`)"
+            )
+
+        # Update waveunit stored in conditions (in particular, update if wavelengths are in vacuum or air)
+        if "waveunit" in meta:
+            meta["waveunit"] = wunit
+
         if Iunit == "default":
             Iunit = self.units[var]
 
-        if wunit in WAVELEN_UNITS:
-            wunit = "nm_vac"  # AFAIK, specutil's Spectrum1D will only handle wavelengths as seen in vacuum
-            # so below we request wavelengths in vac :
+        w, I = self.get(
+            var, wunit=wunit, Iunit=Iunit, copy=True, trim_nan=True, return_units=True
+        )
+        # w, I are dimensionned arrays
 
-        w, I = self.get(var, wunit=wunit, Iunit=Iunit, copy=True, trim_nan=True)
         return Spectrum1D(
-            flux=I * u.Unit(Iunit),
-            spectral_axis=w * u.Unit(wunit),
-            meta=self.get_conditions(),
+            flux=I,
+            spectral_axis=w,
+            meta=meta,
         )
 
     def resample(
@@ -3453,7 +3599,7 @@ def resample(
         w_new,
         unit="same",
         out_of_bounds="nan",
-        energy_threshold=5e-3,
+        energy_threshold="default",
         print_conservation=False,
         inplace=True,
         if_conflict_drop=None,
@@ -3501,10 +3647,11 @@ def resample(
 
         Other Parameters
         ----------------
-        energy_threshold: float or ``None``
+        energy_threshold: float or ``None`` or ``'default'
             if energy conservation (integrals on the intersecting range) is above
-            this threshold, raise an error. If ``None``, dont check for energy conservation
-            Default 5e-3 (0.5%)
+            this threshold, raise an error. If ``None``, dont check for energy conservation.
+            If ``'default'``, look up the value in :py:attr:`radis.config` ["RESAMPLING_TOLERANCE_THRESHOLD"]
+            Default ``'default'``
         print_conservation: boolean
             if ``True``, prints energy conservation. Default ``False``.
         inplace: boolean
@@ -3740,8 +3887,6 @@ def is_at_equilibrium(self, check="warn", verbose=False):
                 assert conditions["overpopulation"] is None
             assert conditions["self_absorption"]  # is True
 
-            guess = True
-
         except AssertionError:
             guess = False
             if verbose:
@@ -3761,6 +3906,8 @@ def is_at_equilibrium(self, check="warn", verbose=False):
                 )
             )
             guess = not equilibrium
+        else:
+            guess = True
 
         if equilibrium != guess:
             msg = (
@@ -3792,7 +3939,7 @@ def is_optically_thin(self):
                 + "value manually with s.conditions['self_absorption']=..."
             )
 
-    def copy(self, copy_lines=True, quantity="all"):
+    def copy(self, copy_lines=True, quantity="all", copy_arrays=True):
         """Returns a copy of this Spectrum object (performs a smart deepcopy)
 
         Parameters
@@ -3801,6 +3948,10 @@ def copy(self, copy_lines=True, quantity="all"):
             default ``True``
         quantity: 'all', or one of 'radiance_noslit', 'absorbance', etc.
             if not 'all', copy only one quantity. Default ``'all'``
+        copy_arrays: bool
+            if ``False``, returned array's quantity is a pointer to the original
+            Spectrum. Faster, but warning, changing them will then change
+            the original Spectrum. Default ``True``
 
         Examples
         --------
@@ -3810,7 +3961,9 @@ def copy(self, copy_lines=True, quantity="all"):
 
         """
         try:
-            return self.__copy__(copy_lines=copy_lines, quantity=quantity)
+            return self.__copy__(
+                copy_lines=copy_lines, quantity=quantity, copy_arrays=copy_arrays
+            )
         except MemoryError:
             raise MemoryError(
                 "during copy of Spectrum. If you don't need them, "
@@ -3818,7 +3971,7 @@ def copy(self, copy_lines=True, quantity="all"):
                 + "del s.lines ; or, use copy_lines=False"
             )
 
-    def __copy__(self, copy_lines=True, quantity="all"):
+    def __copy__(self, copy_lines=True, quantity="all", copy_arrays=True):
         """Generate a new spectrum object.
 
         Note: using deepcopy would work but then the Spectrum object would be pickled
@@ -3830,6 +3983,10 @@ def __copy__(self, copy_lines=True, quantity="all"):
             default ``True``
         quantity: 'all', or one of 'radiance_noslit', 'absorbance', etc.
             if not 'all', copy only one quantity. Default ``'all'``
+        copy_arrays: bool
+            if ``False``, returned array's quantity is a pointer to the original
+            Spectrum. Faster, but warning, changing them will then change
+            the original Spectrum. Default ``True``
 
         Notes
         -----
@@ -3852,7 +4009,7 @@ def __copy__(self, copy_lines=True, quantity="all"):
                     quantity,
                     wunit=self.get_waveunit(),
                     Iunit=self.units[quantity],
-                    copy=True,
+                    copy=copy_arrays,
                 )
             }
         else:
@@ -3862,7 +4019,7 @@ def __copy__(self, copy_lines=True, quantity="all"):
                     quantity,
                     wunit=self.get_waveunit(),
                     Iunit=self.units[quantity],
-                    copy=True,
+                    copy=copy_arrays,
                 )  # copy=True still needed as the waverange is the one of the original array
             }
             del self._q[quantity]  # no need to keep it in this Spectrum
@@ -3909,7 +4066,7 @@ def __copy__(self, copy_lines=True, quantity="all"):
             references=references,
             name=name,
             check_wavespace=False,  # no need, as Spectrum was normally already properly defined
-            warnings=False,  # saves about 3.5 ms on the Performance test object
+            # saves about 3.5 ms on the Performance test object
         )
 
         # Add extra information
@@ -4021,7 +4178,7 @@ def compare_with(
         )
 
     def cite(self, format="bibentry"):
-        """Prints bibliographic references used to compute this spectrum, as
+        r"""Prints bibliographic references used to compute this spectrum, as
         stored in the :py:attr:`~radis.spectrum.spectrum.Spectrum.references`
         dictionary. Default references known to RADIS are listed in :py:data:`radis.db.references.doi`.
 
@@ -4328,13 +4485,22 @@ def __str__(self):
 
         return ""  # self.print_conditions()
 
-    def take(self, var, copy_lines=False):
+    def take(self, var, copy_lines=False, copy_arrays=True):
         """
         Parameters
         ----------
         var : str
             spectral quantity (``'absorbance'``, ``'transmittance'``, ``'xsection'`` etc.)
 
+        Other Parameters
+        ----------------
+        copy_lines: bool
+            if ``True``, export ``s.lines``. Default ``False``
+        copy_arrays: bool
+            if ``False``, returned array's quantity is a pointer to the original
+            Spectrum. Faster, but warning, changing them will then change
+            the original Spectrum. Default ``True``
+
         Returns
         -------
         s: Spectrum
@@ -4351,7 +4517,7 @@ def take(self, var, copy_lines=False):
 
         """
 
-        return self.copy(quantity=var, copy_lines=copy_lines)
+        return self.copy(quantity=var, copy_lines=copy_lines, copy_arrays=copy_arrays)
 
     # %% Add min, max, normalize operations
 
diff --git a/radis/spectrum/utils.py b/radis/spectrum/utils.py
index 4d8065fd6..18f35e870 100644
--- a/radis/spectrum/utils.py
+++ b/radis/spectrum/utils.py
@@ -594,6 +594,8 @@ def walk_print_tree(prof, name, level, write_number_column):
 def generate_perf_profile(profiler):
     """Visual/interactive performance profile
 
+    Requires ``tuna`` to be installed.
+
     See typical output in https://github.com/radis/radis/pull/325
 
     .. image:: https://user-images.githubusercontent.com/16088743/128018032-6049be72-1881-46ac-9d7c-1ed89f9c4f42.png
diff --git a/radis/tools/code_profiler.py b/radis/tools/code_profiler.py
new file mode 100644
index 000000000..0bd302c3e
--- /dev/null
+++ b/radis/tools/code_profiler.py
@@ -0,0 +1,71 @@
+import atexit
+import inspect
+import sys
+from functools import partial
+from io import StringIO
+
+
+class CodeProfiler:
+    def __init__(
+        self,
+        test_func=None,
+        whitelist={"radis"},  # Look for these words in the file path.
+        exclusions={"<"},  # Ignore <listcomp>, etc. in the function name.
+        filename="code_profiler_out.txt",
+    ):
+
+        self.dbg_out = StringIO()
+        self.test_func = test_func
+        sys.setprofile(self.tracefunc)
+        self.test_func = test_func
+        self.memorized = set()
+        self.stack_level = 0
+        self.white_list = whitelist
+        self.exclusions = exclusions
+        atexit.register(partial(self.dump, filename=filename))
+
+    def tracefunc(self, frame, event, arg):
+
+        if event == "call":
+            self.stack_level += 1
+
+            unique_id = frame.f_code.co_filename + str(frame.f_lineno)
+            if unique_id in self.memorized:
+                return
+
+            # Part of filename MUST be in white list.
+            if any(x in frame.f_code.co_filename for x in self.white_list) and not any(
+                x in frame.f_code.co_name for x in self.exclusions
+            ):
+
+                if "self" in frame.f_locals:
+                    module_name = frame.f_locals["self"].__class__.__module__
+                    class_name = frame.f_locals["self"].__class__.__name__
+                    func_name = (
+                        module_name + "." + class_name + "." + frame.f_code.co_name
+                    )
+                else:
+                    module_name = inspect.getmodulename(frame.f_code.co_filename) + "."
+                    func_name = module_name + frame.f_code.co_name
+
+                func_name = self.stack_level * " ." + " " + func_name
+                txt = "{: <60} # {}, {}\n".format(
+                    func_name, frame.f_code.co_filename, frame.f_lineno
+                )
+                self.dbg_out.write(txt)
+                self.memorized.add(unique_id)
+
+        elif event == "return":
+            if self.test_func != None:
+                if self.test_func(frame):
+                    self.dbg_out.write("\n\n~~~ TEST FUNCTION SUCCESS!!! ~~~\n\n")
+                    self.test_func = None
+
+            self.stack_level -= 1
+
+    def dump(self, filename="code_profiler_out.txt"):
+        ptr = self.dbg_out.tell()
+        self.dbg_out.seek(0)
+        with open(filename, "w") as f:
+            f.write(self.dbg_out.read())
+        self.dbg_out.seek(ptr)
diff --git a/radis/tools/database.py b/radis/tools/database.py
index d1cea90cb..aa830c996 100644
--- a/radis/tools/database.py
+++ b/radis/tools/database.py
@@ -433,6 +433,7 @@ def load_spec(file, binary=True) -> Spectrum:  # , return_binary_status=False):
 
     Examples
     --------
+
     .. minigallery:: radis.load_spec
 
     See Also
@@ -1560,7 +1561,7 @@ def get_items(self, condition):
 
         See Also
         --------
-        :meth:`~radis.tools.database.SpecList.to_dict`,
+        :meth:`~radis.tools.database.SpecDatabase.to_dict`,
         :meth:`~radis.tools.database.SpecList.get`,
         :py:meth:`~radis.tools.database.SpecList.create_fname_grid`
         """
@@ -1630,7 +1631,7 @@ def __iter__(self):
         :meth:`~radis.tools.database.SpecList.keys`,
         :meth:`~radis.tools.database.SpecList.values`,
         :meth:`~radis.tools.database.SpecList.items`,
-        :meth:`~radis.tools.database.SpecList.to_dict`
+        :meth:`~radis.tools.database.SpecDatabase.to_dict`
         """
 
         return self.get(inplace=True).__iter__()
@@ -1643,7 +1644,7 @@ def keys(self):
 
         :meth:`~radis.tools.database.SpecList.values`,
         :meth:`~radis.tools.database.SpecList.items`,
-        :meth:`~radis.tools.database.SpecList.to_dict`
+        :meth:`~radis.tools.database.SpecDatabase.to_dict`
         """
 
         return list(self.to_dict().keys())
@@ -1656,7 +1657,7 @@ def values(self):
 
         :meth:`~radis.tools.database.SpecList.keys`,
         :meth:`~radis.tools.database.SpecList.items`,
-        :meth:`~radis.tools.database.SpecList.to_dict`
+        :meth:`~radis.tools.database.SpecDatabase.to_dict`
         """
 
         return list(self.to_dict().values())
@@ -1684,7 +1685,7 @@ def items(self):
         --------
         :meth:`~radis.tools.database.SpecList.keys`,
         :meth:`~radis.tools.database.SpecList.values`,
-        :meth:`~radis.tools.database.SpecList.to_dict`
+        :meth:`~radis.tools.database.SpecDatabase.to_dict`
         """
 
         return list(self.to_dict().items())
@@ -1719,7 +1720,7 @@ def plot(self, nfig=None, legend=True, **kwargs):
 
         See Also
         --------
-        Spectrum :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` method
+        Spectrum's :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` method
         """
 
         import matplotlib.pyplot as plt
@@ -2163,7 +2164,9 @@ def find_duplicates(self, columns=None):
 
         Examples
         --------
-        >>> db.find_duplicates(columns={'x_e', 'x_N_II'})
+        ::
+
+            db.find_duplicates(columns={'x_e', 'x_N_II'})
 
             Out[34]:
             file
@@ -2171,8 +2174,8 @@ def find_duplicates(self, columns=None):
             20180710_103.spec    True
             dtype: bool
 
-        You can see more examples on the :ref:`Spectrum Database section <label_spectrum_database>`
-        of the website.
+        You can see more examples in the :ref:`Spectrum Database section <label_spectrum_database>`
+
         """
         dg = self.see(columns=columns).astype(str).duplicated()
         # need to convert eveything as a str to avoid comparaison problems (Minou)
@@ -2224,8 +2227,10 @@ def add(
             are forwarded to Spectrum.store() method. See the :meth:`~radis.spectrum.spectrum.Spectrum.store`
             method for more information.
 
-        *Other :py:meth:`~radis.spectrum.spectrum.Spectrum.store` parameters can be given
-        as kwargs arguments:*
+            .. note::
+
+                Other :py:meth:`~radis.spectrum.spectrum.Spectrum.store` parameters can be given
+                as kwargs arguments. See below :
 
         compress: 0, 1, 2
             if ``True`` or 1, save the spectrum in a compressed form
@@ -2267,7 +2272,7 @@ def add(
         :meth:`~radis.tools.database.SpecList.get`,
         :meth:`~radis.tools.database.SpecList.get_unique`,
         :meth:`~radis.tools.database.SpecList.get_closest`,
-        :meth:`~spectrum.spectrum.Spectrum.update`
+        :meth:`~radis.tools.database.SpecDatabase.update`
         """
 
         # Check inputs
@@ -2350,7 +2355,8 @@ def add(
         return file
 
     def interpolate(self, **kwconditions):
-        """Interpolate existing spectra from the database to generate a new spectrum with conditions kwargs
+        """Interpolate existing spectra from the database to generate a new spectrum
+        with conditions kwargs
 
         Examples
         --------
@@ -2469,7 +2475,7 @@ def fit_spectrum(
             db.fit_spectrum(s_exp, get_residual=lambda s_exp, s: get_residual(s_exp, s, var='transmittance'))
 
         You can see more examples on the :ref:`Spectrum Database section <label_spectrum_database>`
-        of the website. More advanced tools for interactive fitting of multi-dimensional, multi-slabs
+        More advanced tools for interactive fitting of multi-dimensional, multi-slabs
         spectra can be found in :py:mod:`fitroom`.
 
         See Also
diff --git a/radis/tools/line_survey.py b/radis/tools/line_survey.py
index 7737399e6..6b5750d94 100644
--- a/radis/tools/line_survey.py
+++ b/radis/tools/line_survey.py
@@ -23,6 +23,7 @@
 )
 from radis.io.hitran import columns_2004 as hitrancolumns
 from radis.misc.basics import is_float
+from radis.misc.printer import superscript_map
 from radis.misc.utils import NotInstalled
 from radis.phys.air import vacuum2air
 from radis.phys.constants import k_b
@@ -126,7 +127,9 @@ def LineSurvey(
 
     Examples
     --------
-    An example using the :class:`~radis.lbl.factory.SpectrumFactory` to generate a spectrum::
+    An example using the :class:`~radis.lbl.factory.SpectrumFactory` to generate a spectrum,
+    using the Spectrum :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey` method
+    directly ::
 
         from radis import SpectrumFactory
         sf = SpectrumFactory(
@@ -140,7 +143,7 @@ def LineSurvey(
         sf.load_databank('HITRAN-CO2-TEST')
         s = sf.eq_spectrum(Tgas=1500)
         s.apply_slit(0.5)
-        s.line_survey(overlay='radiance_noslit', barwidth=0.01)
+        s.line_survey(overlay='radiance_noslit', barwidth=0.01, lineinfo="all")
 
     See the output in :ref:`Examples <label_examples>`
 
@@ -156,6 +159,10 @@ def LineSurvey(
     .. [1] `RADIS Online Documentation (LineSurvey) <https://radis.readthedocs.io/en/latest/tools/line_survey.html>`__
 
     .. [2] `SpectraPlot <http://www.spectraplot.com/survey>`__
+
+    See Also
+    --------
+    :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`
     """
 
     # Check inputs
@@ -277,6 +284,41 @@ def get_x(w):
     # Parse databank to get relevant information on each line
     # (one function per databank format)
 
+    def add_details(row, details):
+        """Add details in string; add on 2 columns if "upper" and "lower" value follow"
+
+        Example :  keep "gu", "gl" on the same line
+
+        Also add unit if known
+        """
+        label = ""
+        for k in details:
+            name, _, unit = details[k]
+            if k[-1] == "u" and k[:-1] + "l" in details:
+                continue  #  don't show "upper" value. Will be shown on same line as lower
+
+            name = f"({name})" if name else name
+            if is_float(row[k]):
+                val = f"{row[k]:.3g}"
+            else:
+                val = f"{row[k]}"
+            label += f"<br>{k} {name}: {val} {unit}"
+            # If lower value, also show upper value on same line
+            if k[-1] == "l" and k[:-1] + "u" in details:
+                k_up = k[:-1] + "u"
+                if is_float(row[k_up]):
+                    label += (
+                        "&nbsp;" * (30 - len(k) - len(val))
+                        + f"{k_up} {name}: {row[k_up]:.3g} {unit}"
+                    )
+                else:
+                    label += (
+                        "&nbsp;" * (30 - len(k) - len(val))
+                        + f"{k_up} {name}: {row[k_up]} {unit}"
+                    )
+
+        return label
+
     def get_label_hitran(row, details, attrs):
         """
         Todo
@@ -290,6 +332,7 @@ def get_label_hitran(row, details, attrs):
 
         will be much faster!
         """
+
         if "id" in attrs:
             molecule = get_molecule(attrs["id"])
 
@@ -382,13 +425,33 @@ def get_label_hitran(row, details, attrs):
                 else:
                     iso = "?"
 
-                label = "{molec} [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f} {rl:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f} {ru:.0f})".format(
+                label = "{molec} [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}{l2l}{v3l:.0f} r={rl})->({v1u:.0f}{v2u:.0f}{l2u}{v3u:.0f} r={ru})"
+                label = label.format(
                     **dict(
-                        [(k, row[k]) for k in add]
+                        [
+                            (k, row[k])
+                            for k in add
+                            if k not in ["l2l", "l2u", "ru", "rl"]
+                        ]
+                        +
+                        # Try to get l number as UTF-8 superscript (works if in superscript_map)
+                        [
+                            (
+                                k,
+                                superscript_map.get(
+                                    str(int(row[k])), f"`{row[k]:.0f}`"
+                                ),
+                            )
+                            for k in add
+                            if k in ["l2l", "l2u"]
+                        ]  # ignore missing params (like rl, ru ? )
                         + [
                             ("iso", iso),
                             ("molec", molecule),
                             ("branch", _fix_branch_format[row["branch"]]),
+                            # 'ru', 'rl' may be missing if not all columns loaded
+                            ("ru", f"{row['ru']:.0f}" if "ru" in row else "?"),
+                            ("rl", f"{row['rl']:.0f}" if "rl" in row else "?"),
                         ]
                     )
                 )
@@ -399,16 +462,17 @@ def get_label_hitran(row, details, attrs):
                 )
 
             # Add details about some line properties
-            for k in details:
-                name, _, unit = details[k]
-                if is_float(row[k]):
-                    label += "<br>{0} {1}: {2:.3g} {3}".format(k, name, row[k], unit)
-                else:
-                    label += "<br>{0} {1}: {2} {3}".format(k, name, row[k], unit)
+            label += add_details(row, details)
 
-        except:
+            label += f"<br>s.lines index: {row.name}"
+
+        except KeyError as err:
+            print(
+                f"Error during customized Line survey labelling. Printing everything : \n{str(err)}"
+            )
 
             # Add everything we know
+            # raise
             label = row.to_string().replace("\n", "<br>")
             if "id" not in row:
                 label = "{molecule}<br>" + label
@@ -443,12 +507,7 @@ def get_label_cdsd(row, details):
             )
         )
 
-        for k in details:
-            name, _, unit = details[k]
-            if is_float(row[k]):
-                label += "<br>{0} {1}: {2:.3g} {3}".format(k, name, row[k], unit)
-            else:
-                label += "<br>{0} {1}: {2} {3}".format(name, k, row[k], unit)
+        label += add_details(row, details)
 
         return label
 
@@ -480,12 +539,9 @@ def get_label_cdsd_hitran(row, details):
             )
         )
 
-        for k in details:
-            name, _, unit = details[k]
-            if is_float(row[k]):
-                label += "<br>{0} {1}: {2:.3g} {3}".format(k, name, row[k], unit)
-            else:
-                label += "<br>{0} {1}: {2} {3}".format(name, k, row[k], unit)
+        label += add_details(row, details)
+
+        label += f"<br>s.lines index: {row.name}"
 
         return label
 
@@ -511,15 +567,15 @@ def get_label_nist(row, attrs):
     def get_label_all(row):
         """print all lines details"""
         # label = row.__repr__()
-        label = "<br>".join(
-            [f"{k}: {v}" for k, v in row.items() if k not in ["wav", "shiftwav"]]
-        )
+        label = "<br>".join([f"{k}: {v}" for k, v in row.items()])
         return label
 
     def get_label_none(row):
         return "unknown databank format. \ndetails cant be read"
 
     # add extra info to label
+    if lineinfo == "all":
+        lineinfo = sp.columns
     details = {}
     if columndescriptor:
         for k in lineinfo:
@@ -531,12 +587,13 @@ def get_label_none(row):
                 )
             try:  # to find units and real name (if exists in initial databank)
                 _, ktype, name, unit = columndescriptor[k]
-                details[k] = (name, ktype, " [{0}]".format(unit))
-            except:
+            except KeyError:
                 details[k] = ("", None, "")  # keep short name
+            else:
+                details[k] = (name, ktype, " [{0}]".format(unit))
 
     # Get label
-    if dbformat in ["hitran", "hitemp", "radisdb-hitemp", "geisa"]:
+    if dbformat in ["hitran", "hitemp", "hitemp-radisdb", "radisdb-hitemp", "geisa"]:
         sp["label"] = sp.apply(lambda r: get_label_hitran(r, details, sp.attrs), axis=1)
     elif dbformat in ["cdsd-hitemp", "cdsd-4000"]:
         try:
diff --git a/setup.cfg b/setup.cfg
index c7029168a..0e5696c84 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,6 @@
 [tool:pytest]
 markers =
+    needs_cuda: this requires CUDA Installed (deselect with '-m "not needs_cuda"')
     fast: this tests runs within a few seconds (deselect with '-m "not fast"')
     needs_config_file: requires a ~/radis.json configuration file (deselect with '-m "not needs_config_file"')
     needs_connection: requires an internet connection (deselect with '-m "not needs_connection"')
diff --git a/setup.py b/setup.py
index fd3576e03..7ba1f9679 100644
--- a/setup.py
+++ b/setup.py
@@ -253,7 +253,7 @@ def run_setup(with_binary):
             "cython",
             "hitran-api",
             "lxml",  # parser used for ExoMol website
-            "numpy",
+            "numpy<=1.22.3 ",
             "matplotlib",  # ">=3.4.0" to suppress the Ruler warning, but only available for Python >= 3.7
             "habanero",  # CrossRef API to retrieve data from doi
             "h5py",  # HDF5
@@ -276,8 +276,8 @@ def run_setup(with_binary):
             "psutil",  # for getting user RAM
             "seaborn",  # other matplotlib themes
             "scipy>=1.4.0",
-            "tuna",  # to generate visual/interactive performance profiles
-            "vaex>=4.8.0",  # load HDF5 files  (version needed to fix issue with Spyder https://github.com/spyder-ide/spyder/issues/16183). #TODO : install only required sub-packages
+            # "tuna",  # to generate visual/interactive performance profiles
+            "vaex>=4.9.2",  # load HDF5 files  (version needed to fix https://github.com/radis/radis/issues/486). #TODO : install only required sub-packages
         ],
         extras_require={
             "dev": [


Test patch:
diff --git a/radis/test/db/test_molecule_lists.py b/radis/test/db/test_molecule_lists.py
index 6751d2ee7..9b180d4aa 100644
--- a/radis/test/db/test_molecule_lists.py
+++ b/radis/test/db/test_molecule_lists.py
@@ -46,7 +46,7 @@ def test_ExoMol_molecules_list(verbose=True, *args, **kwargs):
 
     if verbose:
         print("ExoMol molecules, fetched online ")
-        print(molecules)
+        print(sorted(molecules))
 
         print("Comparing Radis hardcoded ExoMol molecules list to the ExoMol website: ")
 
diff --git a/radis/test/db/test_molparams.py b/radis/test/db/test_molparams.py
index e4a17e880..c593e4dc0 100644
--- a/radis/test/db/test_molparams.py
+++ b/radis/test/db/test_molparams.py
@@ -1,11 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-Created on Wed Jul 21 00:47:30 2021
 
-@author: erwan
 """
 
-from radis.db.molparam import MolParams
+from radis.db.molparam import MOLPARAMS_EXTRA_PATH, MolParams
 
 
 def test_molparams(verbose=True, *args, **kwargs):
@@ -13,6 +11,25 @@ def test_molparams(verbose=True, *args, **kwargs):
     molpar = MolParams()
 
     assert molpar.get(2, 2, "abundance") == 0.0110574
+    assert molpar.get("CO2", 1, "molar_mass") == 43.98983
+
+    # Test errors :
+    import pytest
+
+    with pytest.raises(NotImplementedError):
+        molpar.get("C20H25N3O", 1, "molar_mass")
+
+    # Test reading of extra parameters
+    import pytest
+
+    molpar = MolParams(extra_file_json=None)
+    with pytest.raises(NotImplementedError):
+        molpar.get("trans-P2H2", 1, "molar_mass") == 43.98983
+
+    molpar = MolParams(extra_file_json=MOLPARAMS_EXTRA_PATH)
+    assert "abundance" in molpar.extra_molparams
+    assert "molar_mass" in molpar.extra_molparams
+    assert molpar.get("trans-P2H2", 1, "molar_mass") == 63.9631740604
 
 
 if __name__ == "__main__":
diff --git a/radis/test/files/geisa_O2_fragment.par b/radis/test/files/geisa_O2_fragment.par
index 7a2e45214..e86948a18 100644
--- a/radis/test/files/geisa_O2_fragment.par
+++ b/radis/test/files/geisa_O2_fragment.par
@@ -4,4 +4,4 @@
     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
+    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
diff --git a/radis/test/io/test_exomol.py b/radis/test/io/test_exomol.py
index 9dd6d56c4..1828f72e0 100644
--- a/radis/test/io/test_exomol.py
+++ b/radis/test/io/test_exomol.py
@@ -95,7 +95,7 @@ def test_calc_exomol_vs_hitemp(verbose=True, plot=True, *args, **kwargs):
     }
 
     s_exomol = calc_spectrum(
-        **conditions, databank="exomol", name="EXOMOL/HITEMP (H2 broadened)"
+        **conditions, databank="exomol", name="EXOMOL/HITEMP (default broadening)"
     )
     s_hitemp = calc_spectrum(
         **conditions,
diff --git a/radis/test/io/test_geisa.py b/radis/test/io/test_geisa.py
index b119e6016..293f74351 100644
--- a/radis/test/io/test_geisa.py
+++ b/radis/test/io/test_geisa.py
@@ -8,18 +8,10 @@
 --------
 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
@@ -32,7 +24,6 @@
 }
 
 
-@pytest.mark.local
 def test_local_geisa_co(verbose=True, warnings=True, **kwargs):
 
     # 1. Load local file
@@ -63,7 +54,6 @@ def test_local_geisa_co(verbose=True, warnings=True, **kwargs):
     return True
 
 
-@pytest.mark.local
 def test_local_geisa_h2o(verbose=True, warnings=True, **kwargs):
 
     # 1. Load local file
@@ -94,7 +84,6 @@ def test_local_geisa_h2o(verbose=True, warnings=True, **kwargs):
     return True
 
 
-@pytest.mark.local
 def test_local_geisa_co2(verbose=True, warnings=True, **kwargs):
 
     # 1. Load local file
diff --git a/radis/test/io/test_hdf5.py b/radis/test/io/test_hdf5.py
index 031a8b328..cbbdafc2f 100644
--- a/radis/test/io/test_hdf5.py
+++ b/radis/test/io/test_hdf5.py
@@ -7,7 +7,7 @@
 
 import pytest
 
-from radis.io.hdf5 import HDF5Manager, hdf2df
+from radis.io.hdf5 import DataFileManager, hdf2df
 from radis.io.hitemp import fetch_hitemp
 from radis.misc.config import getDatabankEntries
 
@@ -39,31 +39,40 @@ def test_hdf5_io_engines(*args, **kwargs):
             f.create_dataset(c, data=df0[c])
 
     # Test Pytables engine : add_metadata ; load ; read_metadata
-    manager = HDF5Manager(engine="pytables")
+    manager = DataFileManager(engine="pytables")
     manager.add_metadata("test_pytables.h5", metadata0)
 
-    df = manager.load("test_pytables.h5")
+    df = manager.read("test_pytables.h5")
     assert (df == df0).all().all()
     assert manager.read_metadata("test_pytables.h5") == metadata0
 
     # Test h5py engine : add_metadata ; load ; read_metadata
-    manager = HDF5Manager(engine="h5py")
+    manager = DataFileManager(engine="h5py")
     manager.add_metadata("test_h5py.h5", metadata0, key=None)
-    df = manager.load("test_h5py.h5", key=None)
+    df = manager.read("test_h5py.h5", key=None)
     assert (df == df0).all().all()
     assert manager.read_metadata("test_h5py.h5") == metadata0
 
     # Test vaex engine : add_metadata ; load ; read_
     # .. also able to read h5py files
-    manager = HDF5Manager(engine="vaex")
+    manager = DataFileManager(engine="vaex")
     manager.add_metadata("test_h5py.h5", metadata0, key=None)
-    df = manager.load("test_h5py.h5", key=None)
+    df = manager.read("test_h5py.h5", key=None)
     # this time; df is a vaex DataFrame
     # ... it keeps the file open so we should close the file handle
     assert (df.to_pandas_df() == df0).all().all()
     df.close()
     assert manager.read_metadata("test_h5py.h5", key=None) == metadata0
 
+    # Test get_columns function of DataFileManager
+    # For vaex
+    manager = DataFileManager(engine="vaex")
+    assert list(manager.get_columns("test_h5py.h5")) == ["a", "b"]
+
+    # For pytables
+    manager = DataFileManager(engine="pytables")
+    assert list(manager.get_columns("test_pytables.h5")) == ["a", "b"]
+
 
 @pytest.mark.needs_connection
 def test_local_hdf5_lines_loading(*args, **kwargs):
diff --git a/radis/test/io/test_hitemp.py b/radis/test/io/test_hitemp.py
index 02c9eff2c..06a1883bf 100644
--- a/radis/test/io/test_hitemp.py
+++ b/radis/test/io/test_hitemp.py
@@ -12,6 +12,29 @@
 from radis.misc.config import getDatabankList
 
 
+@pytest.mark.needs_connection
+@pytest.mark.fast
+def test_no_redownload(*args, **kwargs):
+    """Test that modification time for 2 successive hitemp fetch is the same, i.e.,
+    there is no redownload"""
+
+    # 1st call
+    df, local_files = fetch_hitemp(
+        "OH",
+        return_local_path=True,
+    )
+    from os.path import getmtime
+
+    first_mod_time = getmtime(local_files[0])
+
+    # 2nd call
+    df, local_files = fetch_hitemp(
+        "OH",
+        return_local_path=True,
+    )
+    assert first_mod_time == getmtime(local_files[0])
+
+
 @pytest.mark.fast
 def test_relevant_files_filter():
     from radis.io.hitemp import keep_only_relevant
@@ -425,6 +448,7 @@ def test_parse_hitemp_missing_labels_issue280(*args, **kwargs):
 
 
 if __name__ == "__main__":
+    test_no_redownload()
     test_relevant_files_filter()
     test_fetch_hitemp_OH_pytables()
     test_fetch_hitemp_OH_vaex()
diff --git a/radis/test/io/test_query.py b/radis/test/io/test_query.py
index f08f72abb..403a85473 100644
--- a/radis/test/io/test_query.py
+++ b/radis/test/io/test_query.py
@@ -168,13 +168,18 @@ def test_calc_hitran_spectrum(verbose=True, plot=False, *args, **kwargs):
     )
 
     if plot:
+        import matplotlib.pyplot as plt
+
         from radis import plot_diff
 
-        plot_diff(s, s2)
+        # Make sure matplotlib is interactive so that test are not stuck in pytest
+        plt.ion()
+
+        plot_diff(s, s2, method="ratio")
 
     # assert s == s2
     # Note: tiny differences (0.56%) probably due to how data is stored on disk
-    assert s.compare_with(s2, spectra_only="abscoeff", method="ratio", rtol=0.007)
+    assert s.compare_with(s2, spectra_only="abscoeff", rtol=0.007, plot=plot)
 
     return
 
diff --git a/radis/test/lbl/test_broadening.py b/radis/test/lbl/test_broadening.py
index b03898ef6..f0345fffe 100644
--- a/radis/test/lbl/test_broadening.py
+++ b/radis/test/lbl/test_broadening.py
@@ -938,6 +938,71 @@ def test_noneq_continuum(plot=False, verbose=2, warnings=True, *args, **kwargs):
     assert res < 5.2e-6
 
 
+def test_broadening_chunksize_eq(verbose=True, plot=False, *args, **kwargs):
+    """
+    Test equilibrium spectra with and without chunksize,
+    using different optimization strategies
+    and ensure that the residual is very small.
+
+    Tests the chunksize implementation introduced for calculation
+    of equilibrium spectra in https://github.com/radis/radis/pull/489/
+
+    """
+    if plot:  # Make sure matplotlib is interactive so that test are not stuck in pytest
+        plt.ion()
+
+    sf = SpectrumFactory(
+        wavenum_min=2000,
+        wavenum_max=2300,
+        cutoff=1e-27,
+        pressure=1,
+        isotope="1,2",
+        truncation=5,
+        molecule="CO",
+        neighbour_lines=5,
+        path_length=0.1,
+        mole_fraction=1e-3,
+        wstep=0.001,
+        verbose=True,
+    )
+    sf.fetch_databank("hitemp")
+    # sf.params.broadening_method = "convolve"
+    Tgas = 2000
+
+    # Testing chunksizes with different optimization strategies
+    for optimization in [None, "simple"]:
+        sf.params["optimization"] = optimization  # Setting optimization
+        sf.misc["chunksize"] = None
+        s_no_chunk = sf.eq_spectrum(Tgas=Tgas)
+        s_no_chunk.name = f"no chunksize   ({s_no_chunk.c['calculation_time']:.1f}s)"
+        assert (
+            s_no_chunk.c["chunksize"] is None
+        )  # make sure it was taken into account in Spectrum conditions
+
+        sf.misc["chunksize"] = 1e8  # Setting chunksize
+        s_chunk = sf.eq_spectrum(Tgas=Tgas)
+        s_chunk.name = (
+            f"chunksize {sf.misc.chunksize:.1e}  ({s_chunk.c['calculation_time']:.1f}s)"
+        )
+        assert s_chunk.c["chunksize"] == 1e8
+
+        # TODO @Sagar: add a way to check that number of chunks is not 1 (else
+        # we're just computing all lines at the same time and not checking anything)
+
+        # Residual calculation
+        res = get_residual(s_no_chunk, s_chunk, "abscoeff")
+        assert res < 1e-6
+        print(
+            "Res for chunksize = {0}, optimization = {1} : {2}".format(
+                sf.misc.chunksize, optimization, res
+            )
+        )
+        if plot:
+            plot_diff(
+                s_chunk, s_no_chunk, "abscoeff", title=f"Optimization: {optimization}"
+            )
+
+
 def _run_testcases(plot=False, verbose=True, *args, **kwargs):
 
     # Test broadening
@@ -950,6 +1015,7 @@ def _run_testcases(plot=False, verbose=True, *args, **kwargs):
     test_broadening_LDM_FT(plot=plot, verbose=3, *args, **kwargs)
     test_broadening_LDM_noneq(plot=plot, verbose=verbose, *args, **kwargs)
     test_truncations_and_neighbour_lines(*args, **kwargs)
+    test_broadening_chunksize_eq(plot=plot, verbose=verbose, *args, **kwargs)
 
     # Test warnings
     test_broadening_warnings(*args, **kwargs)
@@ -962,5 +1028,5 @@ def _run_testcases(plot=False, verbose=True, *args, **kwargs):
 
 
 if __name__ == "__main__":
-    # printm("test_broadening: ", _run_testcases(plot=True, verbose=True, debug=False))
+    printm("test_broadening: ", _run_testcases(plot=True, verbose=True, debug=False))
     printm("Testing broadening:", pytest.main(["test_broadening.py", "--pdb"]))
diff --git a/radis/test/lbl/test_gpu.py b/radis/test/lbl/test_gpu.py
index 04f747e17..9d1348cbb 100644
--- a/radis/test/lbl/test_gpu.py
+++ b/radis/test/lbl/test_gpu.py
@@ -9,18 +9,20 @@
 """
 import pytest
 
-from radis import SpectrumFactory
+from radis import SpectrumFactory, get_residual
 from radis.misc.printer import printm
 from radis.test.utils import getTestFile
 
 
-@pytest.mark.needs_cuda
-def test_eq_spectrum_gpu():
+def test_eq_spectrum_emulated_gpu(emulate=True, plot=False, *args, **kwargs):
+
+    print("Emulate: ", emulate)
     T = 1000
     p = 0.1
     wstep = 0.001
     wmin = 2284.0  # cm-1
     wmax = 2285.0  # cm-1
+
     sf = SpectrumFactory(
         wavenum_min=wmin,
         wavenum_max=wmax,
@@ -28,7 +30,7 @@ def test_eq_spectrum_gpu():
         path_length=1,  # doesnt change anything
         wstep=wstep,
         pressure=p,
-        isotope="1",
+        isotope="1,2",
         warnings={
             "MissingSelfBroadeningWarning": "ignore",
             "NegativeEnergiesWarning": "ignore",
@@ -42,13 +44,23 @@ def test_eq_spectrum_gpu():
         format="cdsd-4000",
         parfuncfmt="hapi",
     )
-    s_cpu = sf.eq_spectrum(Tgas=T)
-    s_gpu = sf.eq_spectrum_gpu(Tgas=T)
+
+    s_cpu = sf.eq_spectrum(Tgas=T, name="CPU")
+    s_gpu = sf.eq_spectrum_gpu(
+        Tgas=T, emulate=emulate, name="GPU (emulate)" if emulate else "GPU"
+    )
     s_cpu.crop(wmin=2284.2, wmax=2284.8)  # remove edge lines
     s_gpu.crop(wmin=2284.2, wmax=2284.8)
-    assert s_cpu.compare_with(
-        s_gpu, spectra_only=True, rtol=0.07, plot=False
-    )  # set the appropriate tolerance
+    if plot:
+        s_cpu.compare_with(s_gpu, spectra_only=True, plot=plot)
+    assert get_residual(s_cpu, s_gpu, "abscoeff") < 1.4e-5
+    assert get_residual(s_cpu, s_gpu, "radiance_noslit") < 7.3e-6
+    assert get_residual(s_cpu, s_gpu, "transmittance_noslit") < 1.4e-5
+
+
+@pytest.mark.needs_cuda
+def test_eq_spectrum_gpu(plot, *args, **kwargs):
+    test_eq_spectrum_emulated_gpu(emulate=False, plot=plot, *args, **kwargs)
 
 
 # --------------------------
diff --git a/radis/test/misc/test_utils.py b/radis/test/misc/test_utils.py
index fe4b7de30..b622c8bce 100644
--- a/radis/test/misc/test_utils.py
+++ b/radis/test/misc/test_utils.py
@@ -35,6 +35,9 @@ def test_files_from_regex_1(*args, **kwargs):
         "hitran_co_3iso_2000_2300cm.par",
         "hitran_CO2_fragment.par",
     ]
+
+    assert len(path_test) == len(path_actual)
+
     path_actual.sort()
     path_test.sort()
 
diff --git a/radis/test/phys/test_blackbody.py b/radis/test/phys/test_blackbody.py
index 4a7552d9f..9b37122b4 100644
--- a/radis/test/phys/test_blackbody.py
+++ b/radis/test/phys/test_blackbody.py
@@ -91,7 +91,7 @@ def test_planck_nm(verbose=True, plot=True, *args, **kwargs):
 
     # Check that max is correct
     # hardcoded for 2200 K, epsilon = 0.36 (~1.3 µm)
-    assert I_nm.max() == 75.98733181512608
+    assert I_nm.max() == 75.98736024707178
 
     # Test planck and planck_wn
     assert np.allclose(planck(w_nm, T, eps, unit=Iunit_per_nm), I_nm)
diff --git a/radis/test/spectrum/test_equations.py b/radis/test/spectrum/test_equations.py
new file mode 100644
index 000000000..404c8a9fe
--- /dev/null
+++ b/radis/test/spectrum/test_equations.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+Test RTE and rescale equations from :py:meth:`radis.spectrum.spectrum.Spectrum.update`
+"""
+
+
+# test_equations
+from numpy import allclose, exp
+
+
+def test_equations(*args, **kwargs):
+
+    import radis
+    from radis.phys.blackbody import planck, planck_wn
+
+    s = radis.test_spectrum(verbose=False)
+    s.apply_slit(0.5, "nm")
+
+    for k in list(s._q.keys()):  # reset all quantities
+        if k in ["wavespace", "wavelength", "wavenumber"]:
+            pass
+        elif k == "abscoeff":
+            new_abscoeff = s._q["abscoeff"].copy()
+        else:
+            del s._q[k]
+
+    # Recompute quantities ourselves
+    new_absorbance = new_abscoeff * s.conditions["path_length"]
+    new_transmittance_noslit = exp(-new_absorbance)
+    new_emissivity_noslit = 1 - new_transmittance_noslit
+    new_radiance_noslit = new_emissivity_noslit * planck_wn(
+        s.get_wavenumber(),
+        s.conditions["Tgas"],
+        unit="W/sr/cm2/cm-1",
+    )
+    new_radiance_noslit_wavelength = new_emissivity_noslit * planck(
+        1e7 / s.get_wavenumber(),
+        s.conditions["Tgas"],
+        unit="W/sr/cm2/nm",
+    )
+
+    # Compare with arrays recomputed automatically
+    assert (s.get("absorbance")[1] == new_absorbance).all()
+    assert (s.get("transmittance_noslit")[1] == new_transmittance_noslit).all()
+    assert (s.get("emissivity_noslit")[1] == new_emissivity_noslit).all()
+    assert allclose(
+        s.get("radiance_noslit", Iunit="W/sr/cm2/cm-1")[1], new_radiance_noslit
+    )
+    assert allclose(
+        s.get("radiance_noslit", Iunit="W/sr/cm2/nm")[1], new_radiance_noslit_wavelength
+    )
+
+
+if __name__ == "__main__":
+    test_equations()
diff --git a/radis/test/spectrum/test_operations.py b/radis/test/spectrum/test_operations.py
index 4bae6cddd..f7cceb4da 100644
--- a/radis/test/spectrum/test_operations.py
+++ b/radis/test/spectrum/test_operations.py
@@ -22,7 +22,7 @@ def test_crop(verbose=True, *args, **kwargs):
     # 1) A crop example in the same unit as the one stored
 
     # Work with a Spectrum object that was generated by Specair
-    s = load_spec(getTestFile("N2C_specair_380nm.spec"))
+    s = load_spec(getTestFile("N2C_specair_380nm.spec"), binary=False)
     # Focus on N2(C->B), v'=0, v''=2 band:
     s.crop(376, 381, "nm")
 
@@ -103,7 +103,7 @@ def test_operations_inplace(verbose=True, *args, **kwargs):
     # Multiply, make sure it worked
     I_max = s.get("radiance_noslit")[1].max()
     s *= 10
-    assert s.get("radiance_noslit")[1].max() == 10 * I_max
+    assert np.isclose(s.get("radiance_noslit")[1].max(), 10 * I_max)
     if verbose:
         print("test_operations: s *= 10: OK")
 
@@ -351,4 +351,10 @@ def _run_testcases(verbose=True, plot=False, *args, **kwargs):
 
 if __name__ == "__main__":
 
-    _run_testcases(plot=True)
+    from warnings import catch_warnings, filterwarnings
+
+    with catch_warnings():
+        filterwarnings("error")
+        _run_testcases(plot=True)
+
+    # _run_testcases(plot=True)
diff --git a/radis/test/spectrum/test_rescale.py b/radis/test/spectrum/test_rescale.py
index f3d52b504..90981e95f 100644
--- a/radis/test/spectrum/test_rescale.py
+++ b/radis/test/spectrum/test_rescale.py
@@ -481,13 +481,11 @@ def test_xsections(*args, **kwargs):
     # Get spectrum
     s = load_spec(getTestFile("CO_Tgas1500K_mole_fraction0.01.spec"), binary=True)
 
-    N = s.c["pressure_mbar"] * 1e2 / k_b / s.c["Tgas"] * 1e-6  # cm-3
-
     p = s.c["pressure_mbar"] * u("mbar")
-    k_b = k_b * u("J/K")
+    kb = k_b * u("J/K")
     T = s.c["Tgas"] * u("K")
 
-    N = p / k_b / T
+    N = p / kb / T
 
     assert s.c["pressure_mbar"] == 1013.25
     assert s.c["Tgas"] == 1500
@@ -497,7 +495,7 @@ def test_xsections(*args, **kwargs):
 
     N_x = N * 0.01
 
-    assert s.take("abscoeff").max() == s.take("xsection").max() * N_x
+    assert np.isclose(s.take("abscoeff").max(), s.take("xsection").max() * N_x)
 
     # Test again for x =1
 
diff --git a/radis/test/spectrum/test_spectrum_io.py b/radis/test/spectrum/test_spectrum_io.py
new file mode 100644
index 000000000..cd3e32e52
--- /dev/null
+++ b/radis/test/spectrum/test_spectrum_io.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""
+Test spectrum creation / export
+"""
+
+import pytest
+
+from radis import Spectrum, get_residual, test_spectrum
+
+
+@pytest.mark.fast
+def test_specutils_io(verbose=True, plot=False, *args, **kwargs):
+    """Test Radis/specutils  interface in particular when dealing with wavelengths
+
+    see https://github.com/radis/radis/pull/499
+    """
+
+    s = test_spectrum().take("transmittance_noslit")
+
+    processed_spec = Spectrum.from_array(
+        *s.get("transmittance_noslit", wunit="nm"),
+        "transmittance_noslit",
+        wunit="nm",
+        Iunit="",
+    )
+
+    with pytest.raises(ValueError) as err:
+
+        spec_u = processed_spec.to_specutils(
+            "transmittance_noslit", wunit="nm_air", Iunit="default"
+        )
+        assert (
+            "specutil's Spectrum1D will only handle wavelengths as seen in vacuum"
+            in str(err)
+        )
+
+    spec_u = processed_spec.to_specutils(
+        "transmittance_noslit", wunit="nm_vac", Iunit="default"
+    )
+
+    if plot:
+
+        import matplotlib.pyplot as plt
+
+        s.plot("transmittance_noslit", wunit="nm_vac", lw=5)
+        plt.plot(spec_u.wavelength.to("nm"), spec_u.flux, lw=3)
+        plt.xlim((4591.91, 4597.57))
+
+    assert spec_u.meta["waveunit"] == "nm_vac"  # was corrected properly
+    # Reverse
+
+    s2 = Spectrum.from_specutils(spec_u, "transmittance_noslit")
+
+    if plot:
+
+        s2.plot("transmittance_noslit", wunit="nm_vac", lw=1, nfig="same")
+
+    assert get_residual(s, s2, "transmittance_noslit", ignore_nan=True) < 1e-9
+
+
+if __name__ == "__main__":
+    test_specutils_io(plot=True)
diff --git a/radis/test/spectrum/test_utils.py b/radis/test/spectrum/test_utils.py
index e58b1ca8d..bf57d0c59 100644
--- a/radis/test/spectrum/test_utils.py
+++ b/radis/test/spectrum/test_utils.py
@@ -5,9 +5,9 @@
 @author: erwan
 """
 
-import os
-import os.path
-import time
+# import os
+# import os.path
+# import time
 
 
 def test_perf_profile(*args, **kwargs):
@@ -41,16 +41,19 @@ def test_perf_profile(*args, **kwargs):
                 calc_hwhm                        0.007s
                 generate_wavenumber_arrays       0.001s
                 calc_line_broadening             0.074s ██████
-                    precompute_LDM_lineshapes        0.012s
-                    LDM_Initialized_vectors          0.000s
-                    LDM_closest_matching_line        0.001s
-                    LDM_Distribute_lines             0.001s
-                    LDM_convolve                     0.060s █████
+                    precompute_DLM_lineshapes        0.012s
+                    DLM_Initialized_vectors          0.000s
+                    DLM_closest_matching_line        0.001s
+                    DLM_Distribute_lines             0.001s
+                    DLM_convolve                     0.060s █████
                     others                           0.001s
                 calc_other_spectral_quan         0.003s
                 generate_spectrum_obj            0.000s
                 others                           -0.016s
+
+    Generate a graphical, explorative perf profile (requires ``tuna`` installed)
     ::
+
         s.generate_perf_profile()
 
     See output of :py:meth:`~radis.spectrum.spectrum.Spectrum.generate_perf_profile`
@@ -79,17 +82,18 @@ def test_perf_profile(*args, **kwargs):
     # Test Console profiler
     s.print_perf_profile()
 
-    # Test interactive profiler
-    if os.path.exists("spectrum.prof"):
-        os.remove("spectrum.prof")
-    s.generate_perf_profile()
-    time.sleep(
-        5
-    )  # note @dev: would be better to wait for the subprocess to end; however it won't even if the window is closed
-    with open("spectrum.prof", "rb") as f:
-        assert "calculation_time" in str(f.read())
+    # EP 28/08/22 : starting from Radis 0.13.1 we do not test Tuna anymore.
+    # # Test interactive profiler
+    # if os.path.exists("spectrum.prof"):
+    #     os.remove("spectrum.prof")
+    # s.generate_perf_profile()
+    # time.sleep(
+    #     5
+    # )  # note @dev: would be better to wait for the subprocess to end; however it won't even if the window is closed
+    # with open("spectrum.prof", "rb") as f:
+    #     assert "calculation_time" in str(f.read())
 
-    os.remove("spectrum.prof")
+    # os.remove("spectrum.prof")
 
 
 def test_perf_profile_from_factory(*args, **kwargs):
@@ -112,17 +116,18 @@ def test_perf_profile_from_factory(*args, **kwargs):
     # Test Console profiler
     sf.print_perf_profile()
 
-    # Test interactive profiler
-    if os.path.exists("spectrum.prof"):
-        os.remove("spectrum.prof")
-    sf.generate_perf_profile()
-    time.sleep(
-        5
-    )  # note @dev: would be better to wait for the subprocess to end; however it won't even if the window is closed
-    with open("spectrum.prof", "rb") as f:
-        assert "calculation_time" in str(f.read())
-
-    os.remove("spectrum.prof")
+    # EP 28/08/22 : starting from Radis 0.13.1 we do not test Tuna anymore.
+    # # Test interactive profiler
+    # if os.path.exists("spectrum.prof"):
+    #     os.remove("spectrum.prof")
+    # sf.generate_perf_profile()
+    # time.sleep(
+    #     5
+    # )  # note @dev: would be better to wait for the subprocess to end; however it won't even if the window is closed
+    # with open("spectrum.prof", "rb") as f:
+    #     assert "calculation_time" in str(f.read())
+
+    # os.remove("spectrum.prof")
 
 
 if __name__ == "__main__":
diff --git a/radis/test/test_phys.py b/radis/test/test_phys.py
index 97961c2ad..17b8edbde 100644
--- a/radis/test/test_phys.py
+++ b/radis/test/test_phys.py
@@ -3,7 +3,7 @@
 Summary
 -------
 
-Runs tests for neq/phys so that they can be accessed by pytest (and hopefully
+Runs tests for radis/phys so that they can be accessed by pytest (and hopefully
 the CI test suite)
 
 Examples
@@ -80,7 +80,7 @@ def test_convert(verbose=True, *args, **kwargs):
 
     E = 250  # nm
     assert isclose(nm2eV(E), cm2eV(nm2cm(E)))
-    assert eV2nm(nm2eV(E)) == E
+    assert isclose(eV2nm(nm2eV(E)), E)
     assert hz2nm(nm2hz(E)) == E
 
     fwhm = 1.5  # nm