Repository: radis/radis (language: python)

Issue:
Exomol H2O failing on the 0.11.1  ('feather' mode)
### 🐛 Describe the bug
Hello, 
I cannot compute ExoMol spectra on the 0.11.1. I tried on spyder and on Radis lab. 
Sorry, I cannot spend more time on this. In holidays in an hour.

### 🖥 Steps To Reproduce
```python
from radis import calc_spectrum
import radis 

radis.config['MEMORY_MAPPING_ENGINE'] = 'feather'
s = calc_spectrum(2290, 2300,         # cm-1
                  molecule='H2O',
                  isotope='1',
                  pressure=1.01325,   # bar
                  Tgas=700,           # K
                  mole_fraction=0.1,
                  path_length=1,      # cm
                  databank='exomol',   # or use 'hitemp'
                  verbose=False
                  )
s.plot()    # plot the radiance by default 
```
And I got 
```python
Traceback (most recent call last):

  File "E:\Python\bug_radis\untitled0.py", line 12, in <module>
    s = calc_spectrum(2290, 2300,         # cm-1

  File "e:\python\radis\radis\lbl\calc.py", line 507, in calc_spectrum
    generated_spectrum = _calc_spectrum_one_molecule(

  File "e:\python\radis\radis\lbl\calc.py", line 720, in _calc_spectrum_one_molecule
    sf.fetch_databank(**conditions)

  File "e:\python\radis\radis\lbl\loader.py", line 1275, in fetch_databank
    df, local_path, Z_exomol = fetch_exomol(

  File "e:\python\radis\radis\io\exomol.py", line 362, in fetch_exomol
    mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)

  File "e:\python\radis\radis\io\exomol.py", line 764, in __init__
    Q=self.QTref,

AttributeError: 'MdbExomol' object has no attribute 'QTref'
```

### 🖥 Steps To Reproduce on Radis-lab
```python
%matplotlib inline
from radis import calc_spectrum
s = calc_spectrum(2290, 2300,         # cm-1
                  molecule='H2O',
                  isotope='1',
                  pressure=1.01325,   # bar
                  Tgas=700,           # K
                  mole_fraction=0.1,
                  path_length=1,      # cm
                  databank='exomol',   # or use 'hitemp'
                  verbose=False
                  )
s.plot()
```

```python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_291/3219838641.py in <module>
      1 get_ipython().run_line_magic('matplotlib', 'inline')
      2 from radis import calc_spectrum
----> 3 s = calc_spectrum(2290, 2300,         # cm-1
      4                   molecule='H2O',
      5                   isotope='1',

/srv/conda/envs/notebook/lib/python3.8/site-packages/radis/lbl/calc.py in calc_spectrum(wmin, wmax, wunit, Tgas, Tvib, Trot, pressure, molecule, isotope, mole_fraction, path_length, databank, medium, wstep, truncation, neighbour_lines, cutoff, parsum_mode, optimization, broadening_method, overpopulation, name, save_to, use_cached, mode, export_lines, verbose, return_factory, **kwargs)
    505         kwargs_molecule.update(**dict_arguments)
    506 
--> 507         generated_spectrum = _calc_spectrum_one_molecule(
    508             wavenum_min=wavenum_min,
    509             wavenum_max=wavenum_max,

/srv/conda/envs/notebook/lib/python3.8/site-packages/radis/lbl/calc.py in _calc_spectrum_one_molecule(wavenum_min, wavenum_max, Tgas, Tvib, Trot, pressure, overpopulation, molecule, isotope, mole_fraction, path_length, databank, medium, wstep, truncation, neighbour_lines, cutoff, parsum_mode, optimization, broadening_method, name, use_cached, verbose, mode, export_lines, return_factory, **kwargs)
    718         conditions["parse_local_global_quanta"] = (not _equilibrium) or export_lines
    719         # Finally, LOAD :
--> 720         sf.fetch_databank(**conditions)
    721     elif exists(databank):
    722         conditions = {

/srv/conda/envs/notebook/lib/python3.8/site-packages/radis/lbl/loader.py in fetch_databank(self, source, database, parfunc, parfuncfmt, levels, levelsfmt, load_energies, include_neighbouring_lines, parse_local_global_quanta, drop_non_numeric, db_use_cached, lvl_use_cached, memory_mapping_engine, load_columns, parallel)
   1273             }  # partition function tabulators for all isotpes
   1274             for iso in isotope_list:
-> 1275                 df, local_path, Z_exomol = fetch_exomol(
   1276                     molecule,
   1277                     database=database,

/srv/conda/envs/notebook/lib/python3.8/site-packages/radis/io/exomol.py 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)
    360     if load_wavenum_max is None:
    361         load_wavenum_max = np.inf
--> 362     mdb = MdbExomol(local_path, [load_wavenum_min, load_wavenum_max], engine=engine)
    363 
    364     # Attributes of the DataFrame

/srv/conda/envs/notebook/lib/python3.8/site-packages/radis/io/exomol.py in __init__(self, path, nurange, margin, crit, bkgdatm, broadf, engine, verbose)
    745                     if (
    746                         engine == "vaex"
--> 747                         and self.trans_file.with_suffix(".hdf5").exists()
    748                     ):
    749                         # if engine == 'feather' we recompute all the time

AttributeError: 'list' object has no attribute 'with_suffix'
```
major error in sparse algorithm if lines were not sorted
Sparse algorithm introduced in #386 , #388  [Radis 0.11](https://github.com/radis/radis/pull/400)  introduced an error in cases where the line database was not sorted in wavenumber. 

This happened in real-life scenario when : 
- downloading from HITRAN , where each isotope is downloaded one after another 
- and large spectral ranges where the sparse-mode is activated 
Typically, CO spectra. 

- or computing from ExoMol with multiple isotopes. 

No problems for HITEMP  calculations (all isotopes are stored together and already sorted). 

Fix : 

- [x] assert database is always sorted at runtime
- [x] sort lines after fetch_databank
- [x] sort lines when downloading HITRAN with vaex


Lines missing in 0.11, fixed in 0.12 : 

![image](https://user-images.githubusercontent.com/16088743/146694521-70354c37-6a67-4a42-8e13-ad8eb4887044.png)



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 7f0f9e28f..7a3b8f2f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,7 +27,7 @@ __pycache__/
 *.py[cod]
 *.pyc
 *.c
-*.cpp
+radis/cython/*.cpp
 
 # IPython notebook checkpoints
 *-checkpoint.ipynb
@@ -40,6 +40,7 @@ __pycache__/
 env/
 build/
 develop-eggs/
+develop-tests/
 dist/
 eggs/
 lib/
diff --git a/.travis.yml b/.travis.yml
index 296dfad65..3daccaf56 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: python
 python:
   # We don't actually use the Travis Python, but this keeps it organized.
   #- "2.7"    # not tested anymore starting from 0.9.19
-  - "3.7"
+  - "3.8"
 os: linux
 
 stages:
@@ -38,8 +38,9 @@ jobs:
         - conda env create -n radis-env -f environment.yml
         # Activate and install test-only dependencies
         - source activate radis-env
+        - conda list
         - pip install pytest-cov
-        - python setup.py install
+        - pip install -e . -v
 
       script:
         # Your test script goes here
diff --git a/MANIFEST.in b/MANIFEST.in
index bed9c2985..80372e7f3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,8 @@
 include README.rst
 include LICENSE
 include radis/__version__.txt
+include radis/lbl/gpu.cpp
+include radis/lbl/gpu.h
 include radis/phys/units.txt
 include radis/test/files/*.txt
 include radis/test/files/*.spec
diff --git a/README.rst b/README.rst
index 984cc2741..4bad0ba3c 100644
--- a/README.rst
+++ b/README.rst
@@ -93,6 +93,39 @@ Typical output of `plot_diff <https://radis.readthedocs.io/en/latest/source/radi
 Refer to the `Examples <https://radis.readthedocs.io/en/latest/examples/examples.html>`__ section for more examples, and to the
 `Spectrum page <https://radis.readthedocs.io/en/latest/spectrum/spectrum.html>`__ for more post-processing functions.
 
+
+GPU Acceleration
+----------------
+
+RADIS supports GPU acceleration for super-fast computation of spectra:
+
+    from radis import SpectrumFactory
+    from radis.tools.plot_tools import ParamRange
+
+    sf = SpectrumFactory(
+        2100,
+        2400,  # cm-1
+        molecule="CO2",
+        isotope="1,2,3",
+        wstep=0.002,
+    )
+
+    sf.fetch_databank("hitemp")
+
+    s = sf.eq_spectrum_gpu_interactive(
+        var="radiance",
+        Tgas=ParamRange(300.0, 2500.0, 1100.0),  # K
+        pressure=ParamRange(0.1, 2, 1),  # bar
+        mole_fraction=ParamRange(0, 1, 0.8),
+        path_length=ParamRange(0, 1, 0.2),  # cm
+        slit_FWHM=ParamRange(0, 1.5, 0.24),  # cm-1
+        emulate=False,  # runs on GPU
+        plotkwargs={"nfig": "same", "wunit": "nm"},
+    )
+
+.. image:: https://raw.githubusercontent.com/dcmvdbekerom/radis/gpu_widget_new_branch/docs/examples/GPU_spectrum.png
+
+Refer to :ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>` for more details on GPU acceleration.
 ---------------------------------------------------------------------
 
 =======================================
diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md
index 9d2f7b9a6..9e80b7884 100644
--- a/docs/PULL_REQUEST_TEMPLATE.md
+++ b/docs/PULL_REQUEST_TEMPLATE.md
@@ -11,4 +11,4 @@ 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. -->
 
-Fixes #<Issue Number>
\ No newline at end of file
+Fixes #<Issue Number>
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index be9b833d4..1e250a9b2 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -13,4 +13,4 @@
 
   </script>
 
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/docs/_templates/searchbox.html b/docs/_templates/searchbox.html
new file mode 100644
index 000000000..daf0a1cfb
--- /dev/null
+++ b/docs/_templates/searchbox.html
@@ -0,0 +1,15 @@
+<div>
+   <h3>{{ _('Quick search') }}</h3>
+   <script>
+      (function() {
+         var cx = '011868766723427137892:jpmxeesvyix';
+         var gcse = document.createElement('script');
+         gcse.type = 'text/javascript';
+         gcse.async = true;
+         gcse.src = 'https://cse.google.com/cse.js?cx=' + cx;
+         var s = document.getElementsByTagName('script')[0];
+         s.parentNode.insertBefore(gcse, s);
+      })();
+   </script>
+  <gcse:search></gcse:search>
+</div>
diff --git a/docs/conf.py b/docs/conf.py
index d01b8618d..83a370f45 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -84,7 +84,7 @@
 
 
 def linkcode_resolve(domain, info):
-    """ for sphinx.ext.linkcode"""
+    """for sphinx.ext.linkcode"""
     if domain != "py":
         return None
     if not info["module"]:
diff --git a/docs/dev/_architecture.rst b/docs/dev/_architecture.rst
index af6530567..ede899355 100644
--- a/docs/dev/_architecture.rst
+++ b/docs/dev/_architecture.rst
@@ -38,7 +38,7 @@ are the input parameters of the different RADIS front-ends:
 
 -------------------------------------------------------------------------
 
-The Input databases are either automatically downloaded from [HITRAN-2016]_, or defined
+The Input databases are either automatically downloaded from [HITRAN-2020]_, or defined
 locally in a :ref:`Configuration file <label_lbl_config_file>`
 
 -------------------------------------------------------------------------
diff --git a/docs/examples/GPU_spectrum.png b/docs/examples/GPU_spectrum.png
new file mode 100644
index 000000000..a62bf9e82
Binary files /dev/null and b/docs/examples/GPU_spectrum.png differ
diff --git a/docs/features/description.rst b/docs/features/description.rst
index f4c65c562..205b9c0a3 100644
--- a/docs/features/description.rst
+++ b/docs/features/description.rst
@@ -1,7 +1,7 @@
 Description
 ===========
 
-Written as a general purpose radiative solver, the code is built around the [HITRAN-2016]_,
+Written as a general purpose radiative solver, the code is built around the [HITRAN-2020]_,
 [HITEMP-2010]_ and [CDSD-4000]_ databases for molecules in their electronic ground state. Energy
 levels are read from tabulated databases or calculated from Dunham developments.
 Boltzmann, Treanor, and state specific vibrational distributions can be generated.
diff --git a/docs/features/features.rst b/docs/features/features.rst
index 249722f94..a3d37373d 100644
--- a/docs/features/features.rst
+++ b/docs/features/features.rst
@@ -13,9 +13,9 @@ RADIS is both an infrared :ref:`line-by-line code <label_line_by_line>`
 and a :ref:`post-processing library <label_spectrum>`.
 It includes:
 
-- Absorption and emission spectra of all [HITRAN-2016]_ and [ExoMol-2020]_ species under equilibrium calculations (:py:data:`~radis.db.MOLECULES_LIST_EQUILIBRIUM`)
+- Absorption and emission spectra of all [HITRAN-2020]_ and [ExoMol-2020]_ species under equilibrium calculations (:py:data:`~radis.db.MOLECULES_LIST_EQUILIBRIUM`)
 - Absorption and emission spectra of CO2 and CO for non-LTE calculations (see :py:data:`~radis.db.MOLECULES_LIST_NONEQUILIBRIUM` )
-- Different Line Databases: support of [HITRAN-2016]_, [HITEMP-2010]_ and [CDSD-4000]_ line databases (see :py:data:`~radis.lbl.loader.KNOWN_DBFORMAT`)
+- Different Line Databases: support of [HITRAN-2020]_, [HITEMP-2010]_ and [CDSD-4000]_ line databases (see :py:data:`~radis.lbl.loader.KNOWN_DBFORMAT`)
 - Calculation of :ref:`Rovibrational Energies of molecules <label_examples_rovibrational_energies>`.
 - Calculation of equilibrium and nonequilibrium :ref:`Partition Functions <label_examples_partition_functions>`.
 - Spatially heterogeneous spectra (see :ref:`see line-of-sight <label_los_index>`)
@@ -90,6 +90,7 @@ Line Databases
 List of supported line databases formats: :py:data:`~radis.lbl.loader.KNOWN_DBFORMAT` :
 
 - [HITRAN-2016]_
+- [HITRAN-2020]_
 - [HITEMP-2010]_
 - [CDSD-4000]_
 - [ExoMol-2020]_
diff --git a/docs/index.rst b/docs/index.rst
index 628dd117c..8b49c5654 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -37,7 +37,7 @@ Quick Start
 ===========
 
 
-Calculate a CO equilibrium spectrum from the [HITRAN-2016]_ database, using the
+Calculate a CO equilibrium spectrum from the [HITRAN-2020]_ database, using the
 :py:func:`~radis.lbl.calc.calc_spectrum` function. Lines are downloaded automatically
 using Astroquery (based on [HAPI]_). Output is a
 :ref:`Spectrum object <label_spectrum>`: ::
@@ -102,7 +102,7 @@ More examples
 .. minigallery:: radis.Spectrum
 
 
-The Quick Start examples automatically downloaded the line databases from [HITRAN-2016]_, which is valid for temperatures below 700 K.
+The Quick Start examples automatically downloaded the line databases from [HITRAN-2020]_, which is valid for temperatures below 700 K.
 For *high temperature* cases, you may need to use :ref:`other line databases <label_line_databases>` such as
 [HITEMP-2010]_ (typically T < 2000 K) or [CDSD-4000]_ (T < 5000 K). These databases must be described in a ``~/radis.json``
 :ref:`Configuration file <label_lbl_config_file>`.
diff --git a/docs/lbl/_databases.rst b/docs/lbl/_databases.rst
index 74d10c9b7..d35eef032 100644
--- a/docs/lbl/_databases.rst
+++ b/docs/lbl/_databases.rst
@@ -17,7 +17,7 @@ Refer to :py:func:`~radis.io.query.fetch_astroquery` for more information.
 You can also download the HITRAN databases files locally:
 
 - HITRAN can be downloaded from https://hitran.org/lbl/. Expect
-  ~80 Mb for CO2, or 50 Mb for H2O. Cite with [HITRAN-2016]_.
+  ~80 Mb for CO2, or 50 Mb for H2O. Cite with [HITRAN-2020]_.
 
 .. note::
 
diff --git a/docs/lbl/gpu.rst b/docs/lbl/gpu.rst
index 89ec69194..a8c73a37b 100644
--- a/docs/lbl/gpu.rst
+++ b/docs/lbl/gpu.rst
@@ -4,239 +4,86 @@
 RADIS-GPU Spectrum Calculation
 ==============================
 
-This page is meant to serve as the technical documentation and provide examples for RADIS GPU spectrum calculation.
-The contents of the page have been listed below:
-
-0. Introduction
-1. Download, install and setup the dependencies.
-2. How to calculate the spectrum on GPU?
-3. Performance tests.
-4. Behind the scenes: how does the GPU calculation actually work?
-
-INTRODUCTION
-------------
-
-The GPU powered spectrum calculation that takes place in RADIS happens in a three step process as listed below:
-1. The first step includes the user providing the necessary parameters and data needed for the spectrum calculation.
-This is done with the pre-existing RADIS interface with the assistance of new input parameters and methods.
-
-2. The second step involves preprocessing as well as certain steps that are not a performance bottleneck, which are implemented
-using Cython. This allows RADIS to compile the code prior to its use, which leads to siginificant performance boost. This
-part of the code also serves as the host side, launching the necessary CUDA kernels required to compute the spectra.
-
-3. The third and final step is the actual CUDA code itself. It has been written in CuPy, embedded in a Cython/Python host code.
-This part of the code is responsible for executing the bottleneck we see in traditional synthetic spectra calculation,
-the broadening step. Apart from that, the fourier transforms are also performed on the GPU.
-
-1. Downloading, installing and setting up the dependencies.
------------------------------------------------------------
-
-Now that we are familiar with the general outline of how GPU powered spectrum calculations are done on RADIS, let us move
-on to trying it out ourselves. Following the roadmap given in the introduction, we see that in order to use RADIS+GPU,
-extra dependencies are introduced in the second and third step.
-
-The second step begins with a Cython file known as `py_cuffs.pyx` that is located in `radis/lbl/py_cuffs`. This file,
-which is written in Cython, contains the code that sets up the parameters, copies the necessary data to the GPU memory,
-and executes certain parts of the spectrum calculation process which are not bottlenecks or affect performance in any significant
-way. The way the implementation has been done, an instruction passed by the user to calculate spectrum on the GPU would first
-load the dataset if it isn't present in the memory already. After that, RADIS tries to import the `py_cuffs` module,
-which is obtained after the compilation of the Cython source file (`py_cuffs.pyx`). In order to ensure that the compilation
-takes place properly, the user needs to ensure that they have Cython installed on their system. Right now, RADIS does not
-mandate Cython as a necessary dependency, so users will have to personally make sure that Cython is present on their system.
-To do so, please follow the steps given on the Cython documentation (https://cython.readthedocs.io/en/latest/src/quickstart/install.html).
-
-Once the Cython installation is complete, the users should now be able to load the `py_cuffs` module in RADIS whenever a
-GPU call is made. While RADIS automatically compiles the Cython file into the required module, users can manually compile
-the Cython file as well by following these steps:
-
-1. Change your working directory to `radis/lbl/py_cuffs`.
-2. There should be atleast 2 files located in the folder, namely `setup.py` and `py_cuffs.pyx`. If the Cython file has
-been compiled already, users may see other files/folders present in the folder as well. In this case, they may simply skip
-the manual compilation. However, if a fresh compilation is needed, delete all the files and folders present except `setup.py`
-and `py_cuffs.pyx`.
-3. Execute the following commands in terminal: `python setup.py build_ext --inplace`
-4. The Cython file should be compiled, and a copy of the compiled binary will be saved in the `py_cuffs` folder.
-
-If you would like to use this compiled binary file directly in Python for some reason, you can do so by starting Python
-in the same directory where the compiled binary is present, and type `import py_cuffs` in Python REPL/file to import the
-module. Note that the compiled binary file will not be named `py_cuffs`, but will have a longer name instead which will
-contain information about the system for which it has been compiled. However, to import the binary, users should still
-type `import py_cuffs` and not the file name in system. For example, in Linux x64 systems, the compiled binary file will
-be stored as `py_cuffs.cpython-36m-x86_64-linux-gnu.so`.
-
-Once Cython has been installed and is working properly, users can focus on the third step of the GPU spectrum calculation
-process, which is the actual GPU kernel code. In order to run this part of the code, users need to ensure the following:
-
-1. They have a system with an Nvidia CUDA GPU with compute capability >= 3.0.
-2. They have CUDA Toolkit version >=8.0 installed on their system.
-3. Python >= 3.5 installed on their system.
-4. Appropriate version of CuPy depending on their CUDA Toolkit version installed on their system.
-
-Let us see how the users can check whether they satisfy each of the above requirements:
-
-1. In order to check if your system has an Nvidia GPU installed, follow the steps on this link given at the bottom of the
-page: https://developer.nvidia.com/cuda-gpus. If you are using Linux systems, enter the following command in your terminal
-and check if the results contain an Nvidia device: `lspci -nn | grep '\[03'`. This will return results similar to the following:
-pip install cupy-cuda102
-```
-00:02.0 VGA compatible controller [0300]: Intel Corporation Device [8086:3e9b] (rev 02)
-01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:1f91] (rev a1)
-```
-
-You can find out more information about your GPU (like its model name) by searching for its PCI ID (in the above case,
-[10de:1f91]) on the internet.
-
-If you do not have an Nvidia device installed, then this documentation is not for you and you can stop reading now.
-
-Once you have ensured that your system has an Nvidia GPU installed, make sure that the device has compute capability
->= 3.0. This should not be an issue unless you possess a really old system. To check your device's compute capability,
-again follow the same link given above and expand the section to which your GPU belongs. Look for your GPU model name in
-the table and ensure your device has CC >= 3.0.
-
-2. Now that it is confirmed that the system has a GPU which supports RADIS, install the CUDA Toolkit for your GPU if
-isn't already installed. Note that the CUDA Toolkit is different from the Nvidia drivers that you install. CUDA Toolkit is
-entirely different from the Geforce drivers that the operating system often installs by itself, and are meant for CUDA
-development instead. To check if you have CUDA Toolkit installed, or to install the CUDA Toolkit, follow the documentation
-given on the Nvidia site here: https://docs.nvidia.com/cuda/
-
-3. Once the CUDA Toolkit has been installed (it might take some time to finish the installation process), ensure that the
-Python version installed on your system is atleast 3.5. If not, either create a new virtual environment or install the
-updated Python version globally.
-
-4. Once these prerequisites have been installed properly, install CuPy following the documentation given on this page:
-https://docs.cupy.dev/en/stable/install.html#install-cupy. IMPORTANT: Ensure that you install the PRE-RELEASE version
-of CuPy. RADIS GPU methods make use of constant memory which is currently supported only in the prelease version of CuPy.
-If you install the latest release, the GPU code WILL NOT WORK. To ensure you're installing the prerelease version
-using pip, just add the flag `--pre` at the end of your command. For instance, if you have CUDA version 10.2 and want to
-install CuPy, enter the following command `pip install cupy-cuda102 --pre`.
-
-With this, all the necessary dependencies for calculating RADIS spectra on GPU should be installed on the system. Move on
-to the examples section to see how to use your GPU to calculate spectra with RADIS (very) efficiently!
-
-2. How to calculate the spectrum on GPU?
-----------------------------------------
-
-RADIS supports calculation of spectra at thermal equilibrium using GPU for the
-calculation of lineshapes and broadening. If your system supports it, the spectrum
-can be calculated on the GPU using the :py:func:`~radis.lbl.calc.calc_spectrum`
-function with parameter `mode` set to `gpu`.
-
-GPU-enabled spectrum calculations can be done using either the standard RADIS
-databank loader or using databank that has been preprocessed and saved in numpy
-array (`npy`) format. In case the standard loader is used for loading the data
-for GPU-powered spectrum calculation, some preprocessing is done on that data
-before the spectrum calculation begins.
-# TODO: perform timing test to see how much time calculating log_2gs separately takes
-
-Currently, GPU-powered spectra calculations are supported only at thermal equilibrium
-and therefore, the method to calculate the spectra has been named :py:func:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`.
-In order to use this method to calculate the spectra, follow the same steps as in the
-case of a normal equilibrium spectra, and if using :py:func:`~radis.lbl.calc.calc_spectrum`
-function set the parameter `mode` to `gpu`, or use :py:func:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`
-
-Consider the following example which demonstrates the above information::
-
-    from radis import SpectrumFactory
-    from radis.test.utils import getTestFile
-    T = 1000
-    p = 0.1
-    wstep = 0.001
-    wmin = 2200  # cm-1
-    wmax = 2400  # cm-1
+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.
+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.
+
+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:`~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.
+
+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.
+
+
+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:
+
     sf = SpectrumFactory(
-            wavenum_min=wmin,
-            wavenum_max=wmax,
-            mole_fraction=1,
-            path_length=1,  # doesnt change anything
-            wstep=wstep,
-            pressure=p,
-            isotope="1",
-            chunksize="DLM"
-        )
-    sf.load_databank(getTestFile("cdsd_hitemp_09_fragment.txt"), format="cdsd-hitemp", parfuncfmt="hapi")
-    s_gpu = sf.eq_spectrum_gpu(Tgas=T)
-
-Alternatively, one could compute the spectra with the assistance of GPU using the
-following code as well ::
-
-    s = calc_spectrum(
-        	wavenum_min=1900,
-        	wavenum_max=2300,
-        	Tgas=700,
-        	path_length=0.1,
-        	mole_fraction=0.01,
-        	isotope=1,
-        	mode='gpu'
-    		)
-
-As mentioned previously, the GPU-enabled spectrum calculations can also be done
-using databank that has been preprocessed and saved in numpy's `npy` format.
-
-In order to calculate the data using the `npy` files, first place all the 7 files in the
-same directory. Then, set the `databank` parameter of :py:func:`~radis.lbl.calc.calc_spectrum`
-to point to one of the 7 files in the directory. The program will automatically detect and read the
-other files present in the same folder ::
-
-      s = calc_spectrum(
-        	wavenum_min=1900,
-        	wavenum_max=2300,
-        	Tgas=700,
-        	path_length=0.1,
-        	databank='/path/to/v0.npy',
-        	mole_fraction=0.01,
-        	isotope=1,
-        	mode='gpu'
-    		)
-
-## TODO: Once the npy2df implementation is complete, also mention about loading the npy files
-from different directories by passing a dictionary instead
-
-The `npy` files that are needed for calculating the spectra on GPU can be extracted
-from any databank and stored in the following format. The name of the file is written
-first, followed by the physical quantity it stores and it's name and position in the
-CDSD-4000 database.
-
-`v0.npy`: wavenumber in vacuum; `v0`, line[3:15]
-`da.npy`: air-pressure induced shift; `d_air`, line[59:67]
-`El.npy`: low-state energy; `Elow`, line[45:55]
-`na.npy`: temperature dependence exponent for air; `n_air`, line[55:59]
-
-In addition to the above 4 quantities, we also need 3 more quantities which are not
-directly stored in the databank. They are explained below:
-
-`log_2gs.npy`: np.log(2*gs), where `gs` is HITRAN/HITEMP HWHM pressure broadening constant
-for self-broadening; `gamma_self`, line[40:45]
-`log_2vMm.npy`: np.log(2*v0) + 0.5*np.log(2*k*np.log(2)/(c**2*Mm)), where `v0` is the
-wavenumber in vacuum, `k` is Boltzmann's constant, `c` is speed of light in vacuum
-and `Mm` is the molecular mass of gas molecule in kilogram.
-`S0.npy`: f_ab * gu * A21 / (8*pi*c_cm*v0**2) where,
-`f_ab`: np.array([ 0.98420, 0.01106, 0.0039471])[iso.astype(int)-1],
-`gu`: 2*Ju + 1, where
-`Ju` = Jl + DJ, where
-`DJ` = ord(line[117:118])-ord('Q')
-`Jl` = int(line[118:121])
-`A21` is the Einstein's coefficient, line[25:35]
-`c_cm` is speed of light in vacuum in centimeters/second,
-`v0` is wavenumber in vacuum.
-
-In order to facililate the conversion of data from the CDSD-4000 par format to the format explained
-above, users can use the scripts present in `/radis/misc/prepare-npy-data`.
-
-`par2npy.py` extracts the relevant information from the dataset files and stores them in `npy` files
-where each file contains all the information for multiple lines.
-
-`reshape_arrays.py` extracts and separates the different fields for each line, and saves the values of
-a specific field for all the lines in a separate file as explained above, e.g. `v0.npy`, 'da.npy`, etc.
-
-3. Performance tests
+        2100,
+        2400,  # cm-1
+        molecule="CO2",
+        isotope="1,2,3",
+        wstep=0.002,
+    )
+
+    sf.fetch_databank("hitemp")
+
+    s = sf.eq_spectrum_gpu(
+        Tgas=1100.0,  # K
+        pressure=1,  # bar
+        mole_fraction=0.8,
+        path_length=0.2,  # cm
+        emulate=False,  # runs on GPU
+    )
+
+    s.apply_slit(0.5)#cm-1
+    s.plot("radiance", show=True)
+
+
+Interactive Spectrum
 --------------------
 
-We have created a jupyter notebook to highlight and contrast the performance difference that a user can
-achieve between the CPU and GPU versions of RADIS which can be found here: https://github.com/radis/radis-benchmark.
+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(
+        2100,
+        2400,  # cm-1
+        molecule="CO2",
+        isotope="1,2,3",
+        wstep=0.002,
+    )
+
+    sf.fetch_databank("hitemp")
+
+    s = sf.eq_spectrum_gpu_interactive(
+        var="radiance",
+        Tgas=ParamRange(300.0, 2500.0, 1100.0),  # K
+        pressure=ParamRange(0.1, 2, 1),  # bar
+        mole_fraction=ParamRange(0, 1, 0.8),
+        path_length=ParamRange(0, 1, 0.2),  # cm
+        slit_FWHM=ParamRange(0, 1.5, 0.24),  # cm-1
+        emulate=False,  # runs on GPU
+        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.
+
+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.
 
-4. Behind the scenes: how does the GPU calculation actually work?
------------------------------------------------------------------
 
-#TODO: Maybe talk about the DLM matrix and explain more of the mathematics that happens in the GPU.
-Maybe share a link to the DLM paper? Or maybe get rid of this section entirely? To be decided.
 
diff --git a/docs/lbl/lbl.rst b/docs/lbl/lbl.rst
index 28d9fffd2..e2e5a7f1c 100644
--- a/docs/lbl/lbl.rst
+++ b/docs/lbl/lbl.rst
@@ -172,7 +172,7 @@ provides an interace to [CANTERA]_ directly from RADIS ::
 Nonequilibrium Calculations
 ---------------------------
 
-Nonequilibrium calculations (multiple temperatures) require to know the
+Non-LTE calculations (multiple temperatures) require to know the
 vibrational and rotational energies of each
 level in order to calculate the nonequilibrium populations.
 
@@ -193,17 +193,12 @@ Fit a Spectrum
 Calculating spectrum using GPU
 ------------------------------
 
-RADIS supports calculation of spectra at thermal equilibrium using GPU for the
-calculation of lineshapes and broadening. If your system supports it, the spectrum
-can be calculated on the GPU using the :py:func:`~radis.lbl.calc.calc_spectrum`
-function with parameter `mode` set to `gpu`.
-
-GPU-enabled spectrum calculations can be done using either the standard RADIS
-databank loader or using databank that has been preprocessed and saved in numpy
-array (`npy`) format. In case the standard loader is used for loading the data
-for GPU-powered spectrum calculation, some preprocessing is done on that data
-before the spectrum calculation begins.
-# TODO: perform timing test to see how much time calculating log_2gs separately takes
+RADIS also supports CUDA-native parallel computation, specifically
+for lineshape calculation and broadening. To use these GPU-accelerated methods to compute the spectra, use either :py:func:`~radis.lbl.calc.calc_spectrum`
+function with parameter `mode` set to `gpu`, or :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`. In order to use these methods,
+ensure that your system has an Nvidia GPU with compute capability of atleast 3.0 and CUDA Toolkit 8.0 or above. Refer to
+:ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>` to see how to setup your system to run GPU accelerated spectrum
+calculation methods, examples and performance tests.
 
 Currently, GPU-powered spectra calculations are supported only at thermal equilibrium
 and therefore, the method to calculate the spectra has been named :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`.
@@ -211,29 +206,7 @@ In order to use this method to calculate the spectra, follow the same steps as i
 case of a normal equilibrium spectra, and if using :py:func:`~radis.lbl.calc.calc_spectrum`
 function set the parameter `mode` to `gpu`, or use :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`
 
-Consider the following example which demonstrates the above information::
-
-    from radis import SpectrumFactory
-    from radis.test.utils import getTestFile
-    T = 1000
-    p = 0.1
-    wstep = 0.001
-    wmin = 2200  # cm-1
-    wmax = 2400  # cm-1
-    sf = SpectrumFactory(
-            wavenum_min=wmin,
-            wavenum_max=wmax,
-            mole_fraction=1,
-            path_length=1,  # doesnt change anything
-            wstep=wstep,
-            pressure=p,
-            isotope="1",
-            chunksize="DLM"
-        )
-    sf.load_databank(getTestFile("cdsd_hitemp_09_fragment.txt"), format="cdsd-hitemp", parfuncfmt="hapi")
-    s_gpu = sf.eq_spectrum_gpu(Tgas=T)
-
-Alternatively, one could compute the spectra with the assistance of GPU using the
+One could compute the spectra with the assistance of GPU using the
 following code as well ::
 
     s = calc_spectrum(
@@ -246,64 +219,7 @@ following code as well ::
         	mode='gpu'
     		)
 
-As mentioned previously, the GPU-enabled spectrum calculations can also be done
-using databank that has been preprocessed and saved in numpy's `npy` format.
-
-In order to calculate the data using the `npy` files, first place all the 7 files in the
-same directory. Then, set the `databank` parameter of :py:func:`~radis.lbl.calc.calc_spectrum`
-to point to one of the 7 files in the directory. The program will automatically detect and read the
-other files present in the same folder ::
-
-      s = calc_spectrum(
-        	wavenum_min=1900,
-        	wavenum_max=2300,
-        	Tgas=700,
-        	path_length=0.1,
-        	databank='/path/to/v0.npy',
-        	mole_fraction=0.01,
-        	isotope=1,
-        	mode='gpu'
-    		)
-
-## TODO: Once the npy2df implementation is complete, also mention about loading the npy files
-from different directories by passing a dictionary instead
-
-The `npy` files that are needed for calculating the spectra on GPU can be extracted
-from any databank and stored in the following format. The name of the file is written
-first, followed by the physical quantity it stores and it's name and position in the
-CDSD-4000 database.
-
-`v0.npy`: wavenumber in vacuum; `v0`, line[3:15]
-`da.npy`: air-pressure induced shift; `d_air`, line[59:67]
-`El.npy`: low-state energy; `Elow`, line[45:55]
-`na.npy`: temperature dependence exponent for air; `n_air`, line[55:59]
-
-In addition to the above 4 quantities, we also need 3 more quantities which are not
-directly stored in the databank. They are explained below:
-
-`log_2gs.npy`: np.log(2*gs), where `gs` is HITRAN/HITEMP HWHM pressure broadening constant
-for self-broadening; `gamma_self`, line[40:45]
-`log_2vMm.npy`: np.log(2*v0) + 0.5*np.log(2*k*np.log(2)/(c**2*Mm)), where `v0` is the
-wavenumber in vacuum, `k` is Boltzmann's constant, `c` is speed of light in vacuum
-and `Mm` is the molecular mass of gas molecule in kilogram.
-`S0.npy`: f_ab * gu * A21 / (8*pi*c_cm*v0**2) where,
-`f_ab`: np.array([ 0.98420, 0.01106, 0.0039471])[iso.astype(int)-1],
-`gu`: 2*Ju + 1, where
-`Ju` = Jl + DJ, where
-`DJ` = ord(line[117:118])-ord('Q')
-`Jl` = int(line[118:121])
-`A21` is the Einstein's coefficient, line[25:35]
-`c_cm` is speed of light in vacuum in centimeters/second,
-`v0` is wavenumber in vacuum.
-
-In order to facililate the conversion of data from the CDSD-4000 par format to the format explained
-above, users can use the scripts present in `/radis/misc/prepare-npy-data`.
-
-`par2npy.py` extracts the relevant information from the dataset files and stores them in `npy` files
-where each file contains all the information for multiple lines.
-
-`reshape_arrays.py` extracts and separates the different fields for each line, and saves the values of
-a specific field for all the lines in a separate file as explained above, e.g. `v0.npy`, 'da.npy`, etc.
+Refer to :ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>` for more details.
 
 
 Under the hood
@@ -380,14 +296,28 @@ Configuration file
 ------------------
 
 The ``~/radis.json`` configuration file is used to store the list and attributes of the Line databases
-available on your computer.
+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 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
+``default_radis.json``.
+
+.. note::
+    You can also update config parameters at runtime by setting::
+
+        import radis
+        radis.config["SOME_KEY"] = "SOME_VALUE"
+
+    Although it is recommended to simply edit your ``~/radis.json`` file.
+
 
 Without a configuration file, you can still:
 
-- download the corresponding [HITRAN-2016]_ line database automatically,
+- 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:
@@ -396,13 +326,23 @@ A configuration file will help to:
 - 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
+is ``~/.radisdb``. You can change this at runtime by setting the ``radis.config["DEFAULT_DOWNLOAD_PATH"]``
+key, or (recommended) by adding a ``DEFAULT_DOWNLOAD_PATH`` key in your ``~/radis.json``
+configuration file.
+
+
 .. 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
     reason you cannot create a configuration file.
 
-A ``~/radis.json`` is user-dependant, and machine-dependant. It contains a list of database, everyone of which
+
+
+A ``~/radis.json`` is user-dependant, and machine-dependant. It contains a list of database, each of which
 is specific to a given molecule. It typically looks like::
 
 str: Typical expected format of a ~/radis.json entry::
@@ -530,7 +470,7 @@ The full description of a `~/radis.json` entry is given in :py:data:`~radis.misc
 *How to create the configuration file?*
 
 A default ``~/radis.json`` configuration file can be generated with :py:func:`~radis.test.utils.setup_test_line_databases`, which
-creates two test databases from fragments of [HITRAN-2016]_ line databases::
+creates two test databases from fragments of [HITRAN-2020]_ line databases::
 
     from radis.test.utils import setup_test_line_databases
     setup_test_line_databases()
@@ -660,7 +600,7 @@ Two approaches can be used:
 
 - improve the convolution efficiency. This involves using an efficient convolution algorithm,
   using a reduced convolution kernel, analytical approximations, or multiple spectral grid.
-- reduce the number of convolutions (for a given number of lines): this is done using the DLM strategy.
+- reduce the number of convolutions (for a given number of lines): this is done using the LDM strategy.
 
 RADIS implements the two approaches as well as various strategies and parameters
 to calculate the lineshapes efficiently.
@@ -684,13 +624,13 @@ to calculate the lineshapes efficiently.
   calculate the lineshape wings with a lower resolution. This strategy is not
   implemented in RADIS.
 
-- *DLM* :  lines are projected on a Lineshape database to reduce the number of calculated
+- *LDM* :  lines are projected on a Lineshape database to reduce the number of calculated
   lineshapes from millions to a few dozens.
   With this optimization strategy, the lineshape convolution becomes almost instantaneous
   and all the other strategies are rendered useless. Projection of all lines on the lineshape
   database becomes the performance bottleneck.
-  parameters: :py:attr:`~radis.lbl.loader.Parameters.dlm_res_L`,
-  :py:attr:`~radis.lbl.loader.Parameters.dlm_res_G`.
+  parameters: :py:attr:`~radis.lbl.loader.Parameters.ldm_res_L`,
+  :py:attr:`~radis.lbl.loader.Parameters.ldm_res_G`.
   (this is the default strategy implemented in RADIS). Learn more in [Spectral-Synthesis-Algorithm]_
 
 More details on the parameters below:
@@ -799,26 +739,6 @@ For instance::
 line database reduction steps, population calculation, and scaling of intensity and broadening parameters
 with the calculated conditions.
 
-Parallelization
----------------
-
-Note : internal CPU-parallelization was discarded in RADIS 0.9.28, as not efficient
-enough with the new lineshape algorithms implemented with radis==0.9.20.
-
-A much faster GPU-parallelization is available for equilibrium calculation::
-
-    SpectrumFactory.eq_spectrum(mode='gpu')
-
-
-GPU accelerated spectrum calculation
-------------------------------------
-
-Apart from the parallelization method mentioned above, RADIS also supports CUDA-native parallel computation, specifically
-for lineshape calculation and broadening. To use these GPU-accelerated methods to compute the spectra, use either :py:func:`~radis.lbl.calc.calc_spectrum`
-function with parameter `mode` set to `gpu`, or :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`. In order to use these methods,
-ensure that your system has an Nvidia GPU with compute capability of atleast 3.0 and CUDA Toolkit 8.0 or above. Refer to
-:ref:`GPU Spectrum Calculation on RADIS <label_radis_gpu>` to see how to setup your system to run GPU accelerated spectrum
-calculation methods, examples and performance tests.
 
 
 Tabulated Partition Functions
@@ -880,11 +800,11 @@ For the above example::
             calc_hwhm                        0.007s
             generate_wavenumber_arrays       0.001s
             calc_line_broadening             0.074s ██████
-                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 █████
+                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 █████
                 others                           0.001s
             calc_other_spectral_quan         0.003s
             generate_spectrum_obj            0.000s
diff --git a/docs/references/_bibliography.rst b/docs/references/_bibliography.rst
index 8af3de556..8b3d31f7b 100644
--- a/docs/references/_bibliography.rst
+++ b/docs/references/_bibliography.rst
@@ -42,6 +42,20 @@ Reference of supported line databases:
                  The HITRAN2016 Molecular Spectroscopic Database, Journal of Quantitative Spectroscopy and Radiative
                  Transfer 6 (38) (2017) 3–69, ISSN 00224073, `doi:10.1016/j.jqsrt.2017.06.038 <https://www.sciencedirect.com/science/article/pii/S0022407317301073>`__.
 
+.. [HITRAN-2020] I.E. Gordon, L.S. Rothman, R.J. Hargreaves, R. Hashemi, E.V. Karlovets, F.M. Skinner, E.K. Conway,
+                 C. Hill, R.V. Kochanov, Y. Tan, P. Wcis{\\l}o, A.A. Finenko, K. Nelson, P.F. Bernath, M. Birk, V. Boudon,
+                 A. Campargue, K.V. Chance, A. Coustenis, B.J. Drouin, J.{\textendash}M. Flaud, R.R. Gamache, J.T. Hodges,
+                 D. Jacquemart, E.J. Mlawer, A.V. Nikitin, V.I. Perevalov, M. Rotger, J. Tennyson, G.C. Toon, H. Tran, V.G. Tyuterev,
+                 E.M. Adkins, A. Baker, A. Barbe, E. Canè, A.G. Cs{'{a}}sz{'{a}}r, A. Dudaryonok, O. Egorov, A.J. Fleisher,
+                 H. Fleurbaey, A. Foltynowicz, T. Furtenbacher, J.J. Harrison, J.{\textendash}M. Hartmann, V.M. Horneman,
+                 X. Huang, T. Karman, J. Karns, S. Kassi, I. Kleiner, V. Kofman, F. KwabiaTchana, N.N. Lavrentieva, T.J. Lee,
+                 D.A. Long, A.A. Lukashevskaya, O.M. Lyulin, V.Yu. Makhnev, W. Matt, S.T. Massie, M. Melosso, S.N. Mikhailenko,
+                 D. Mondelain, H.S.P. Müller, O.V. Naumenko, A. Perrin, O.L. Polyansky, E. Raddaoui, P.L. Raston, Z.D. Reed,
+                 M. Rey, C. Richard, R. T{'{o}}bi{'{a}}s, I. Sadiek, D.W. Schwenke, E. Starikova, K. Sung, F. Tamassia, S.A. Tashkun,
+                 J. Vander Auwera, I.A. Vasilenko, A.A. Vigasin, G.L. Villanueva, B. Vispoel, G. Wagner, A. Yachmenev, S.N. Yurchenko
+                 The HITRAN2020 molecular spectroscopic database, Journal of Quantitative Spectroscopy and Radiative
+                 Transfer (277) (2022), `doi:10.1016/j.jqsrt.2021.107949 <https://doi.org/10.1016%2Fj.jqsrt.2021.107949>`__.
+
 The latest HITRAN database version is automatically downloaded if using ``databank='hitran'``.
 
 .. [HITEMP-2010] L. S. Rothman, I. E. Gordon, R. J. Barber, H. Dothe, R. R. Gamache, A. Goldman, V. I. Perevalov,
@@ -73,4 +87,4 @@ For data retrieval :
           A comprehensive approach to working with spectroscopic data", Journal of Quantitative Spectroscopy
           and Radiative Transfer 177 (2016) 15–30, ISSN 00224073, `doi:10.1016/j.jqsrt.2016.03.005 <https://www.researchgate.net/publication/297682202_HITRAN_Application_Programming_Interface_HAPI_A_comprehensive_approach_to_working_with_spectroscopic_data>`__.
 
-.. [Astroquery] astroquery: An Astronomical Web-querying Package in Python 10.3847/1538-3881/aafc33
\ No newline at end of file
+.. [Astroquery] astroquery: An Astronomical Web-querying Package in Python 10.3847/1538-3881/aafc33
diff --git a/docs/references/references.rst b/docs/references/references.rst
index 1a9195cbe..8a8ded956 100644
--- a/docs/references/references.rst
+++ b/docs/references/references.rst
@@ -43,8 +43,8 @@ Line-by-line algorithm :
 
 Database and database retrieval algorithms :
 
-- cite the Line Databases used (for instance, [HITRAN-2016]_, [HITEMP-2010]_ or [CDSD-4000]_ ).
-- if downloading [HITRAN-2016]_ directly with ``fetch('hitran')``, cite [HAPI]_ which is the underlying
+- cite the Line Databases used (for instance, [HITRAN-2020]_, [HITEMP-2010]_ or [CDSD-4000]_ ).
+- if downloading [HITRAN-2020]_ directly with ``fetch('hitran')``, cite [HAPI]_ which is the underlying
   interface that makes it work !
 - if running nonequilibrium calculations, mention the reference of the spectroscopic constants used
   to calculate the energies (for instance, the
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 964145152..11a7faf10 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -4,3 +4,4 @@ sphinx>=1.7.0
 astroquery>=0.3.9
 sphinxcontrib-apidoc
 sphinx-gallery
+specutils
diff --git a/docs/spectrum/spectrum.rst b/docs/spectrum/spectrum.rst
index 8ba0b4873..ad3325ab4 100644
--- a/docs/spectrum/spectrum.rst
+++ b/docs/spectrum/spectrum.rst
@@ -255,6 +255,7 @@ is sometimes confusingly called *spectral intensity*.
 - ``'abscoeff'``: spectral absorption coefficient (typically in :math:``'cm^{-1}'``), also called *opacity*.
   This is sometimes found as the *extinction coefficient* in the literature (strictly speaking, *extinction*
   is *absorption* + *diffusion*, the latter being negligible in the infrared).
+- ``'xsection'``: absorption cross-section, typically in cm2
 
 Additionally, RADIS may calculate extra quantities such as:
 
diff --git a/environment.yml b/environment.yml
index ebdfd8792..b5b5091aa 100644
--- a/environment.yml
+++ b/environment.yml
@@ -24,7 +24,7 @@ dependencies:
 - seaborn   # other matplotlib themes
 - termcolor  # terminal colors
 - specutils
-- vaex>=4.4.0   # load HDF5 files  (version for custom HDF5 groups)
+- vaex>=4.6.0   # load HDF5 files  (version for custom HDF5 groups + avoids asyncio https://github.com/vaexio/vaex/pull/1546). #TODO : install only required sub-packages
 - pip
 - pip:
   - beautifulsoup4 # parse ExoMol website
diff --git a/examples/plot_hitran_full_range.py b/examples/calc_hitran_full_range.py
similarity index 99%
rename from examples/plot_hitran_full_range.py
rename to examples/calc_hitran_full_range.py
index 9a5959056..46f0153a0 100644
--- a/examples/plot_hitran_full_range.py
+++ b/examples/calc_hitran_full_range.py
@@ -70,7 +70,7 @@
     "Number of lines: ", s.c["total_lines"]
 )  # some are discarded by linestrength cutoff
 print("Number of lines: ", s.c["lines_calculated"])
-print("Whether the sparse wavenumber algorithm was activated: ", s.c["sparse_dlm"])
+print("Whether the sparse wavenumber algorithm was activated: ", s.c["sparse_ldm"])
 print(f"Lineshape truncation used: {s.c['truncation']:.1f}cm-1")
 print(f"Total calculation time: {s.c['calculation_time']:.1f}s")
 print(
diff --git a/examples/plot_SpecDatabase.py b/examples/plot_SpecDatabase.py
index 8265bcb3d..6bc0c892f 100644
--- a/examples/plot_SpecDatabase.py
+++ b/examples/plot_SpecDatabase.py
@@ -1,14 +1,25 @@
 """
-================================================
+=================
 Spectrum Database
-================================================
-RADIS has :py:class:`~radis.tools.database.SpecDatabase` feature used to store and retrieve calculated Spectrums. A path can be specified for SpecDatabase all Spectrums are stored as .spec files which can be loaded
-from the SpecDatabase object itself.  A csv file is generated which contains all input and conditional parameters of Spectrum.
+=================
 
-RADIS also has :py:meth:`~radis.lbl.loader.DatabankLoader.init_database` feature which initializes the SpecDatabase for the SpectrumFactory and every Spectrum
+RADIS has :py:class:`~radis.tools.database.SpecDatabase` feature used to store
+and retrieve calculated Spectrums. A path can be specified for SpecDatabase all
+Spectrums are stored as .spec files which can be loaded
+from the SpecDatabase object itself.  A csv file is generated which contains all
+input and conditional parameters of Spectrum.
+
+RADIS also has :py:meth:`~radis.lbl.loader.DatabankLoader.init_database` feature
+which initializes the SpecDatabase for the SpectrumFactory and every Spectrum
 generated from it will be stored in the SpecDatabase automatically.
 
-You can use :py:meth:`~radis.tools.database.SpecList.plot_cond` to make a 2D plot using the conditions of the Spectrums in the SpecDatabase and use a z_label to plot a heat map based on it.
+You can use :py:meth:`~radis.tools.database.SpecList.plot_cond` to make a 2D plot
+using the conditions of the Spectrums in the SpecDatabase and use a z_label to plot
+a heat map based on it.
+
+:py:class:`~radis.tools.database.SpecDatabase` also has useful
+:py:meth:`~radis.tools.database.SpecDatabase.fit_spectrum` and
+:py:meth:`~radis.tools.database.SpecDatabase.interpolate` methods.
 
 
 """
@@ -37,7 +48,9 @@
 s1 = sf.eq_spectrum(Tgas=300, path_length=1)
 
 # Creating SpecDatabase
-my_folder = Path.cwd() / "SpecDatabase_Test"
+from radis.test.utils import getTestFile
+
+my_folder = Path(getTestFile(".")) / "SpecDatabase_Test"
 db = SpecDatabase(my_folder)
 
 # Method 1: Creating .spec file and adding manually to SpecDatabase
diff --git a/examples/plot_compare_CO_exomol_hitemp.py b/examples/plot_compare_CO_exomol_hitemp.py
index b35044e54..40ee2ffee 100644
--- a/examples/plot_compare_CO_exomol_hitemp.py
+++ b/examples/plot_compare_CO_exomol_hitemp.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 """
-==============================================
-Compare CO from the ExoMol and HITEMP database
-==============================================
+=========================================================
+Compare CO xsections from the ExoMol and HITEMP database
+=========================================================
 
 Auto-download and calculate CO spectrum from the HITEMP database, and the
 ExoMol database. ExoMol references multiple databases for CO. Here we do not
@@ -44,11 +44,11 @@
     databank="hitemp",
     name="HITEMP (Air broadened)",
 )
-plot_diff(s_exomol, s_hitemp, "abscoeff")
+plot_diff(s_exomol, s_hitemp, "xsection")
 
 #%% Broadening coefficients are different but areas under the lines should be the same :
 import numpy as np
 
 assert np.isclose(
-    s_exomol.get_integral("abscoeff"), s_hitemp.get_integral("abscoeff"), rtol=0.001
+    s_exomol.get_integral("xsection"), s_hitemp.get_integral("xsection"), rtol=0.001
 )
diff --git a/examples/plot_gpu.py b/examples/plot_gpu.py
new file mode 100644
index 000000000..1c04b1526
--- /dev/null
+++ b/examples/plot_gpu.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+"""
+Example using GPU sliders and GPU calculation with :py:meth:`~radis.lbl.SpectrumFactory.eq_spectrum_gpu`
+
+.. note::
+
+    in the example below, the GPU code runs on CPU, using the parameter ``emulate=True``.
+    In your environment, to run the GPU code with the full power of the GPU, remove this line
+    or set  ``emulate=False`` (default)
+
+"""
+
+from radis import SpectrumFactory
+
+sf = SpectrumFactory(
+    2100,
+    2400,  # cm-1
+    molecule="CO2",
+    isotope="1,2,3",
+    wstep=0.002,
+)
+
+sf.fetch_databank("hitemp")
+
+s = sf.eq_spectrum_gpu(
+    Tgas=1100.0,  # K
+    pressure=1,  # bar
+    mole_fraction=0.8,
+    path_length=0.2,  # cm
+    emulate=True,  # if True, runs CPU code on GPU. Set to False or remove to run on the GPU
+)
+print(s)
+
+s.apply_slit(0.5)  # cm-1
+s.plot("radiance", show=True)
diff --git a/examples/plot_gpu_widgets.py b/examples/plot_gpu_widgets.py
new file mode 100644
index 000000000..ef20dba41
--- /dev/null
+++ b/examples/plot_gpu_widgets.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+"""
+Example using GPU sliders and GPU calculation with :py:meth:`~radis.lbl.SpectrumFactory.eq_spectrum_gpu`
+
+.. note::
+
+    in the example below, the GPU code runs on CPU, using the parameter ``emulate=True``.
+    In your environment, to run the GPU code with the full power of the GPU, remove this line
+    or set  ``emulate=False`` (default)
+
+"""
+
+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)
+
+s_exp.crop(4120, 4790).plot(Iunit="mW/cm2/sr/nm")
+
+
+sf = SpectrumFactory(
+    2100,
+    2400,  # cm-1
+    molecule="CO2",
+    isotope="1,2,3",
+    wstep=0.002,
+)
+
+sf.fetch_databank("hitemp")
+
+s = sf.eq_spectrum_gpu_interactive(
+    var="radiance",
+    Tgas=ParamRange(300.0, 2500.0, 1100.0),  # K
+    pressure=ParamRange(0.1, 2, 1),  # bar
+    mole_fraction=ParamRange(0, 1, 0.8),
+    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"},
+)
+print(s)
diff --git a/radis/__init__.py b/radis/__init__.py
index af4d3ddc9..c0f4756f9 100644
--- a/radis/__init__.py
+++ b/radis/__init__.py
@@ -150,7 +150,7 @@
 
     See Also
     --------
-    :py:meth:`~radis.lbl.broadening.BroadenFactory._apply_lineshape_DLM`
+    :py:meth:`~radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
 
 
 
diff --git a/radis/__version__.txt b/radis/__version__.txt
index af88ba824..ac454c6a1 100644
--- a/radis/__version__.txt
+++ b/radis/__version__.txt
@@ -1,1 +1,1 @@
-0.11.1
+0.12.0
diff --git a/radis/cython/cpu_gpu_agnostic.pxd b/radis/cython/cpu_gpu_agnostic.pxd
new file mode 100644
index 000000000..2fd310926
--- /dev/null
+++ b/radis/cython/cpu_gpu_agnostic.pxd
@@ -0,0 +1,12 @@
+from numpy cimport complex64_t
+
+cdef extern from "../lbl/gpu.h":
+
+    void fillLDM(unsigned char* iso, float* v0, float* da, float* S0, float* El, float* gamma, float* na, float* S_klm);
+    void applyLineshapes(complex64_t* S_klm_FT, complex64_t* abscoeff);
+    void calcTransmittanceNoslit(float* abscoeff, float* transmittance_noslit);
+    void applyGaussianSlit(complex64_t* transmittance_noslit_FT, complex64_t* transmittance_FT);
+    void* get_init_ptr();
+    void* get_iter_ptr();
+    void set_dims(int blockDim_x, int gridDim_x);
+    float get_T();
diff --git a/radis/cython/radis_cython_extensions.pyx b/radis/cython/radis_cython_extensions.pyx
index 6e2009bd6..392a93bc8 100644
--- a/radis/cython/radis_cython_extensions.pyx
+++ b/radis/cython/radis_cython_extensions.pyx
@@ -1,3 +1,4 @@
+#cython: language_level=3
 import numpy as np
 cimport numpy as np
 
@@ -6,17 +7,36 @@ from cpython cimport array
 from cython.operator import dereference
 
 import ctypes
-
+from libcpp cimport bool
 from libcpp.map cimport map as mapcpp
 from libcpp.set cimport set
 from libcpp.utility cimport pair
 from libcpp.vector cimport vector
+from libc.string cimport memcpy
+from libc.math cimport log
+#from libcpp.functional cimport greater
+
+# std::greater is currently not implemented in cython (but will be in the future)
+# for now we have to import it in this way:
+#cdef extern from "<functional>" namespace "std" nogil:
+#    # Comparisons
+#    cdef cppclass greater[T=*]:
+#        greater() except +
+#        bool operator()(const T& lhs, const T& rhs) except +
 
+cdef extern from *:
+    """
+    struct greatercpp {
+        bool operator () (const float x, const float y) const {return x > y;}
+    };
+    """
+    ctypedef struct greatercpp:
+        float a
+        float b
 
 
-
-
-
+#This can be modified to see if the compiled/built/installed version is current:
+__version__ = 0.021
 
 # Fast compiled version of add_at():
 def add_at(arr, k,l,m, I):
@@ -66,24 +86,14 @@ def add_at_32(np.ndarray[np.float32_t, ndim=3] arr,
 
 # Fast compiled functions for determining the min/max broadening envelope.
 
-cdef extern from *:
-    """
-    struct greater {
-        bool operator () (const float x, const float y) const {return x > y;}
-    };
-    """
-    ctypedef struct greater:
-        float a
-        float b
-
 
 @cython.cdivision(True)
 @cython.boundscheck(False)
 @cython.wraparound(False)
 @cython.nonecheck(False)
 def calc_lorentzian_envelope_params(
-    np.ndarray[dtype=np.float32_t, ndim=1] log_2gs,
     np.ndarray[dtype=np.float32_t, ndim=1] na,
+    np.ndarray[dtype=np.float32_t, ndim=1] gamma,
     verbose_gpu = False):
 
     cdef set[pair[float,float]] unique_set
@@ -91,8 +101,8 @@ def calc_lorentzian_envelope_params(
 
     cdef vector[pair[float,float]] duplicates_removed
     cdef vector[float] na_short
-    cdef vector[float] log_2gs_short
-    cdef mapcpp[float, float, greater] bottom_envelope_map
+    cdef vector[float] gamma_short
+    cdef mapcpp[float, float, greatercpp] bottom_envelope_map
     cdef mapcpp[float, float] top_envelope_map
 
 
@@ -105,10 +115,10 @@ def calc_lorentzian_envelope_params(
     cdef size_t top_size = 0
     cdef size_t bottom_size = 0
 
-    fname = "Lorenzian_minmax_" + str(len(log_2gs)) + ".dat"
+    fname = "Lorenzian_minmax_" + str(len(gamma)) + ".dat"
 
     cdef unsigned int i,na_len
-    cdef float na_i, log_2gs_i
+    cdef float na_i, gamma_i
 
     na_len = na.size
     for i in range(na_len):
@@ -120,44 +130,44 @@ def calc_lorentzian_envelope_params(
         # of this loop anymore.
 
         float_pair[0] = na[i]
-        float_pair[1] = log_2gs[i]
+        float_pair[1] = gamma[i]
         unique_set.insert((<const pair[float,float]*>float_pair)[0])
 
     duplicates_removed.assign(unique_set.begin(), unique_set.end())
 
     # make two new vectors where all duplicates are removed:
-    for na_i, log_2gs_i in duplicates_removed:
+    for na_i, gamma_i in duplicates_removed:
         na_short.push_back(na_i)
-        log_2gs_short.push_back(log_2gs_i)
+        gamma_short.push_back(gamma_i)
 
     # identify candidates that might be part of the envelope:
     for i in range(len(na_short)):
         na_i = na_short[i]
-        log_2gs_i = log_2gs_short[i]
+        gamma_i = gamma_short[i]
 
         if bottom_envelope_map.count(na_i):
-            if log_2gs_i < bottom_envelope_map.at(na_i):
-                bottom_envelope_map[na_i] = log_2gs_i
+            if gamma_i < bottom_envelope_map.at(na_i):
+                bottom_envelope_map[na_i] = gamma_i
         else:
-            bottom_envelope_map.insert({na_i, log_2gs_i})
+            bottom_envelope_map.insert({na_i, gamma_i})
 
         if top_envelope_map.count(na_i):
-            if log_2gs_i > top_envelope_map.at(na_i):
-                top_envelope_map[na_i] = log_2gs_i
+            if gamma_i > top_envelope_map.at(na_i):
+                top_envelope_map[na_i] = gamma_i
         else:
-            top_envelope_map.insert({na_i, log_2gs_i})
+            top_envelope_map.insert({na_i, gamma_i})
 
     # For all candidates check which ones are actually part of the envelope:
     # First for the top:
     top_a = [dereference(top_envelope_map.begin()).first]
-    top_b = [dereference(top_envelope_map.begin()).second]
+    top_b = [log(dereference(top_envelope_map.begin()).second)]
     top_x = [FLOAT_MIN]
 
     idx = 0
     for first_el, second_el in top_envelope_map:
         if idx != 0:
             for i in range(len(top_x)):
-                x_ij = (second_el - top_b[i]) / (top_a[i] - first_el)
+                x_ij = (log(second_el) - top_b[i]) / (top_a[i] - first_el)
                 if x_ij >= top_x[i]:
                     if i < len(top_x) - 1:
                         if x_ij < top_x[i+1]:
@@ -166,7 +176,7 @@ def calc_lorentzian_envelope_params(
                         break
 
             top_a = top_a[:i+1] + [first_el]
-            top_b = top_b[:i+1] + [second_el]
+            top_b = top_b[:i+1] + [log(second_el)]
             top_x = top_x[:i+1] + [x_ij]
 
         idx+=1
@@ -175,14 +185,14 @@ def calc_lorentzian_envelope_params(
 
     #Then for the bottom:
     bottom_a = [dereference(bottom_envelope_map.begin()).first]
-    bottom_b = [dereference(bottom_envelope_map.begin()).second]
+    bottom_b = [log(dereference(bottom_envelope_map.begin()).second)]
     bottom_x = [FLOAT_MIN]
 
     idx = 0
     for first_el, second_el in bottom_envelope_map:
         if idx != 0:
             for i in range(len(bottom_x)):
-                x_ij = (second_el - bottom_b[i]) / (bottom_a[i] - first_el)
+                x_ij = (log(second_el) - bottom_b[i]) / (bottom_a[i] - first_el)
                 if x_ij >= bottom_x[i]:
                     if i < len(bottom_x) - 1:
                         if x_ij < bottom_x[i+1]:
@@ -191,19 +201,15 @@ def calc_lorentzian_envelope_params(
                         break
 
             bottom_a = bottom_a[:i+1] + [first_el]
-            bottom_b = bottom_b[:i+1] + [second_el]
+            bottom_b = bottom_b[:i+1] + [log(second_el)]
             bottom_x = bottom_x[:i+1] + [x_ij]
 
         idx+=1
 
     bottom_x = bottom_x[1:] + [FLOAT_MAX]
 
-    return (np.array(top_a),
-            np.array(top_b),
-            np.array(top_x),
-            np.array(bottom_a),
-            np.array(bottom_b),
-            np.array(bottom_x))
+    return ((bottom_a, bottom_b, bottom_x),
+            (top_a, top_b, top_x))
 
 
 def calc_gaussian_envelope_params(
@@ -215,65 +221,70 @@ def calc_gaussian_envelope_params(
 
     return log_2vMm_min, log_2vMm_max
 
+# Below are CPU functions extracted from the GPU module
+
+cimport cpu_gpu_agnostic as cga
+#from radis.lbl.gpu import initData, iterData
+
+cpdef void fillLDM(gridDim, blockDim, args):
+
+   cdef np.ndarray[np.uint8_t, ndim=1] iso
+   cdef np.ndarray[np.float32_t, ndim=1] v0
+   cdef np.ndarray[np.float32_t, ndim=1] da
+   cdef np.ndarray[np.float32_t, ndim=1] S0
+   cdef np.ndarray[np.float32_t, ndim=1] El
+   cdef np.ndarray[np.float32_t, ndim=1] gamma
+   cdef np.ndarray[np.float32_t, ndim=1] na
+   cdef np.ndarray[np.float32_t, ndim=3] S_klm
+
+   iso, v0, da, S0, El, gamma, na, S_klm = args
+
+   cga.set_dims(blockDim[0],gridDim[0])
+   cga.fillLDM(&iso[0], &v0[0], &da[0], &S0[0], &El[0], &gamma[0], &na[0], &S_klm[0,0,0])
 
-class blockData(ctypes.Structure):
-    _fields_ = [("line_offset", ctypes.c_int), ("iv_offset", ctypes.c_int)]
 
+cpdef void applyLineshapes(gridDim, blockDim, args):
 
-# Fast compiled version for preparing GPU blocks
-def prepare_blocks(
-    host_params_h_v0_dec,
-    host_params_h_da_dec,
-    host_params_h_dec_size,
-    host_params_h_block_preparation_step_size,
-    iter_params_h,
-    init_params_h):
+    cdef np.ndarray[np.complex64_t, ndim=3] S_klm_FT
+    cdef np.ndarray[np.complex64_t, ndim=1] abscoeff
+    S_klm_FT, abscoeff = args
 
-    cdef np.ndarray[dtype=np.float32_t, ndim=1] v0 = host_params_h_v0_dec
-    cdef np.ndarray[dtype=np.float32_t, ndim=1] da = host_params_h_da_dec
+    cga.set_dims(blockDim[0], gridDim[0])
+    cga.applyLineshapes(&S_klm_FT[0,0,0], &abscoeff[0])
 
 
-    cdef float v_prev
-    cdef float dvdi
-    cdef int i = 0
-    cdef int n = 0
-    cdef int step = host_params_h_block_preparation_step_size
+cpdef void calcTransmittanceNoslit(gridDim, blockDim, args):
 
-    new_block = blockData()
+    cdef np.ndarray[np.float32_t, ndim=1] abscoeff,
+    cdef np.ndarray[np.float32_t, ndim=1] transmittance_noslit
+    abscoeff, transmittance_noslit = args
 
-    cdef float v_cur = v0[0] + iter_params_h.p * da[0]
-    cdef float v_max = v_cur + init_params_h.N_points_per_block * init_params_h.dv
-    cdef int i_max = init_params_h.Max_iterations_per_thread
+    cga.set_dims(blockDim[0], gridDim[0])
+    cga.calcTransmittanceNoslit(&abscoeff[0], &transmittance_noslit[0])
 
-    new_block.line_offset = 0
-    new_block.iv_offset = int(((v_cur - init_params_h.v_min) / init_params_h.dv))
-    while True:
-        i += step
-        if i > host_params_h_dec_size:
-            iter_params_h.blocks[n] = new_block
 
-            n+=1
-            new_block.line_offset = i * init_params_h.N_threads_per_block
+cpdef void applyGaussianSlit(gridDim, blockDim, args):
 
-            iter_params_h.blocks[n] = new_block
-            break
+    cdef np.ndarray[np.complex64_t, ndim=1] transmittance_noslit_FT,
+    cdef np.ndarray[np.complex64_t, ndim=1] transmittance_FT
+    transmittance_noslit_FT, transmittance_FT = args
 
-        v_prev = v_cur
-        v_cur = v0[i] + iter_params_h.p * da[i]
-        if ((v_cur > v_max) or (i >= i_max)) :
-            # if (v_cur > v_max) :
-            #     dvdi = (v_cur - v_prev) / <float>step
-            #     i -= int(((v_cur - v_max) / dvdi)) + 1
-            #     v_cur = v0[i] + iter_params_h.p * da[i]
-            iter_params_h.blocks[n] = new_block
-            n+=1
-            new_block.iv_offset = int(((v_cur - init_params_h.v_min) / init_params_h.dv))
-            new_block.line_offset = i * init_params_h.N_threads_per_block
-            v_max = v_cur + (init_params_h.N_points_per_block) * init_params_h.dv
-            i_max = i + init_params_h.Max_iterations_per_thread
+    cga.set_dims(blockDim[0],gridDim[0])
+    cga.applyGaussianSlit(&transmittance_noslit_FT[0], &transmittance_FT[0])
 
-    return n
 
+cpdef set_init_params(params):
+    cdef void* cpp_ptr = cga.get_init_ptr()
+    cdef size_t size = ctypes.sizeof(params)
+    cdef char[:] struct_memview = bytearray(params)
+    memcpy(cpp_ptr, &struct_memview[0], size)
 
 
+cpdef set_iter_params(params):
+    cdef void* cpp_ptr = cga.get_iter_ptr()
+    cdef size_t size = ctypes.sizeof(params)
+    cdef char[:] struct_memview = bytearray(params)
+    memcpy(cpp_ptr, &struct_memview[0], size)
 
+cpdef get_T():
+    return cga.get_T()
diff --git a/radis/db/CO2/gordon1966.json b/radis/db/CO2/gordon1966.json
index 4c254c756..c33056621 100644
--- a/radis/db/CO2/gordon1966.json
+++ b/radis/db/CO2/gordon1966.json
@@ -33,4 +33,4 @@
         "626"
         ]
     }
-}
\ No newline at end of file
+}
diff --git a/radis/db/CO2/klarenaar2017.json b/radis/db/CO2/klarenaar2017.json
index 80b319f2d..f200d2f8b 100644
--- a/radis/db/CO2/klarenaar2017.json
+++ b/radis/db/CO2/klarenaar2017.json
@@ -109,4 +109,4 @@
         "627"
         ]
     }
-}
\ No newline at end of file
+}
diff --git a/radis/db/__init__.py b/radis/db/__init__.py
index f7bf2bc88..16faaa6cd 100644
--- a/radis/db/__init__.py
+++ b/radis/db/__init__.py
@@ -8,7 +8,7 @@
 # %% Get list of supported molecules
 def _get_supported_molecules_equilibrium():
     """Molecules supported in RADIS equilibrium calculations Basically, all
-    [HITRAN-2016]_ species."""
+    [HITRAN-2020]_ species."""
     # Add all HITRAN species
     # Add ExoMol species
     from .classes import EXOMOL_MOLECULES, HITRAN_MOLECULES
@@ -31,10 +31,10 @@ def _get_supported_molecules_nonequilibrium():
     __supported_molecules_equilibrium__
 ) = _get_supported_molecules_equilibrium()
 """ list: molecules that can be calculated in RADIS at equilibrium.
-All [HITRAN-2016]_ and [ExoMol-2020]_ species are available.
+All [HITRAN-2020]_ and [ExoMol-2020]_ species are available.
 
 Absorption coefficient calculated with RADIS at 300 K, 1 atm are shown for all
-[HITRAN-2016]_ molecules in the :ref:`HITRAN spectra page <label_examples_hitran_spectra>` .
+[HITRAN-2020]_ molecules in the :ref:`HITRAN spectra page <label_examples_hitran_spectra>` .
 
 - 1 	``'H2O'`` : 	Water 	(`spectrum <https://raw.githubusercontent.com/radis/radis-examples/master/hitran_spectra/out/0%20-%20H2O%20infrared%20spectrum.png>`__)
 - 2 	``'CO2'`` : 	Carbon Dioxide    (`spectrum <https://raw.githubusercontent.com/radis/radis-examples/master/hitran_spectra/out/1%20-%20CO2%20infrared%20spectrum.png>`__)
@@ -103,7 +103,7 @@ def _get_supported_molecules_nonequilibrium():
 Spectroscopic constants to calculate energy levels are needed.
 
 RADIS features some built-in :ref:`spectroscopic constants <label_db_spectroscopic_constants>`
-for the following species ([HITRAN-2016]_ nomenclature):
+for the following species ([HITRAN-2020]_ nomenclature):
 
 - 2 	``'CO2'`` : 	Carbon Dioxide
 - 5 	``'CO'`` : 	Carbon Monoxide
diff --git a/radis/db/classes.py b/radis/db/classes.py
index 02c95dbbd..e99fdba16 100644
--- a/radis/db/classes.py
+++ b/radis/db/classes.py
@@ -214,7 +214,7 @@
     "55": "NF3",
 }
 HITRAN_MOLECULES = list(trans.values())
-""" str: list of [HITRAN-2016]_ molecules. """
+""" str: list of [HITRAN-2020]_ molecules. """
 
 
 def get_molecule_identifier(molecule_name):
@@ -357,6 +357,7 @@ def get_molecule(molecule_id):
     "NaCl",
     "NaF",
     "NaH",
+    "NaO",
     "NaOH",
     "NiH",
     "O2",
diff --git a/radis/db/references.py b/radis/db/references.py
index b5b178290..f7059378b 100644
--- a/radis/db/references.py
+++ b/radis/db/references.py
@@ -12,6 +12,7 @@
     # Line Databases
     "HITRAN-1996": "10.1016/S0022-4073(98)00078-8",
     "HITRAN-2016": "10.1016/j.jqsrt.2017.06.038",
+    "HITRAN-2020": "10.1016/j.jqsrt.2021.107949",
     "HITEMP-2010": "10.1016/j.jqsrt.2010.05.001",
     "CDSD-4000": "10.1016/j.jqsrt.2011.03.005",
     "ExoMol-2020": "10.1016/j.jqsrt.2020.107228",
@@ -26,3 +27,11 @@
     "Klarenaar-2017": "10.1088/1361-6595/aa902e",
     "Guelachvili-1983": "10.1016/0022-2852(83)90203-5",
 }
+"""dict: list of references used in RADIS calculations.
+
+Print the BibTex entries used to compute your spectrum with :py:meth:`~radis.spectrum.spectrum.Spectrum.cite` ::
+
+    s = calc_spectrum(...)
+    s.cite()
+
+"""
diff --git a/radis/default_radis.json b/radis/default_radis.json
index 0e0a01c9d..11472d721 100644
--- a/radis/default_radis.json
+++ b/radis/default_radis.json
@@ -21,7 +21,8 @@
     "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 unless you're using the Spyder IDE (where it may result in freezes). see https://github.com/spyder-ide/spyder/issues/16183.
-    "SPARSE_WAVERANGE": "auto",              # true/false. sparse DLM algorithm. May be smaller on dense spectra. If "auto", a scarcity criterion is used (Nlines/Ngrids > 1)
+    "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.
 
     # molecular parameters
     # --------------------
diff --git a/radis/io/cdsd.py b/radis/io/cdsd.py
index f40c57774..d3518b50a 100644
--- a/radis/io/cdsd.py
+++ b/radis/io/cdsd.py
@@ -125,7 +125,7 @@ def cdsd2df(
 ):
     """Convert a CDSD-HITEMP [1]_ or CDSD-4000 [2]_ file to a Pandas dataframe.
 
-    Parameter
+    Parameters
     ----------
     fname: str
         CDSD file name
diff --git a/radis/io/dbmanager.py b/radis/io/dbmanager.py
index 0bc67317e..94a6a43eb 100644
--- a/radis/io/dbmanager.py
+++ b/radis/io/dbmanager.py
@@ -108,11 +108,19 @@ def __init__(
                 # see https://github.com/spyder-ide/spyder/issues/16183.
                 # and https://github.com/radis/radis/issues/401
                 if any("SPYDER" in name for name in environ):
+                    engine = "pytables"  # for HITRAN and HITEMP databases
                     if verbose >= 3:
                         print(
-                            "Spyder IDE detected. Memory-mapping-engine set to 'pytables' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
+                            f"Spyder IDE detected. Memory-mapping-engine set to '{engine}' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
                         )
+                # temp fix for vaex not building on RTD
+                # see https://github.com/radis/radis/issues/404
+                elif 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"
 
@@ -121,7 +129,7 @@ def __init__(
             from radis.misc.log import printwarn
 
             printwarn(
-                "Spyder IDE detected while using memory_mapping_engine='vaex'.\nVaex is the fastest way to read database files in RADIS, but Vaex processes may be stuck if ran from Spyder. See https://github.com/spyder-ide/spyder/issues/16183. You may consider using another IDE, or using a different `memory_mapping_engine` such as 'pytables' or 'feather'. You can change the engine in Spectrum.fetch_databank() calls, or globally by setting the 'MEMORY_MAPPING_ENGINE' key in your ~/radis.json \n"
+                "Spyder IDE detected while using memory_mapping_engine='vaex'.\nVaex is the fastest way to read database files in RADIS, but Vaex processes may be stuck if ran from Spyder. See https://github.com/spyder-ide/spyder/issues/16183. Quick fix: starting a new console releases the lock, usually for the rest of your session. You may consider using another IDE, or using a different `memory_mapping_engine` such as 'pytables' or 'feather'. You can change the engine in Spectrum.fetch_databank() calls, or globally by setting the 'MEMORY_MAPPING_ENGINE' key in your ~/radis.json \n"
             )
 
         self.name = name
@@ -134,6 +142,18 @@ def __init__(
             make_folders(*split(abspath(dirname(local_databases))))
             make_folders(*split(abspath(local_databases)))
 
+        if self.is_registered():
+            registered_paths = getDatabankEntries(self.name)["path"]
+            for registered_path in registered_paths:
+                if (
+                    not abspath(expanduser(registered_path))
+                    .lower()
+                    .startswith(abspath(expanduser(local_databases).lower()))
+                ):  # TODO: replace with pathlib
+                    raise ValueError(
+                        f"Databank `{self.name}` is already registered in radis.json but the declared path ({registered_path}) is not in the expected local databases folder ({local_databases}). Please fix/delete the radis.json entry, or change the default local databases path entry 'DEFAULT_DOWNLOAD_PATH' in `radis.config` or ~/radis.json"
+                    )
+
         self.downloadable = False  # by default
         self.format = ""
         self.engine = engine
@@ -179,8 +199,10 @@ def get_filenames(self):
 
             # Check that local files are the one we expect :
             for f in local_files:
-                if not abspath(expanduser(f)).startswith(
-                    abspath(expanduser(local_databases))
+                if (
+                    not abspath(expanduser(f))
+                    .lower()
+                    .startswith(abspath(expanduser(local_databases)).lower())
                 ):
                     raise ValueError(
                         f"Database {self.name} is inconsistent : it should be stored in {local_databases} but files registered in ~/radis.json contains {f}. Please fix or delete the ~/radis.json entry."
@@ -471,6 +493,7 @@ def plot(self, local_files, isotope, wavenum_min, wavenum_max):
     def get_nrows(self, local_file):
         """ Get number of rows (without loading all DataFrame)"""
         engine = self.engine
+        local_file = expanduser(local_file)
         if engine == "pytables":
             with pd.HDFStore(local_file, "r") as store:
                 nrows = store.get_storer("df").nrows
diff --git a/radis/io/exomol.py b/radis/io/exomol.py
index 099987aad..f37834997 100644
--- a/radis/io/exomol.py
+++ b/radis/io/exomol.py
@@ -213,7 +213,7 @@ def get_exomol_database_list(molecule, isotope_full_name):
 def fetch_exomol(
     molecule,
     database=None,
-    local_databases=r"~/.radisdb/exomol/",
+    local_databases=None,
     databank_name="EXOMOL-{molecule}",
     isotope="1",
     load_wavenum_min=None,
@@ -239,7 +239,8 @@ def fetch_exomol(
         :py:data:`~radis.io.exomol.KNOWN_EXOMOL_DATABASE_NAMES`. If ``None`` and
         there is only one database available, use it.
     local_databases: ``str``
-        where to create the RADIS HDF5 files. Default ``"~/.radisdb/exomol"``
+        where to create the RADIS HDF5 files. Default ``"~/.radisdb/exomol"``.
+        Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in ~/radis.json config file
     databank_name: ``str``
         name of the databank in RADIS :ref:`Configuration file <label_lbl_config_file>`
         Default ``"EXOMOL-{molecule}"``
@@ -348,6 +349,11 @@ def fetch_exomol(
                 f"{database} is not of the known available ExoMol databases for {full_molecule_name}. Choose one of : {known_exomol_databases}. ({recommended_database} is recommended by the ExoMol team). {_exomol_use_hint}"
             )
 
+    if local_databases is None:
+        import radis
+
+        local_databases = pathlib.Path(radis.config["DEFAULT_DOWNLOAD_PATH"]) / "exomol"
+
     local_path = (
         pathlib.Path(local_databases).expanduser()
         / molecule
@@ -469,6 +475,14 @@ def __init__(
                             "Spyder IDE detected. Memory-mapping-engine set to 'feather' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
                         )
                     engine = "feather"  # for ExoMol database
+                # temp fix for vaex not building on RTD
+                # see https://github.com/radis/radis/issues/404
+                elif 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"
 
@@ -574,6 +588,11 @@ def __init__(
 
         # trans file(s)
         print("Reading transition file")
+
+        # Compute linestrengths or retrieve them from cache
+        self.Tref = 296.0
+        self.QTref = np.array(self.QT_interp(self.Tref))
+
         if dic_def["numinf"] is None:
             self.trans_file = self.path / pathlib.Path(molec + ".trans.bz2")
             if not self.trans_file.exists():
@@ -629,10 +648,6 @@ def __init__(
                 self._quantumNumbers,
             ) = exomolapi.pickup_gE(ndstates, ndtrans, self.trans_file, dic_def)
 
-            # Compute linestrengths or retrieve them from cache
-            self.Tref = 296.0
-            self.QTref = np.array(self.QT_interp(self.Tref))
-
             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 ?
@@ -689,14 +704,15 @@ def __init__(
                         molec, extension=[".trans.bz2"], numtag=dic_def["numtag"][i]
                     )
                 if engine == "feather":
-                    if trans_file.with_suffix(".feather").exists():
-                        trans = pd.read_feather(trans_file.with_suffix(".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_file.with_suffix(".feather"))
+                        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()
@@ -742,15 +758,11 @@ def __init__(
                         self._quantumNumbers,
                     ) = exomolapi.pickup_gE(ndstates, ndtrans, 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 ?
+                    if engine == "vaex" and trans_file.with_suffix(".hdf5").exists():
+                        # Sij0x already stored in cache file
                         self.Sij0 = ndtrans[:, 4]
                     else:
-                        ##Line strength:
+                        ##Recompute Line strength:
                         from radis.lbl.base import (  # TODO: move elsewhere
                             linestrength_from_Einstein,
                         )
@@ -766,9 +778,15 @@ def __init__(
                         )
 
                         # 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["nu_positive"] = mask_zeronu
-                        trans = trans[trans.nu_positive].extract()
-                        trans.drop("nu_positive", inplace=True)
+                        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
@@ -785,9 +803,10 @@ def __init__(
                         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:
-                        ##Line strength:
+                        ##Recompute Line strength:
                         from radis.lbl.base import (  # TODO: move elsewhere
                             linestrength_from_Einstein,
                         )
@@ -803,22 +822,30 @@ def __init__(
                         )
 
                         # 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["nu_positive"] = mask_zeronu
-                        trans = trans[trans.nu_positive].extract()
-                        trans.drop("nu_positive", inplace=True)
+                        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])
-                    self._quantumNumbers = np.hstack(
-                        [self._quantumNumbers, quantumNumbersx]
-                    )
+                    for key in self._quantumNumbers.keys():
+                        self._quantumNumbers[key] = np.hstack(
+                            [self._quantumNumbers[key], quantumNumbersx[key]]
+                        )
 
                     if (
                         engine == "vaex"
diff --git a/radis/io/hdf5.py b/radis/io/hdf5.py
index 1b92fcc3c..451965d1a 100644
--- a/radis/io/hdf5.py
+++ b/radis/io/hdf5.py
@@ -7,7 +7,7 @@
 
 import os
 import sys
-from os.path import exists, splitext
+from os.path import exists, expanduser, splitext
 from time import time
 
 import h5py
@@ -124,6 +124,7 @@ def write(
             only these column names will be searchable directly on disk to
             load certain lines only. See :py:func:`~radis.io.hdf5.hdf2df`
         """
+        file = expanduser(file)
         if self.engine == "pytables":
             if key == "default":
                 key = "df"
@@ -170,10 +171,11 @@ def write(
             raise NotImplementedError(self.engine)
             # h5py is not designed to write Pandas DataFrames
 
-    def combine_temp_batch_files(self, file, key="default"):
+    def combine_temp_batch_files(self, file, key="default", sort_values=None):
         """Combine all batch files in ``self._temp_batch_files`` into one.
         Removes all batch files.
         """
+        file = expanduser(file)
         if self.engine == "vaex":
             if len(self._temp_batch_files) == 0:
                 raise ValueError(f"No batch temp files were written for {file}")
@@ -182,7 +184,10 @@ def combine_temp_batch_files(self, file, key="default"):
             import vaex
 
             df = vaex.open(self._temp_batch_files, group=key)
-            df.export_hdf5(file, group=key, mode="w")
+            if sort_values:
+                df.sort(by=sort_values).export_hdf5(file, group=key, mode="w")
+            else:
+                df.export_hdf5(file, group=key, mode="w")
             df.close()
         self._close_temp_batch_files()
 
@@ -235,6 +240,7 @@ def load(
         """
 
         if self.engine in ["pytables", "pytables-fixed"]:
+            fname = expanduser(fname)
             if key == "default":
                 key = "df"
             try:
@@ -267,6 +273,7 @@ def load(
             # Open file
             assert len(store_kwargs) == 0
             fname_list = fname if isinstance(fname, list) else [fname]
+            fname_list = [expanduser(f) for f in fname_list]
             # vaex can open several files at the same time
             # First check group exists
             for fname in fname_list:
@@ -296,6 +303,7 @@ def load(
             return df
 
         elif self.engine == "h5py":
+            fname = expanduser(fname)
             # TODO: define default key ?
             if key == "default":
                 key = None
@@ -339,6 +347,7 @@ def add_metadata(
         from radis.io.cache_files import _h5_compatible
 
         if self.engine in ["pytables", "pytables-fixed"]:
+            fname = expanduser(fname)
             if key == "default":
                 key = "df"
             with pd.HDFStore(fname, mode="a", complib="blosc", complevel=9) as f:
@@ -349,6 +358,7 @@ def add_metadata(
                 f.get_storer(key).attrs.metadata = metadata
 
         elif self.engine == "h5py":
+            fname = expanduser(fname)
             if key == "default":
                 key = None
             with h5py.File(fname, "a") as hf:
@@ -367,6 +377,7 @@ def add_metadata(
             if isinstance(fname, list):
                 assert isinstance(metadata, list)
                 for f, m in zip(fname, metadata):
+                    f = expanduser(f)
                     with h5py.File(f, "a") as hf:
                         if create_empty_dataset:
                             assert key is not None
@@ -376,6 +387,7 @@ def add_metadata(
                         else:
                             hf[key].attrs.update(_h5_compatible(m))
             else:
+                fname = expanduser(fname)
                 with h5py.File(fname, "a") as hf:
                     if create_empty_dataset:
                         assert key is not None
@@ -399,6 +411,7 @@ def read_metadata(self, fname: str, key="default") -> dict:
         """
 
         if self.engine in ["pytables", "pytables-fixed"]:
+            fname = expanduser(fname)
             if key == "default":
                 key = "df"
             with pd.HDFStore(fname, mode="r", complib="blosc", complevel=9) as f:
@@ -409,6 +422,7 @@ def read_metadata(self, fname: str, key="default") -> dict:
                     raise err
 
         elif self.engine == "h5py":
+            fname = expanduser(fname)
             if key == "default":
                 key = None
             with h5py.File(fname, "r") as hf:
@@ -427,12 +441,14 @@ def read_metadata(self, fname: str, key="default") -> dict:
             if isinstance(fname, list):
                 metadata = []
                 for f in fname:
+                    f = expanduser(f)
                     with h5py.File(f, "r") as hf:
                         if key is None:  # read metadta at root level
                             metadata.append(dict(hf.attrs))
                         else:
                             metadata.append(dict(hf[key].attrs))
             else:
+                fname = expanduser(fname)
                 with h5py.File(fname, "r") as hf:
                     if key is None:  # add metadta at root level
                         metadata = dict(hf.attrs)
@@ -476,6 +492,7 @@ def guess_engine(self, file, verbose=True):
             engine = "pytables"
         else:
             # Try Vaex
+            file = expanduser(file)
             with h5py.File(file, mode="r") as hf:
                 try:
                     hf[r"/table"]
diff --git a/radis/io/hitemp.py b/radis/io/hitemp.py
index f12f64b0c..f410b361f 100644
--- a/radis/io/hitemp.py
+++ b/radis/io/hitemp.py
@@ -424,7 +424,7 @@ def register(self):
 
 def fetch_hitemp(
     molecule,
-    local_databases="~/.radisdb/hitemp/",
+    local_databases=None,
     databank_name="HITEMP-{molecule}",
     isotope=None,
     load_wavenum_min=None,
@@ -447,7 +447,8 @@ def fetch_hitemp(
     molecule: `"H2O", "CO2", "N2O", "CO", "CH4", "NO", "NO2", "OH"`
         HITEMP molecule. See https://hitran.org/hitemp/
     local_databases: str
-        where to create the RADIS HDF5 files. Default ``"~/.radisdb/"``
+        where to create the RADIS HDF5 files. Default ``"~/.radisdb/hitemp"``.
+        Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in ~/radis.json config file
     databank_name: str
         name of the databank in RADIS :ref:`Configuration file <label_lbl_config_file>`
         Default ``"HITEMP-{molecule}"``
@@ -519,6 +520,10 @@ def fetch_hitemp(
     if r"{molecule}" in databank_name:
         databank_name = databank_name.format(**{"molecule": molecule})
 
+    if local_databases is None:
+        import radis
+
+        local_databases = join(radis.config["DEFAULT_DOWNLOAD_PATH"], "hitemp")
     local_databases = abspath(expanduser(local_databases))
 
     ldb = HITEMPDatabaseManager(
diff --git a/radis/io/hitran.py b/radis/io/hitran.py
index af7059f11..b035ba09a 100644
--- a/radis/io/hitran.py
+++ b/radis/io/hitran.py
@@ -1188,7 +1188,11 @@ def download_all_hitran_isotopes(molecule, directory):
             )  # create temporary files if required
 
         # Open all HDF5 cache files and export in a single file with Vaex
-        writer.combine_temp_batch_files(local_file)  # used for vaex mode only
+        writer.combine_temp_batch_files(
+            local_file, sort_values="wav"
+        )  # used for vaex mode only
+        # Note: by construction, in Pytables mode the database is not sorted
+        # by 'wav' but by isotope
 
         self.wmin = wmin_final
         self.wmax = wmax_final
@@ -1266,7 +1270,7 @@ def register(self):
 # TODO: implement parallel=True for all isotopes ?
 def fetch_hitran(
     molecule,
-    local_databases="~/.radisdb/hitran/",
+    local_databases=None,
     databank_name="HITRAN-{molecule}",
     isotope=None,
     load_wavenum_min=None,
@@ -1289,7 +1293,8 @@ def fetch_hitran(
     molecule: `"H2O", "CO2", "N2O", "CO", "CH4", "NO", "NO2", "OH"`
         HITEMP molecule. See https://hitran.org/hitemp/
     local_databases: str
-        where to create the RADIS HDF5 files. Default ``"~/.radisdb/"``
+        where to create the RADIS HDF5 files. Default ``"~/.radisdb/hitran"``.
+        Can be changed in ``radis.config["DEFAULT_DOWNLOAD_PATH"]`` or in ~/radis.json config file
     databank_name: str
         name of the databank in RADIS :ref:`Configuration file <label_lbl_config_file>`
         Default ``"HITRAN-{molecule}"``
@@ -1362,6 +1367,10 @@ def fetch_hitran(
     if r"{molecule}" in databank_name:
         databank_name = databank_name.format(**{"molecule": molecule})
 
+    if local_databases is None:
+        import radis
+
+        local_databases = join(radis.config["DEFAULT_DOWNLOAD_PATH"], "hitran")
     local_databases = abspath(local_databases.replace("~", expanduser("~")))
 
     ldb = HITRANDatabaseManager(
diff --git a/radis/io/query.py b/radis/io/query.py
index c9e90d804..26e8dd7de 100644
--- a/radis/io/query.py
+++ b/radis/io/query.py
@@ -4,7 +4,7 @@
 (based on [HAPI]_)
 
 .. note::
-    if using, cite [HAPI]_ and [HITRAN-2016]_
+    if using, cite [HAPI]_ and [HITRAN-2020]_
 
 
 References
@@ -51,7 +51,7 @@ def fetch_astroquery(
     Wrapper to the fetch function of Astroquery [1]_ (itself based on [HAPI]_)
 
     .. note::
-        if using, cite [HAPI]_ and [HITRAN-2016]_
+        if using, cite [HAPI]_ and [HITRAN-2020]_
 
     Parameters
     ----------
diff --git a/radis/io/spec_hdf.py b/radis/io/spec_hdf.py
index 6576557ea..268b7ae0b 100644
--- a/radis/io/spec_hdf.py
+++ b/radis/io/spec_hdf.py
@@ -13,7 +13,7 @@
 
 
 def spec2hdf(s, file, engine="pytables"):
-    """ Convert Spectrum s to HDF5 file"""
+    """Convert Spectrum s to HDF5 file"""
 
     df_arrays = s.to_pandas(copy=False)
 
@@ -44,7 +44,7 @@ def spec2hdf(s, file, engine="pytables"):
 
 
 def hdf2spec(file, wmin=None, wmax=None, wunit=None, columns=None, engine="pytables"):
-    """ Read HDF5 file into a Spectrum object"""
+    """Read HDF5 file into a Spectrum object"""
     from radis import Spectrum
 
     mgr = HDF5Manager(engine=engine)
diff --git a/radis/io/tools.py b/radis/io/tools.py
index f8c381b59..1d2567316 100644
--- a/radis/io/tools.py
+++ b/radis/io/tools.py
@@ -87,7 +87,7 @@ def _get_linereturnformat(data, columns, fname=""):
 
 
 def _ndarray2df(data, columns, linereturnformat):
-    """"""
+    """ """
 
     # ... Cast to new type
     # This requires to recast all the data already read, but is still the fastest
diff --git a/radis/lbl/bands.py b/radis/lbl/bands.py
index 66c7826dc..bcc969d8a 100644
--- a/radis/lbl/bands.py
+++ b/radis/lbl/bands.py
@@ -873,10 +873,10 @@ def _broaden_lines_bands(self, df):
 
         for i, (band, dg) in enumerate(gb):
             if optimization in ("simple", "min-RMS"):
-                line_profile_DLM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_DLM(dg)
-                (wavenumber, absorption) = self._apply_lineshape_DLM(
+                line_profile_LDM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_LDM(dg)
+                (wavenumber, absorption) = self._apply_lineshape_LDM(
                     dg.S.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     dg.shiftwav.values,
                     wL,
                     wG,
@@ -924,10 +924,10 @@ def _broaden_lines_noneq_bands(self, df):
         pb = ProgressBar(len(gb), active=self.verbose)
         for i, (band, dg) in enumerate(gb):
             if optimization in ("simple", "min-RMS"):
-                line_profile_DLM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_DLM(dg)
-                (wavenumber, absorption) = self._apply_lineshape_DLM(
+                line_profile_LDM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_LDM(dg)
+                (wavenumber, absorption) = self._apply_lineshape_LDM(
                     dg.S.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     dg.shiftwav.values,
                     wL,
                     wG,
@@ -935,9 +935,9 @@ def _broaden_lines_noneq_bands(self, df):
                     wG_dat,
                     optimization,
                 )
-                (_, emission) = self._apply_lineshape_DLM(
+                (_, emission) = self._apply_lineshape_LDM(
                     dg.Ei.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     dg.shiftwav.values,
                     wL,
                     wG,
@@ -1065,7 +1065,7 @@ def _calc_broadening_noneq_bands(self):
         return wavenumber, abscoeff_bands, emisscoeff_bands
 
     def _retrieve_bands_from_database(self):
-        """"""
+        """ """
 
         # TODO: options:
         # - Implement store / retrieve machinery for Bands
diff --git a/radis/lbl/base.py b/radis/lbl/base.py
index b7ac44071..e7d050f08 100644
--- a/radis/lbl/base.py
+++ b/radis/lbl/base.py
@@ -1835,7 +1835,9 @@ def calc_reference_linestrength(self):
                     assert (df.loc[idx, "iso"] == iso).all()
 
             Qref = df["iso"].map(Qref_dict)
-
+        # NOTE: This S0 is not the same as the one calculated by calc_S0()!!!
+        # The difference is that this one is multiplied by the fractional population of transition's levels
+        # TO-DO: Resolve this ambiguity
         S0 = linestrength_from_Einstein(
             A=df.A, gu=df.gu, El=df.El, Ia=df.Ia, nu=df.wav, Q=Qref, T=Tref
         )
@@ -1943,6 +1945,7 @@ def calc_lineshift(self):
 
         # Calculate
         air_pressure = self.input.pressure_mbar / 1013.25  # convert from mbar to atm
+
         if "Pshft" in df.columns:
             df["shiftwav"] = df.wav + (df.Pshft * air_pressure)
         else:
@@ -1952,10 +1955,71 @@ def calc_lineshift(self):
             )
             df["shiftwav"] = df.wav
 
+        # Sorted lines is needed for sparse wavenumber range algorithm.
+        df.sort_values("shiftwav", inplace=True)
+
         self.profiler.stop("calc_lineshift", "Calculated lineshift")
 
         return
 
+    def calc_S0(self):
+        """Calculate the unscaled intensity from the tabulated Einstein coefficient.
+
+        Parameters
+        ----------
+        None
+
+        Returns
+        -------
+        None: ``self.df0`` is updated directly with new column ``S0``
+
+        References
+        ----------
+
+        .. math::
+            S_0 = \\frac{I_a g' A_{21}}{8 \\pi c \\nu^2}
+
+        Notes
+        -----
+        Currently this value is only used in GPU calculations.
+        It is one of the columns that is transferred to the GPU
+        memory. The idea behind S0 is that it is scaled with all
+        variabled that do not change during iterations as to
+        minimize calculations.
+
+        Units: cm-1/(molecules/cm-2
+
+        NOTE: S0 is not directly related to S(T) used elsewhere!!!
+              (It may even differ in units!!!)
+
+        """
+
+        from radis.phys.constants import c  # m.s-1
+
+        c_cm = c * 100  # cm.s-1
+
+        df0 = self.df0
+
+        if len(df0) == 0:
+            return  # no lines
+
+        self.profiler.start("scaled_S0", 2, "... Scaling equilibrium linestrength")
+
+        gp = df0["gp"]
+        A = df0["A"]
+        wav = df0["wav"]
+        Ia = self.get_lines_abundance(df0)
+
+        S0 = Ia * gp * A / (8 * pi * c_cm * wav ** 2)
+
+        df0["S0"] = S0  # [cm-1/(molecules/cm-2)]
+
+        assert "S0" in self.df0
+
+        self.profiler.stop("scaled_S0", "Scaled equilibrium linestrength")
+
+        return
+
     def _calc_Q(self, molecule, iso, state, T):
         """Get partition function at temperature ``T`` from tabulated values, try with
         calculated partition function (full summation) if Out of Bounds.
@@ -3334,8 +3398,8 @@ def _check_inputs(self, mole_fraction, Tmax):
         # Checks there if there is change in wstep value if initial wstep != "auto" (could happen if users modified the _wstep value directly)
         if self._wstep != "auto":
             assert self._wstep == self.params.wstep
-        if self._sparse_dlm != "auto":
-            assert self._sparse_dlm == self.params.sparse_dlm
+        if self._sparse_ldm != "auto":
+            assert self._sparse_ldm == self.params.sparse_ldm
 
         # Checks there if there is change in truncation value
         # (except in the case where truncation is None, where we set it to be the full range)
@@ -3564,7 +3628,15 @@ def get_waverange(
     return wavenum_min, wavenum_max
 
 
-def linestrength_from_Einstein(A, gu, El, Ia, nu, Q, T):
+def linestrength_from_Einstein(
+    A,
+    gu,
+    El,
+    Ia,
+    nu,
+    Q,
+    T,
+):
     r"""Calculate linestrength at temperature ``T`` from Einstein coefficients.
 
     Parameters
diff --git a/radis/lbl/broadening.py b/radis/lbl/broadening.py
index 007391297..5a9c67636 100644
--- a/radis/lbl/broadening.py
+++ b/radis/lbl/broadening.py
@@ -39,9 +39,9 @@
 - :py:meth:`radis.lbl.broadening.BroadenFactory._add_voigt_broadening_HWHM`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._voigt_broadening`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._calc_lineshape`
-- :py:meth:`radis.lbl.broadening.BroadenFactory._calc_lineshape_DLM`
+- :py:meth:`radis.lbl.broadening.BroadenFactory._calc_lineshape_LDM`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape`
-- :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape_DLM`
+- :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._calc_broadening`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._calc_broadening_noneq`
 - :py:meth:`radis.lbl.broadening.BroadenFactory._find_weak_lines`
@@ -1396,10 +1396,10 @@ def _calc_lineshape(self, dg):
 
         return line_profile
 
-    def _calc_lineshape_DLM(self, df):
+    def _calc_lineshape_LDM(self, df):
         """Generate the lineshape database using the steps defined by the
-        parameters :py:attr:`~radis.lbl.loader.Parameters.dlm_log_pL` and
-        :py:attr:`~radis.lbl.loader.Parameters.dlm_log_pG`.
+        parameters :py:attr:`~radis.lbl.loader.Parameters.dxL` and
+        :py:attr:`~radis.lbl.loader.Parameters.dxG`.
 
         Parameters
         ----------
@@ -1408,26 +1408,26 @@ def _calc_lineshape_DLM(self, df):
 
         Returns
         -------
-        line_profile_DLM: dict
+        line_profile_LDM: dict
             dictionary of Voigt profile template.
             If ``self.params.broadening_method == 'fft'``, templates are calculated
             in Fourier space.
         wL, wG: array
-            Lorentzian and Gaussian FWHM in DLM
+            Lorentzian and Gaussian FWHM in LDM
         wL_dat, wG_dat: array
             Lorentzian and Gaussian FWHM of data lines.
 
         Reference
         ---------
-        DLM implemented based on a code snippet from D.v.d.Bekerom.
+        LDM implemented based on a code snippet from D.v.d.Bekerom.
         See: https://github.com/radis/radis/issues/37
 
         See Also
         --------
-        :py:meth:`~radis.lbl.broadening.BroadenFactory._apply_lineshape_DLM`
+        :py:meth:`~radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
 
         """
-        self.profiler.start("precompute_DLM_lineshapes", 3)
+        self.profiler.start("precompute_LDM_lineshapes", 3)
 
         # Prepare steps for Lineshape database
         # ------------------------------------
@@ -1440,21 +1440,21 @@ def _init_w_axis(w_dat, log_p):
             N = np.ceil((np.log(w_max) - np.log(w_min)) / log_p) + 1
             return w_min * np.exp(log_p * np.arange(N))
 
-        log_pL = self.params.dlm_log_pL  # DLM user params
-        log_pG = self.params.dlm_log_pG  # DLM user params
+        log_pL = self.params.dxL  # LDM user params
+        log_pG = self.params.dxG  # LDM user params
 
         wL_dat = df.hwhm_lorentz.values * 2  # FWHM
         wG_dat = df.hwhm_gauss.values * 2  # FWHM
 
         wL = _init_w_axis(wL_dat, log_pL)  # FWHM
-        self.wL = len(wL)
+        self.NwL = len(wL)
         wG = _init_w_axis(wG_dat, log_pG)  # FWHM
-        self.wG = len(wG)
+        self.NwG = len(wG)
 
         # Calculate the Lineshape
         # -----------------------
 
-        line_profile_DLM = {}
+        line_profile_LDM = {}
         broadening_method = self.params.broadening_method
         if broadening_method == "voigt":
             jit = False  # not enough lines to make the just-in-time FORTRAN compilation useful
@@ -1463,13 +1463,13 @@ def _init_w_axis(w_dat, log_p):
             # Non vectorized loop. Probably slightly slower, but this is not the bottleneck anyway.
             # see commit 6474cb7e on 15/08/2019 for a vectorized version
             for l in range(len(wG)):
-                line_profile_DLM[l] = {}
+                line_profile_LDM[l] = {}
                 for m in range(len(wL)):
                     wV_ij = olivero_1977(wG[l], wL[m])  # FWHM
                     lineshape = voigt_lineshape(
                         wbroad_centered, wL[m] / 2, wV_ij / 2, jit=jit
                     )  # FWHM > HWHM
-                    line_profile_DLM[l][m] = lineshape
+                    line_profile_LDM[l][m] = lineshape
 
         elif broadening_method == "convolve":
             wbroad_centered = self.wbroad_centered
@@ -1484,11 +1484,11 @@ def _init_w_axis(w_dat, log_p):
 
             # Get all combinations of Voigt lineshapes
             for l in range(len(wG)):
-                line_profile_DLM[l] = {}
+                line_profile_LDM[l] = {}
                 for m in range(len(wL)):
                     lineshape = np.convolve(IL[m], IG[l], mode="same")
                     lineshape /= np.trapz(lineshape, x=wbroad_centered)
-                    line_profile_DLM[l][m] = lineshape
+                    line_profile_LDM[l][m] = lineshape
 
         elif broadening_method == "fft":
             # Unlike real space methods ('convolve', 'voigt'), here we calculate
@@ -1503,10 +1503,10 @@ def _init_w_axis(w_dat, log_p):
 
             # Get all combinations of Voigt lineshapes (in Fourier space)
             for l in range(len(wG)):
-                line_profile_DLM[l] = {}
+                line_profile_LDM[l] = {}
                 for m in range(len(wL)):
 
-                    line_profile_DLM[l][m] = voigt_FT(
+                    line_profile_LDM[l][m] = voigt_FT(
                         w_lineshape_ft, wG[l] / 2, wL[m] / 2
                     )
 
@@ -1516,24 +1516,24 @@ def _init_w_axis(w_dat, log_p):
                         voigt_FT(n / (2 * wstep), wG[l] / 2, wL[m] / 2)
                         >= self.params.folding_thresh
                     ):
-                        line_profile_DLM[l][m] += voigt_FT(
+                        line_profile_LDM[l][m] += voigt_FT(
                             n / (2 * wstep) + w_fold[n & 1], wG[l] / 2, wL[m] / 2
                         )
                         n += 1
 
-                    line_profile_DLM[l][m] /= line_profile_DLM[l][m][0]
+                    line_profile_LDM[l][m] /= line_profile_LDM[l][m][0]
 
         else:
             raise NotImplementedError(
-                "Broadening method with DLM: {0}".format(broadening_method)
+                "Broadening method with LDM: {0}".format(broadening_method)
             )
 
         self.profiler.stop(
-            "precompute_DLM_lineshapes",
-            f"Precomputed DLM lineshapes ({len(wL) * len(wG)})",
+            "precompute_LDM_lineshapes",
+            f"Precomputed LDM lineshapes ({len(wL) * len(wG)})",
         )
 
-        return line_profile_DLM, wL, wG, wL_dat, wG_dat
+        return line_profile_LDM, wL, wG, wL_dat, wG_dat
 
     def plot_broadening(self, i=0, pressure_atm=None, mole_fraction=None, Tgas=None):
         """Recalculate and plot broadening for line of index ``i``.
@@ -1743,10 +1743,10 @@ def _get_indices(self, arr_i, axis):
         index = pos.astype(np.int32)
         return index, index + 1, pos - index
 
-    def _apply_lineshape_DLM(
+    def _apply_lineshape_LDM(
         self,
         broadened_param,
-        line_profile_DLM,
+        line_profile_LDM,
         shifted_wavenum,
         wL,
         wG,
@@ -1762,10 +1762,10 @@ def _apply_lineshape_DLM(
         broadened_param: pandas Series (or numpy array)   [size N = number of lines]
             Series to apply lineshape to. Typically linestrength `S` for absorption,
             or `nu * Aul / 4pi * DeltaE` for emission
-        line_profile_DLM:  dict
+        line_profile_LDM:  dict
             dict of line profiles ::
 
-                lineshape = line_profile_DLM[gaussian_index][lorentzian_index]
+                lineshape = line_profile_LDM[gaussian_index][lorentzian_index]
 
             If ``self.params.broadening_method == 'fft'``, templates are given
             in Fourier space.
@@ -1773,13 +1773,13 @@ def _apply_lineshape_DLM(
         shifted_wavenum: (cm-1)     pandas Series (size N = number of lines)
             center wavelength (used to project broaded lineshapes )
         wL: array       (size DL)
-            array of all Lorentzian widths in DLM
+            array of all Lorentzian widths in LDM
         wG: array       (size DG)
-            array of all Gaussian widths in DLM
+            array of all Gaussian widths in LDM
         wL_dat: array    (size N)
-            FWHM of all lines. Used to lookup the DLM
+            FWHM of all lines. Used to lookup the LDM
         wG_dat: array    (size N)
-            FWHM of all lines. Used to lookup the DLM
+            FWHM of all lines. Used to lookup the LDM
         optimization :
             if ``"min-RMS"`` weights optimized by analytical minimization of the RMS-error.
             Otherwise, weights equal to their relative position in the grid.
@@ -1797,15 +1797,15 @@ def _apply_lineshape_DLM(
 
         Reference
         ---------
-        DLM implemented based on a code snippet from D.v.d.Bekerom.
+        LDM implemented based on a code snippet from D.v.d.Bekerom.
         See: https://github.com/radis/radis/issues/37
 
         See Also
         --------
-        :py:meth:`~radis.lbl.broadening.BroadenFactory._calc_lineshape_DLM`
+        :py:meth:`~radis.lbl.broadening.BroadenFactory._calc_lineshape_LDM`
         """
 
-        self.profiler.start("DLM_Initialized_vectors", 3)
+        self.profiler.start("LDM_Initialized_vectors", 3)
         # Get spectrum range
         wavenumber = self.wavenumber  # get vector of wavenumbers (shape W)
         wavenumber_calc = self.wavenumber_calc
@@ -1827,12 +1827,12 @@ def _apply_lineshape_DLM(
         # ---------------------------
         # Apply line profile
 
-        self.profiler.stop("DLM_Initialized_vectors", "Initialized vectors")
-        self.profiler.start("DLM_closest_matching_line", 3)
+        self.profiler.stop("LDM_Initialized_vectors", "Initialized vectors")
+        self.profiler.start("LDM_closest_matching_line", 3)
         # ... First get closest matching spectral point  (on the left, and on the right)
         #         ... @dev: np.interp about 30% - 50% faster than np.searchsorted
 
-        # DLM : Next calculate how the line is distributed over the 2x2x2 bins.
+        # LDM : Next calculate how the line is distributed over the 2x2x2 bins.
         ki0, ki1, tvi = self._get_indices(shifted_wavenum, wavenumber_calc)
         li0, li1, tGi = self._get_indices(np.log(wG_dat), np.log(wG))
         mi0, mi1, tLi = self._get_indices(np.log(wL_dat), np.log(wL))
@@ -1842,8 +1842,8 @@ def _apply_lineshape_DLM(
 
             dv = self.params.wstep
             dxvGi = dv / wG_dat
-            dxG = self.params.dlm_log_pG  # DLM user params
-            dxL = self.params.dlm_log_pL  # DLM user params
+            dxG = self.params.dxG  # LDM user params
+            dxL = self.params.dxL  # LDM user params
 
             C1_GG = ((6 * np.pi - 16) / (15 * np.pi - 32)) ** (1 / 1.50)
             C1_LG = ((6 * np.pi - 16) / 3 * (np.log(2) / (2 * np.pi)) ** 0.5) ** (
@@ -1884,7 +1884,7 @@ def _apply_lineshape_DLM(
             aGi = tGi
             aLi = tLi
 
-        # ... fractions on DLM grid
+        # ... fractions on LDM grid
         awV00 = (1 - aGi) * (1 - aLi)
         awV01 = (1 - aGi) * aLi
         awV10 = aGi * (1 - aLi)
@@ -1894,26 +1894,26 @@ def _apply_lineshape_DLM(
         Iv1 = S * avi
 
         self.profiler.stop(
-            "DLM_closest_matching_line", "Get closest matching line & fraction"
+            "LDM_closest_matching_line", "Get closest matching line & fraction"
         )
-        self.profiler.start("DLM_Distribute_lines", 3)
+        self.profiler.start("LDM_Distribute_lines", 3)
         # ... Initialize array on which to distribute the lineshapes
         if broadening_method in ["voigt", "convolve"]:
-            if self.params.sparse_dlm == True:
-                # DLM is constructed in a sparse-way later
+            if self.params.sparse_ldm == True:
+                # LDM is constructed in a sparse-way later
                 pass
             else:
-                DLM = np.zeros((len(wavenumber_calc) + 2, len(wG), len(wL)))
+                LDM = np.zeros((len(wavenumber_calc) + 2, len(wG), len(wL)))
                 # +2 to allocate one empty grid point on each side : case where a line is on the boundary
                 ki0 += 1
                 ki1 += 1
         elif broadening_method == "fft":
-            if self.params.sparse_dlm == True:
+            if self.params.sparse_ldm == True:
                 if self.verbose >= 2:
                     print(
                         "SPARSE optimisation not implemented with 'fft' mode. Use 'voigt' for analytical voigt, or radis.config['SPARSE_WAVERANGE'] = False"
                     )
-            DLM = np.zeros(
+            LDM = np.zeros(
                 (
                     2 * len(wavenumber_calc),  # TO-DO: Add  + self.misc.zero_padding
                     len(wG),
@@ -1926,7 +1926,7 @@ def _apply_lineshape_DLM(
         # Distribute all line intensities on the 2x2x2 bins.
         if (
             broadening_method in ["voigt", "convolve"]
-            and self.params.sparse_dlm == True
+            and self.params.sparse_ldm == True
         ):
 
             import pandas as pd
@@ -1960,18 +1960,18 @@ def get_non_zero_wranges(groupby_parameters, max_range, intensity_weight):
                 Examples
                 --------
                 ::
-                    DLM_ranges_00 = get_non_zero_wranges(groupby_parameters=["li0", "mi0"], max_range=len(wavenumber_calc))
+                    LDM_ranges_00 = get_non_zero_wranges(groupby_parameters=["li0", "mi0"], max_range=len(wavenumber_calc))
                 """
                 # EP 31/10: the for loop over df.groupby is the current bottleneck
                 # (not even the sparse_add_at function !)
                 dgb = df.groupby(groupby_parameters, sort=False)
-                DLM_ranges = {}
-                DLM_reduced = {}
+                LDM_ranges = {}
+                LDM_reduced = {}
                 for groupby_param, group in dgb:
                     truncation_pts = int(self.params.truncation // self.params.wstep)
-                    # note: truncation can be unique for each point of the DLM basis
+                    # note: truncation can be unique for each point of the LDM basis
                     # (allow to have line-dependant truncatino, at least as all
-                    # lines with same truncation are grouped together in the DLM basis)
+                    # lines with same truncation are grouped together in the LDM basis)
 
                     ki0 = group.ki0.values
                     Iv0 = group.Iv0.values
@@ -1983,94 +1983,94 @@ def get_non_zero_wranges(groupby_parameters, max_range, intensity_weight):
                         ki0, Iv0, Iv1, weight, max_range, truncation_pts
                     )
 
-                    DLM_ranges[groupby_param] = ranges
+                    LDM_ranges[groupby_param] = ranges
 
                     # generate reduced array:
                     b = boolean_array_from_ranges(ranges, len(I))
                     I_reduced = I[b]
-                    DLM_reduced[groupby_param] = I_reduced
+                    LDM_reduced[groupby_param] = I_reduced
 
-                return DLM_ranges, DLM_reduced
+                return LDM_ranges, LDM_reduced
 
             w = wavenumber_calc
 
-            DLM_ranges_00, DLM_reduced_00 = get_non_zero_wranges(
+            LDM_ranges_00, LDM_reduced_00 = get_non_zero_wranges(
                 groupby_parameters=["li0", "mi0"],
                 max_range=len(w),
                 intensity_weight="awV00",
             )
-            DLM_ranges_01, DLM_reduced_01 = get_non_zero_wranges(
+            LDM_ranges_01, LDM_reduced_01 = get_non_zero_wranges(
                 groupby_parameters=["li0", "mi1"],
                 max_range=len(w),
                 intensity_weight="awV01",
             )
-            DLM_ranges_10, DLM_reduced_10 = get_non_zero_wranges(
+            LDM_ranges_10, LDM_reduced_10 = get_non_zero_wranges(
                 groupby_parameters=["li1", "mi0"],
                 max_range=len(w),
                 intensity_weight="awV10",
             )
-            DLM_ranges_11, DLM_reduced_11 = get_non_zero_wranges(
+            LDM_ranges_11, LDM_reduced_11 = get_non_zero_wranges(
                 groupby_parameters=["li1", "mi1"],
                 max_range=len(w),
                 intensity_weight="awV11",
             )
 
-            # Combine all DLM ranges:
+            # Combine all LDM ranges:
             all_keys = (
-                set(DLM_ranges_00.keys())
-                | set(DLM_ranges_01.keys())
-                | set(DLM_ranges_10.keys())
-                | set(DLM_ranges_11.keys())
+                set(LDM_ranges_00.keys())
+                | set(LDM_ranges_01.keys())
+                | set(LDM_ranges_10.keys())
+                | set(LDM_ranges_11.keys())
             )
-            DLM_ranges = {}
-            DLM_reduced = {}
+            LDM_ranges = {}
+            LDM_reduced = {}
             #  (note : could be combined faster by combining the ranges directly, rather than geenrating the boolean arrays?)
             for param in all_keys:
                 b = np.zeros(len(w), dtype=bool)
                 I = np.zeros(len(w))
-                if param in DLM_ranges_00:
-                    bi = boolean_array_from_ranges(DLM_ranges_00[param], len(w))
-                    I[bi] += DLM_reduced_00[param]
+                if param in LDM_ranges_00:
+                    bi = boolean_array_from_ranges(LDM_ranges_00[param], len(w))
+                    I[bi] += LDM_reduced_00[param]
                     b += bi
-                if param in DLM_ranges_01:
-                    bi = boolean_array_from_ranges(DLM_ranges_01[param], len(w))
-                    I[bi] += DLM_reduced_01[param]
+                if param in LDM_ranges_01:
+                    bi = boolean_array_from_ranges(LDM_ranges_01[param], len(w))
+                    I[bi] += LDM_reduced_01[param]
                     b += bi
-                if param in DLM_ranges_10:
-                    bi = boolean_array_from_ranges(DLM_ranges_10[param], len(w))
-                    I[bi] += DLM_reduced_10[param]
+                if param in LDM_ranges_10:
+                    bi = boolean_array_from_ranges(LDM_ranges_10[param], len(w))
+                    I[bi] += LDM_reduced_10[param]
                     b += bi
-                if param in DLM_ranges_11:
-                    bi = boolean_array_from_ranges(DLM_ranges_11[param], len(w))
-                    I[bi] += DLM_reduced_11[param]
+                if param in LDM_ranges_11:
+                    bi = boolean_array_from_ranges(LDM_ranges_11[param], len(w))
+                    I[bi] += LDM_reduced_11[param]
                     b += bi
 
                 # Sparse storage (coordinates & non-zeros ranges) :
                 if b.any():
-                    DLM_ranges[param] = non_zero_ranges_in_array(b)
-                    DLM_reduced[param] = I[b]
+                    LDM_ranges[param] = non_zero_ranges_in_array(b)
+                    LDM_reduced[param] = I[b]
 
         else:
-            _add_at(DLM, ki0, li0, mi0, Iv0 * awV00)
-            _add_at(DLM, ki0, li0, mi1, Iv0 * awV01)
-            _add_at(DLM, ki0, li1, mi0, Iv0 * awV10)
-            _add_at(DLM, ki0, li1, mi1, Iv0 * awV11)
-            _add_at(DLM, ki1, li0, mi0, Iv1 * awV00)
-            _add_at(DLM, ki1, li0, mi1, Iv1 * awV01)
-            _add_at(DLM, ki1, li1, mi0, Iv1 * awV10)
-            _add_at(DLM, ki1, li1, mi1, Iv1 * awV11)
+            _add_at(LDM, ki0, li0, mi0, Iv0 * awV00)
+            _add_at(LDM, ki0, li0, mi1, Iv0 * awV01)
+            _add_at(LDM, ki0, li1, mi0, Iv0 * awV10)
+            _add_at(LDM, ki0, li1, mi1, Iv0 * awV11)
+            _add_at(LDM, ki1, li0, mi0, Iv1 * awV00)
+            _add_at(LDM, ki1, li0, mi1, Iv1 * awV01)
+            _add_at(LDM, ki1, li1, mi0, Iv1 * awV10)
+            _add_at(LDM, ki1, li1, mi1, Iv1 * awV11)
 
             if broadening_method in ["voigt", "convolve"]:
-                DLM = DLM[1:-1, :, :]
+                LDM = LDM[1:-1, :, :]
                 # 1:-1 to remove the empty grid point on each side
 
         # All lines within each bins are convolved with the same lineshape.
         # Let's do it:
 
-        self.profiler.stop("DLM_Distribute_lines", "Distribute lines over DLM")
-        self.profiler.start("DLM_convolve", 3)
+        self.profiler.stop("LDM_Distribute_lines", "Distribute lines over LDM")
+        self.profiler.start("LDM_convolve", 3)
 
-        # For each value from the DLM, retrieve the lineshape and convolve all
+        # For each value from the LDM, retrieve the lineshape and convolve all
         # corresponding lines with it before summing.
         if broadening_method in ["voigt", "convolve"]:
 
@@ -2079,34 +2079,34 @@ def get_non_zero_wranges(groupby_parameters, max_range, intensity_weight):
 
             for l in range(len(wG)):
                 for m in range(len(wL)):
-                    lineshape = line_profile_DLM[l][m]
+                    lineshape = line_profile_LDM[l][m]
 
-                    if self.params.sparse_dlm == True:
-                        if (l, m) in DLM_ranges.keys():
+                    if self.params.sparse_ldm == True:
+                        if (l, m) in LDM_ranges.keys():
                             mask = boolean_array_from_ranges(
-                                DLM_ranges[(l, m)], len(sumoflines_calc)
+                                LDM_ranges[(l, m)], len(sumoflines_calc)
                             )
                             sumoflines_calc[mask] += oaconvolve(
-                                DLM_reduced[(l, m)], lineshape, "same"
+                                LDM_reduced[(l, m)], lineshape, "same"
                             )
                     else:
-                        sumoflines_calc += oaconvolve(DLM[:, l, m], lineshape, "same")
+                        sumoflines_calc += oaconvolve(LDM[:, l, m], lineshape, "same")
 
         elif broadening_method == "fft":
             # ... Initialize array in FT space
-            Idlm_FT = 1j * np.zeros(len(line_profile_DLM[0][0]))
+            Ildm_FT = 1j * np.zeros(len(line_profile_LDM[0][0]))
             for l in range(len(wG)):
                 for m in range(len(wL)):
-                    lineshape_FT = line_profile_DLM[l][m]
-                    Idlm_FT += np.fft.rfft(DLM[:, l, m]) * lineshape_FT
+                    lineshape_FT = line_profile_LDM[l][m]
+                    Ildm_FT += np.fft.rfft(LDM[:, l, m]) * lineshape_FT
             # Back in real space:
-            sumoflines_calc = np.fft.irfft(Idlm_FT)[: len(wavenumber_calc)]
+            sumoflines_calc = np.fft.irfft(Ildm_FT)[: len(wavenumber_calc)]
             sumoflines_calc /= self.params.wstep
 
         else:
             raise NotImplementedError(broadening_method)
 
-        self.profiler.stop("DLM_convolve", "Convolve and sum on spectral range")
+        self.profiler.stop("LDM_convolve", "Convolve and sum on spectral range")
         # Get valid range (discard wings)
         sumoflines = sumoflines_calc[self.woutrange[0] : self.woutrange[1]]
 
@@ -2147,9 +2147,9 @@ def _broaden_lines(self, df):
         try:
             if optimization in ("simple", "min-RMS"):
                 self.reftracker.add(doi["DIT-2020"], "algorithm")
-                # Use DLM
+                # Use LDM
 
-                line_profile_DLM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_DLM(df)
+                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()
@@ -2158,9 +2158,9 @@ def _broaden_lines(self, df):
                             estimated_time
                         )
                     )
-                (wavenumber, abscoeff) = self._apply_lineshape_DLM(
+                (wavenumber, abscoeff) = self._apply_lineshape_LDM(
                     df.S.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     df.shiftwav.values,
                     wL,
                     wG,
@@ -2251,14 +2251,14 @@ def _broaden_lines_noneq(self, df):
         try:
             if optimization in ("simple", "min-RMS"):
                 self.reftracker.add(doi["DIT-2020"], "algorithm")
-                # Use DLM
+                # Use LDM
 
                 if self.misc.zero_padding < 0 or self.misc.zero_padding > len(
                     self.wavenumber_calc
                 ):
                     self.misc.zero_padding = len(self.wavenumber_calc)
 
-                line_profile_DLM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_DLM(df)
+                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()
@@ -2267,9 +2267,9 @@ def _broaden_lines_noneq(self, df):
                             estimated_time
                         )
                     )
-                (wavenumber, abscoeff) = self._apply_lineshape_DLM(
+                (wavenumber, abscoeff) = self._apply_lineshape_LDM(
                     df.S.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     df.shiftwav.values,
                     wL,
                     wG,
@@ -2277,9 +2277,9 @@ def _broaden_lines_noneq(self, df):
                     wG_dat,
                     optimization,
                 )
-                (_, emisscoeff) = self._apply_lineshape_DLM(
+                (_, emisscoeff) = self._apply_lineshape_LDM(
                     df.Ei.values,
-                    line_profile_DLM,
+                    line_profile_LDM,
                     df.shiftwav.values,
                     wL,
                     wG,
@@ -2289,21 +2289,21 @@ def _broaden_lines_noneq(self, df):
                 )
                 # Note @dev: typical results is:
                 # >>> abscoeff:
-                # ... Precomputed DLM lineshapes in 0.0s
+                # ... Precomputed LDM lineshapes in 0.0s
                 # ... Initialized vectors in 0.0s
                 # ... Get closest matching line & fraction in 0.3s
-                # ... Distribute lines over DLM 2.1s
+                # ... Distribute lines over LDM 2.1s
                 # ... Convolve and sum on spectral range 0.4s
                 # >>> emisscoeff
                 # ... Initialized vectors in 0.0s
                 # ... Get closest matching line & fraction in 0.2s
-                # ... Distribute lines over DLM 2.1s
+                # ... Distribute lines over LDM 2.1s
                 # ... Convolve and sum on spectral range 0.3s
                 # @EP: #performance.
-                # unlike in the non DLM case, the nonequilibruum case here is ~2x
+                # unlike in the non LDM case, the nonequilibruum case here is ~2x
                 # the equilibrium case: only the closest matching line is common to the
                 # absorption & emission steps. The bottleneck is the distribution
-                # of the line over the DLM, which has to be done for both abscoeff & emisscoeff.
+                # of the line over the LDM, which has to be done for both abscoeff & emisscoeff.
 
             elif optimization is None:
                 # printing estimated time
@@ -2605,7 +2605,7 @@ def calculate_pseudo_continuum(self, noneq=False):
             wstep = self.params.wstep
             if self.params.optimization is not None:
                 raise ValueError(
-                    "pseudo-continuum not compatible with DLM. "
+                    "pseudo-continuum not compatible with LDM. "
                     + "Choose either optimization=None either pseudo_continuum_threshold=0"
                 )
 
@@ -2870,7 +2870,7 @@ def rough_sum_on_grid():
 
     # TODO: @dev #performance
     # ... try with k_rough_spectrum.add.at()  ?
-    # ... but it probably wont ever be comparable with DLM.
+    # ... but it probably wont ever be comparable with LDM.
 
     k_rough_spectrum = rough_sum_on_grid()
 
diff --git a/radis/lbl/calc.py b/radis/lbl/calc.py
index eff277ce1..364b97de0 100644
--- a/radis/lbl/calc.py
+++ b/radis/lbl/calc.py
@@ -62,7 +62,7 @@ def calc_spectrum(
     export_lines=False,
     verbose=True,
     return_factory=False,
-    **kwargs
+    **kwargs,
 ) -> Spectrum:
     r"""Calculate a :py:class:`~radis.spectrum.spectrum.Spectrum`.
 
@@ -111,7 +111,7 @@ def calc_spectrum(
         Default ``None``.​
     isotope: int, list, str of the form ``'1,2'``, or ``'all'``, or dict
         isotope id (sorted by relative density: (eg: 1: CO2-626, 2: CO2-636 for CO2).
-        See [HITRAN-2016]_ documentation for isotope list for all species. If ``'all'``,
+        See [HITRAN-2020]_ documentation for isotope list for all species. If ``'all'``,
         all isotopes in database are used (this may result in larger computation
         times!). Default ``'all'``.
 
@@ -255,13 +255,15 @@ def calc_spectrum(
         If ``False``, stays quiet. If ``True``, tells what is going on.
         If ``>=2``, gives more detailed messages (for instance, details of
         calculation times). Default ``True``.​
-    mode: ``'cpu'``, ``'gpu'``
+    mode: ``'cpu'``, ``'gpu'``, ``'emulated_gpu'``
         if set to ``'cpu'``, computes the spectra purely on the CPU. if set to ``'gpu'``,
         offloads the calculations of lineshape and broadening steps to the GPU
         making use of parallel computations to speed up the process. Default ``'cpu'``.
         Note that ``mode='gpu'`` 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>`__
+        To try the GPU code without an actual GPU, you can use ``mode='emulated_gpu'``.
+        This will run the GPU equivalent code on the CPU.
     return_factory: bool
         if ``True``, return the :py:class:`~radis.lbl.factory.SpectrumFactory` that
         computes the spectrum. Useful to access computational parameters, the line database,
@@ -270,7 +272,6 @@ def calc_spectrum(
                 s, sf = calc_spectrum(..., return_factory=True)
                 sf.df1  # see the lines calculated
                 sf.eq_spectrum(...)  #  new calculation without reloading the database
-
     **kwargs: other inputs forwarded to SpectrumFactory
         For instance: ``warnings``.
         See :py:class:`~radis.lbl.factory.SpectrumFactory` documentation for more
@@ -531,7 +532,7 @@ def _check_molecules_are_consistent(
             mode=mode,
             export_lines=export_lines,
             return_factory=return_factory,
-            **kwargs_molecule
+            **kwargs_molecule,
         )
         if return_factory:
             factory_dict[molecule] = generated_spectrum[1]
@@ -584,7 +585,7 @@ def _calc_spectrum_one_molecule(
     mode,
     export_lines,
     return_factory=False,
-    **kwargs
+    **kwargs,
 ) -> Spectrum:
     """See :py:func:`~radis.lbl.calc.calc_spectrum`"""
 
@@ -648,7 +649,7 @@ def _is_at_equilibrium():
         optimization=optimization,
         broadening_method=broadening_method,
         export_lines=export_lines,
-        **kwargs
+        **kwargs,
     )
     if (
         databank
@@ -806,16 +807,20 @@ def _is_at_equilibrium():
                 path_length=path_length,
                 name=name,
             )
-        elif mode == "gpu":
+
+        elif mode in ("gpu", "emulated_gpu"):
             s = sf.eq_spectrum_gpu(
                 Tgas=Tgas,
                 mole_fraction=mole_fraction,
                 pressure=pressure,
                 path_length=path_length,
                 name=name,
+                emulate=(True if mode == "emulated_gpu" else False),
             )
         else:
-            raise ValueError(mode)
+            raise ValueError(
+                f"mode= should be one of 'cpu', 'gpu', 'emulated_gpu' (GPU code running on CPU). Got {mode}"
+            )
     else:
         if mode != "cpu":
             raise NotImplementedError(mode)
diff --git a/radis/lbl/factory.py b/radis/lbl/factory.py
index 709531854..10f2d1f6b 100644
--- a/radis/lbl/factory.py
+++ b/radis/lbl/factory.py
@@ -82,7 +82,7 @@
 import astropy.units as u
 import numpy as np
 from numpy import arange, exp
-from scipy.constants import N_A, c, k, pi
+from scipy.constants import c
 from scipy.optimize import OptimizeResult
 
 from radis import version
@@ -96,7 +96,7 @@
     from radis.lbl.bands import BandFactory
     from radis.lbl.base import get_waverange
 
-from radis.misc.basics import flatten, is_float, list_if_float, round_off
+from radis.misc.basics import flatten, is_float, is_range, list_if_float, round_off
 from radis.misc.utils import Default
 from radis.phys.constants import k_b
 from radis.phys.convert import conv2
@@ -228,14 +228,14 @@ class SpectrumFactory(BandFactory):
         size. If ``None``, all lines are processed directly. Usually faster but
         can create memory problems. Default ``None``
     optimization : ``"simple"``, ``"min-RMS"``, ``None``
-        If either ``"simple"`` or ``"min-RMS"`` DLM optimization for lineshape calculation is used:
+        If either ``"simple"`` or ``"min-RMS"`` LDM optimization for lineshape calculation is used:
         - ``"min-RMS"`` : weights optimized by analytical minimization of the RMS-error (See: [Spectral-Synthesis-Algorithm]_)
         - ``"simple"`` : weights equal to their relative position in the grid
 
-        If using the DLM optimization, broadening method is automatically set to ``'fft'``.
+        If using the LDM optimization, broadening method is automatically set to ``'fft'``.
         If ``None``, no lineshape interpolation is performed and the lineshape of all lines is calculated.
 
-        Refer to [Spectral-Synthesis-Algorithm]_ for more explanation on the DLM method for lineshape interpolation.
+        Refer to [Spectral-Synthesis-Algorithm]_ for more explanation on the LDM method for lineshape interpolation.
 
         Default ``"min-RMS"``
     folding_thresh: float
@@ -263,8 +263,8 @@ class SpectrumFactory(BandFactory):
             sf = SpectrumFactory(...)
             sf.params.broadening_method = 'voigt'
 
-        Fast fourier transform ``'fft'`` is only available if using the DLM lineshape
-        calculation ``optimization``. Because the DLM convolves all lines at the same time,
+        Fast fourier transform ``'fft'`` is only available if using the LDM lineshape
+        calculation ``optimization``. Because the LDM convolves all lines at the same time,
         and thus operates on large arrays, ``'fft'`` becomes more appropriate than
         convolutions in real space (``'voit'``, ``'convolve'`` )
 
@@ -397,6 +397,7 @@ def __init__(
         save_memory=False,
         export_populations=None,
         export_lines=False,
+        emulate_gpu=False,
         **kwargs,
     ):
 
@@ -431,6 +432,11 @@ def __init__(
                     + "We recommended, for most cases: `truncation=300, neighbour_lines=0}`"
                 )
             )
+        for boolarg in [self_absorption, save_memory, export_lines]:
+            if boolarg not in [True, False]:
+                raise ValueError(
+                    f"Expected boolean parameter. Got `{boolarg.__repr__()}`. Use `True` or `False`"
+                )
 
         if kwargs0 != {}:
             raise TypeError(
@@ -470,10 +476,10 @@ def __init__(
         # Set default variables from config:
         import radis
 
-        self._sparse_dlm = radis.config[
+        self._sparse_ldm = radis.config[
             "SPARSE_WAVERANGE"
         ]  # target value (can be 'auto'), stored for next calculatinos
-        self.params["sparse_dlm"] = radis.config[
+        self.params["sparse_ldm"] = radis.config[
             "SPARSE_WAVERANGE"
         ]  # value evaluated at each new spectrum calculation
 
@@ -814,7 +820,7 @@ def eq_spectrum(
         self.profiler.start("generate_spectrum_obj", 2)
 
         # Get conditions
-        conditions = self.get_conditions()
+        conditions = self.get_conditions(add_config=True)
         conditions.update(
             {
                 "calculation_time": self.profiler.final[list(self.profiler.final)[-1]][
@@ -835,8 +841,8 @@ def eq_spectrum(
         if self.params.optimization != None:
             conditions.update(
                 {
-                    "wL": self.wL,
-                    "wG": self.wG,
+                    "NwL": self.NwL,
+                    "NwG": self.NwG,
                 }
             )
         del self.profiler.final[list(self.profiler.final)[-1]][
@@ -897,7 +903,13 @@ def eq_spectrum(
         return s
 
     def eq_spectrum_gpu(
-        self, Tgas, mole_fraction=None, path_length=None, pressure=None, name=None
+        self,
+        Tgas,
+        mole_fraction=None,
+        path_length=None,
+        pressure=None,
+        name=None,
+        emulate=False,
     ) -> Spectrum:
         """Generate a spectrum at equilibrium with calculation of lineshapes
         and broadening done on the GPU.
@@ -922,6 +934,11 @@ def eq_spectrum_gpu(
         name: str
             output Spectrum name (useful in batch)
 
+        Other Parameters
+        ----------------
+        emulate: bool
+            if ``True``, execute the GPU code on the CPU (useful for development)
+
         Returns
         -------
         s : Spectrum
@@ -933,9 +950,17 @@ def eq_spectrum_gpu(
                 Or directly the :meth:`~radis.spectrum.spectrum.Spectrum.plot` method
                 to plot it. See [1]_ to get an overview of all Spectrum methods
 
+        Examples
+        --------
+
+        .. minigallery:: radis.lbl.SpectrumFactory.eq_spectrum_gpu
+            :add-heading:
+
+
         See Also
         --------
-        :meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum`
+        :meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum`,
+        :meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu_interactive`
         """
 
         # %% Preprocessing
@@ -1004,31 +1029,28 @@ def eq_spectrum_gpu(
 
             else:
                 mol_id = self.df0.attrs["id"]
-            molecule = get_molecule(mol_id)
         except:
             mol_id = get_molecule_identifier(self.input.molecule)
-            molecule = get_molecule(mol_id)
 
+        molecule = get_molecule(mol_id)
         state = self.input.state
         iso_set = self._get_isotope_list(molecule)
 
-        iso_arr = list(range(max(iso_set) + 1))
+        iso_list = list(range(max(iso_set) + 1))  # element 0 is not used
 
-        Ia_arr = np.empty_like(iso_arr, dtype=np.float32)  # abundance of each isotope
         molarmass_arr = np.empty_like(
-            iso_arr, dtype=np.float32
+            iso_list, dtype=np.float32
         )  # molar mass of each isotope
-        Q_arr = np.empty_like(
-            iso_arr, dtype=np.float32
-        )  # partitioning function of each isotope
-        for iso in iso_arr:
+        Q_interp_list = []
+        for iso in iso_list:
             if iso in iso_set:
                 params = molpar.df.loc[(mol_id, iso)]
-                Ia_arr[iso] = params.abundance
                 molarmass_arr[iso] = params.mol_mass
-                Q_arr[iso] = self._get_parsum(molecule, iso, state).at(T=Tgas)
+                parsum = self.get_partition_function_interpolator(molecule, iso, state)
+                Q_interp_list.append(parsum.at)
+            else:
+                Q_interp_list.append(lambda T: 1.0)
 
-        Ia_arr[np.isnan(Ia_arr)] = 0
         molarmass_arr[np.isnan(molarmass_arr)] = 0
 
         self.profiler.start("spectrum_calculation", 1)
@@ -1043,31 +1065,27 @@ def eq_spectrum_gpu(
 
         # load the data
         df = self.df0
-        iso = df["iso"].to_numpy(dtype=np.int32)
+        iso = df["iso"].to_numpy(dtype=np.uint8)
         v0 = df["wav"].to_numpy(dtype=np.float32)
         da = df["Pshft"].to_numpy(dtype=np.float32)
         El = df["El"].to_numpy(dtype=np.float32)
         na = df["Tdpair"].to_numpy(dtype=np.float32)
 
-        log_2gs = np.array(self._get_log_2gs(), dtype=np.float32)
-        log_2vMm = np.array(self._get_log_2vMm(molarmass_arr), dtype=np.float32)
-        S0 = np.array(self._get_S0(Ia_arr), dtype=np.float32)
+        gamma = np.array(
+            self._get_lorentzian_broadening(mole_fraction), dtype=np.float32
+        )
+
+        self.calc_S0()
+        S0 = self.df0["S0"].to_numpy(dtype=np.float32)
 
-        NwG = 4  # TO-DO: these shouldn't be hardcoded
-        NwL = 8  # TO-DO: these shouldn't be hardcoded
+        dxG = self.params.dxG
+        dxL = self.params.dxL
 
         _Nlines_calculated = len(v0)
 
         if verbose >= 2:
             print("Initializing parameters...", end=" ")
 
-        if verbose is False:
-            verbose_gpu = 0
-        elif verbose is True:
-            verbose_gpu = 1
-        else:
-            verbose_gpu = verbose
-
         try:
             from radis.lbl.gpu import gpu_init, gpu_iterate
         except (ModuleNotFoundError):
@@ -1076,18 +1094,19 @@ def eq_spectrum_gpu(
 
         gpu_init(
             v_arr,
-            NwG,
-            NwL,
+            dxG,
+            dxL,
             iso,
             v0,
             da,
-            log_2gs,
+            gamma,
             na,
-            log_2vMm,
             S0,
             El,
-            Q_arr,
-            verbose_gpu,
+            molarmass_arr,
+            Q_interp_list,
+            verbose=verbose,
+            gpu=(not emulate),
         )
 
         if verbose >= 2:
@@ -1098,13 +1117,12 @@ def eq_spectrum_gpu(
         if verbose >= 2:
             print("Calculating spectra...", end=" ")
 
-        abscoeff = gpu_iterate(
+        abscoeff, transmittance, iter_params = gpu_iterate(
             pressure_mbar * 1e-3,
             Tgas,
             mole_fraction,
-            Ia_arr,
-            molarmass_arr,
-            verbose_gpu,
+            verbose=verbose,
+            gpu=(not emulate),
         )
         # Calculate output quantities
         # ----------------------------------------------------------------------
@@ -1119,6 +1137,7 @@ def eq_spectrum_gpu(
         absorbance = abscoeff * path_length
         # Generate output quantities
         transmittance_noslit = exp(-absorbance)
+        emissivity = 1 - transmittance
         emissivity_noslit = 1 - transmittance_noslit
         radiance_noslit = calc_radiance(
             wavenumber, emissivity_noslit, Tgas, unit=self.units["radiance_noslit"]
@@ -1140,7 +1159,7 @@ def eq_spectrum_gpu(
 
         # Get lines (intensities + populations)
 
-        conditions = self.get_conditions()
+        conditions = self.get_conditions(add_config=True)
         conditions.update(
             {
                 "calculation_time": self.profiler.final[list(self.profiler.final)[-1]][
@@ -1149,6 +1168,7 @@ def eq_spectrum_gpu(
                 "lines_calculated": _Nlines_calculated,
                 "thermal_equilibrium": True,
                 "radis_version": version,
+                "emulate_gpu": emulate,
                 "spectral_points": (
                     self.params.wavenum_max_calc - self.params.wavenum_min_calc
                 )
@@ -1159,8 +1179,8 @@ def eq_spectrum_gpu(
         if self.params.optimization != None:
             conditions.update(
                 {
-                    "wL": self.wL,
-                    "wG": self.wG,
+                    "NwL": iter_params.N_L,
+                    "NwG": iter_params.N_G,
                 }
             )
         del self.profiler.final[list(self.profiler.final)[-1]][
@@ -1171,10 +1191,12 @@ def eq_spectrum_gpu(
         quantities = {
             "abscoeff": (wavenumber, abscoeff),
             "absorbance": (wavenumber, absorbance),
+            "emissivity": (wavenumber, emissivity),
             "emissivity_noslit": (wavenumber, emissivity_noslit),
             "transmittance_noslit": (wavenumber, transmittance_noslit),
             # (mW/cm2/sr/nm)
             "radiance_noslit": (wavenumber, radiance_noslit),
+            "transmittance": (wavenumber, transmittance),
         }
 
         # Store results in Spectrum class
@@ -1207,6 +1229,173 @@ def eq_spectrum_gpu(
 
         return s
 
+    def eq_spectrum_gpu_interactive(
+        self, var="transmittance", slit_FWHM=0.0, plotkwargs={}, *vargs, **kwargs
+    ) -> Spectrum:
+        """
+
+        Parameters
+        ----------
+        var : TYPE, optional
+            DESCRIPTION. The default is "transmittance".
+        slit_FWHM : TYPE, optional
+            DESCRIPTION. The default is 0.0.
+        *vargs : TYPE
+            arguments forwarded to :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`
+        **kwargs : dict
+            arguments forwarded to :py:meth:`~radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu`
+            In particular, see `emulate=`.
+        plotkwargs : dict
+            arguments forwarded to :py:meth:`~radis.spectrum.spectrum.Spectrum.plot`
+
+        Returns
+        -------
+        s : TYPE
+            DESCRIPTION.
+
+        Examples
+        --------
+        ::
+
+            from radis import SpectrumFactory
+            from radis.tools.plot_tools import ParamRange
+
+
+            sf = SpectrumFactory(2200, 2400, # cm-1
+                              molecule='CO2',
+                              isotope='1,2,3',
+                              wstep=0.002,
+                              )
+
+            sf.fetch_databank('hitemp')
+
+            s = sf.eq_spectrum_gpu_interactive(Tgas=ParamRange(300.0,2000.0,1200.0), #K
+                                           pressure=0.2, #bar
+                                           mole_fraction=0.1,
+                                           path_length=ParamRange(0,10,2.0), #cm
+                                           slit_FWHM=ParamRange(0,1,0), #cm
+                                           emulate=False,  # if True, execute the GPU code on the CPU
+                                           )
+
+        .. minigallery:: radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu_interactive
+            :add-heading:
+
+        """
+
+        import matplotlib.pyplot as plt
+        from matplotlib.widgets import Slider
+
+        try:
+            from radis.lbl.gpu import gpu_iterate
+        except (ModuleNotFoundError):
+            print("Failed to load GPU module, exiting!")
+            exit()
+
+        self.interactive_params = {}
+
+        for key in kwargs:
+            if is_range(kwargs[key]):
+                param_range = kwargs[key]
+                self.interactive_params[key] = param_range
+                param_range.name = key
+                kwargs[key] = param_range.valinit
+
+        s = self.eq_spectrum_gpu(*vargs, **kwargs)
+
+        if is_range(slit_FWHM):
+            self.interactive_params["slit_FWHM"] = slit_FWHM
+            slit_FWHM.name = "slit_FWHM"
+            slit_FWHM = slit_FWHM.valinit
+
+        s.apply_slit(slit_FWHM, unit="cm-1")  # to create 'radiance', 'transmittance'
+        s.conditions["slit_FWHM"] = slit_FWHM
+
+        # TODO: this should be resolved; there should be only one pressure that is
+        # both used as keyword by the user and as input value for spectra.
+        s.conditions["pressure"] = s.conditions["pressure_mbar"] * 1e-3
+
+        was_interactive = plt.isinteractive
+        plt.ion()
+
+        line = s.plot(var, show=True, **plotkwargs)
+        fig = line.figure
+
+        def update_plot(val):
+            abscoeff, transmittance, iter_params = gpu_iterate(
+                s.conditions["pressure"],
+                s.conditions["Tgas"],
+                s.conditions["mole_fraction"],
+                l=s.conditions["path_length"],
+                slit_FWHM=s.conditions["slit_FWHM"],
+                verbose=False,
+                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
+                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"],
+                            )
+
+            line.set_ydata(new_y)
+            fig.canvas.draw_idle()
+
+        n_sliders = 0
+        for key in self.interactive_params:
+            param_range = self.interactive_params[key]
+            slider_axis = fig.add_axes([0.25, 0.05 * n_sliders + 0.05, 0.65, 0.03])
+            slider = Slider(
+                ax=slider_axis,
+                label=key,
+                valmin=param_range.valmin,
+                valmax=param_range.valmax,
+                valinit=param_range.valinit,
+            )
+            self.interactive_params[key].set_widget(slider, s, update_plot)
+            n_sliders += 1
+
+        fig.subplots_adjust(bottom=0.05 * n_sliders + 0.15)
+
+        if not was_interactive:
+            plt.ioff()
+
+        return s
+
     def non_eq_spectrum(
         self,
         Tvib,
@@ -1489,7 +1678,7 @@ def non_eq_spectrum(
         self.profiler.start("generate_spectrum_obj", 2)
 
         # Get conditions
-        conditions = self.get_conditions()
+        conditions = self.get_conditions(add_config=True)
         conditions.update(
             {
                 "calculation_time": self.profiler.final[list(self.profiler.final)[-1]][
@@ -1510,8 +1699,8 @@ def non_eq_spectrum(
         if self.params.optimization != None:
             conditions.update(
                 {
-                    "wL": self.wL,
-                    "wG": self.wG,
+                    "NwL": self.NwL,
+                    "NwG": self.NwG,
                 }
             )
         del self.profiler.final[list(self.profiler.final)[-1]][
@@ -1642,14 +1831,14 @@ def _generate_wavenumber_arrays(self):
         # Set sparse waverange mode
 
         # Setting wstep to optimal value and rounding it to a degree 3
-        if self._sparse_dlm == "auto":
+        if self._sparse_ldm == "auto":
             sparsity = len(wavenumber_calc) / len(self.df1)
-            self.params["sparse_dlm"] = (
+            self.params["sparse_ldm"] = (
                 sparsity > 1.0
             )  # works ; TODO : set a threshold based on more data
             if self.verbose >= 2:
                 print(
-                    f"Sparsity (grid points/lines) = {sparsity:.1f}. Set sparse_dlm to {self.params['sparse_dlm']}"
+                    f"Sparsity (grid points/lines) = {sparsity:.1f}. Set sparse_ldm to {self.params['sparse_ldm']}"
                 )
 
         self.profiler.stop("generate_wavenumber_arrays", "Generated Wavenumber Arrays")
@@ -1697,7 +1886,7 @@ def _is_at_equilibrium():
         factor = 1
 
         if not _is_at_equilibrium():
-            factor = 2  #  _apply_broadening_DLM() is called twice
+            factor = 2  #  _apply_broadening_LDM() is called twice
 
         wstep = self.params.wstep
         n_lines = self.misc.total_lines
@@ -1710,13 +1899,13 @@ def _is_at_equilibrium():
         broadening_method = self.params.broadening_method
 
         if optimization in ("simple", "min-RMS"):
-            wL = self.wL
-            wG = self.wG
+            NwL = self.NwL
+            NwG = self.NwG
             if broadening_method == "voigt":
                 estimated_time = (
                     2.096e-07 * n_lines
                     + 7.185e-09
-                    * (1 + wL * wG)
+                    * (1 + NwL * NwG)
                     * spectral_points
                     * np.log(spectral_points)
                     * factor
@@ -1724,7 +1913,7 @@ def _is_at_equilibrium():
             elif broadening_method == "fft":
                 estimated_time = (
                     4.675e-08
-                    * (1 + wL * wG)
+                    * (1 + NwL * NwG)
                     * spectral_points
                     * np.log(spectral_points)
                     * factor
@@ -1753,75 +1942,46 @@ def _is_at_equilibrium():
 
         return estimated_time
 
-    def _get_log_2gs(self):
-        """Returns log_2gs if it already exists in the dataframe, otherwise
-        computes it using gamma_air."""
+    def _get_lorentzian_broadening(self, x):
+        """Calculates ratioed self/air broadening based on molefraction"""
         df = self.df0
         # TODO: deal with the case of gamma_self [so we don't forget]
         # TODO (refactor) : move into BaseFactory or BroadenFactory (parent classes)
 
-        # if the column already exists, then return
-        if "log_2gs" in df.columns:
-            return df["log_2gs"]
-
-        try:
-            gamma_air = df["airbrd"].to_numpy()
-            log_2gs = np.log(2 * gamma_air)
-            df["log_2gs"] = log_2gs
-            return log_2gs
-        except KeyError as err:
-            raise KeyError(
-                "Cannot find air-broadened half-width or log_2gs in the database... please check the database"
-            ) from err
-
-    def _get_log_2vMm(self, molarmass_arr):
-        """Returns log_2vMm if it already exists in the dataframe, otherwise
-        computes it using the abundance and molar mass for each isotope passed
-        in the input."""
-        df = self.df0
-
-        # if the column already exists, then return
-        if "log_2vMm" in df.columns:
-            return df["log_2vMm"]
-
-        try:
-            v0 = df["wav"].to_numpy()  # get wavenumber
-            iso = df["iso"].to_numpy()  # get isotope
-            Mm = molarmass_arr * 1e-3 / N_A
-            log_2vMm = np.log(2 * v0) + 0.5 * np.log(
-                2 * k * np.log(2) / (c ** 2 * Mm.take(iso))
-            )
-            df["log_2vMm"] = log_2vMm
-            return log_2vMm
-        except KeyError as err:
-            raise KeyError(
-                "Cannot find wavenumber, isotope and/or log_2vMm in the database. Please check the database"
-            ) from err
-
-    def _get_S0(self, Ia_arr):
-        """Returns S0 if it already exists, otherwise computes the value using
-        abundance, gamma_air and Einstein's number."""
-        df = self.df0
-
-        # if the column already exists, then return it
-        if "S0" in df.columns:
-            return df["S0"]
-
-        try:
-            v0 = df["wav"].to_numpy()
-            iso = df["iso"].to_numpy()
-            A21 = df["A"].to_numpy()
-            Jl = df["jl"].to_numpy()
-            DJ = df["branch"].to_numpy()
-            Ju = Jl + DJ
-            gu = 2 * Ju + 1  # g_up
-            S0 = Ia_arr.take(iso) * gu * A21 / (8 * pi * c_cm * v0 ** 2)
-            df["S0"] = S0
-            return S0
-        except KeyError as err:
-            raise KeyError(
-                "Could not find wavenumber, Einstein's coefficient, lower state energy or S0 in the dataframe. PLease check the database"
-            ) from err
+        gamma_air = df["airbrd"].to_numpy()
+        gamma_self = df["selbrd"].to_numpy()
+        gamma = x * gamma_self + (1 - x) * gamma_air
+        return gamma
+
+    ##    def _get_S0(self, Ia_arr):
+    ##        """Returns S0 if it already exists, otherwise computes the value using
+    ##        abundance, upper level degeneracy and Einstein's number."""
+    ##        df = self.df0
+    ##
+    ##        # if the column already exists, then return it
+    ##        if "S0" in df.columns:
+    ##            return df["S0"]
+    ##
+    ##        ## TO-DO: I don't think 'int' and 'S0' are the same quantity!
+    ##        ##elif "int" in df.columns:
+    ##        ##    return df["int"]
+    ##
+    ##        try:
+    ##            v0 = df["wav"].to_numpy()
+    ##            iso = df["iso"].to_numpy()
+    ##            A21 = df["A"].to_numpy()
+    ##            Jl = df["jl"].to_numpy()
+    ##            DJ = df["branch"].to_numpy()
+    ##            Ju = Jl + DJ
+    ##            gu = 2 * Ju + 1  # g_up
+    ##            S0 = Ia_arr.take(iso) * gu * A21 / (8 * pi * c_cm * v0 ** 2)
+    ##            df["S0"] = S0
+    ##            return S0
+    ##
+    ##        except KeyError as err:
+    ##            raise KeyError(
+    ##                "Could not find wavenumber, Einstein's coefficient, lower state energy or S0 in the dataframe. PLease check the database"
+    ##            ) from err
 
     def optically_thin_power(
         self,
@@ -2113,11 +2273,11 @@ def print_perf_profile(self, number_format="{:.3f}", precision=16):
                     calc_hwhm                        0.007s
                     generate_wavenumber_arrays       0.001s
                     calc_line_broadening             0.074s ██████
-                        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 █████
+                        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 █████
                         others                           0.001s
                     calc_other_spectral_quan         0.003s
                     generate_spectrum_obj            0.000s
diff --git a/radis/lbl/gpu.cpp b/radis/lbl/gpu.cpp
new file mode 100644
index 000000000..a63cd204b
--- /dev/null
+++ b/radis/lbl/gpu.cpp
@@ -0,0 +1,233 @@
+
+#ifdef __CUDACC__
+
+#include<cupy/complex.cuh>
+#define agnostic_loop(i, max_i)
+#define agnostic_add(addr, val) atomicAdd((addr),(val))
+
+#else
+
+#include <complex>
+#include <cmath>
+
+#define agnostic_loop(i, max_i) for (i = 0; i < (max_i); i++)
+#define agnostic_add(addr, val) *(addr) += (val)
+
+#define __global__
+#define __device__
+#define __constant__
+
+struct threadIdx_t {int x; int y; int z;} threadIdx;
+struct blockDim_t {int x; int y; int z;} blockDim;
+struct blockIdx_t {int x; int y; int z;} blockIdx;
+struct gridDim_t {int x; int y; int z;} gridDim;
+
+void set_dims(int blockDim_x, int gridDim_x){
+    blockDim.x = blockDim_x;
+    gridDim.x = gridDim_x;
+}
+#endif
+
+#ifdef __CUDACC__
+extern "C"{
+#else
+using namespace std;
+#endif
+
+const float pi = 3.141592653589793f;
+const float r4log2 = 0.36067376022224085f; // = 1 / (4 * ln(2))
+
+//TO-DO: These should really be in gpu.h but cupy fails to load this file if it's included.
+struct initData {
+    float v_min;
+    float v_max;
+    float dv;
+    int N_v;
+    float dxG;
+    float dxL;
+    int N_total;
+    int Max_lines;
+    int N_lines;
+    int N_points_per_block;
+    int N_threads_per_block;
+    int N_blocks_per_grid;
+    int N_points_per_thread;
+    int	N_iterations_per_thread;
+    int shared_size_floats;
+    float log_c2Mm[16];
+};
+
+
+struct iterData {
+    float p;
+    float log_2p;
+    float hlog_T;
+    float log_rT;
+    float c2T;
+    float N;
+    float l;
+    float slit_FWHM;
+    float log_wG_min;
+    float log_wL_min;
+    int N_G;
+    int N_L;
+    float Q[16];
+};
+
+__device__ __constant__ struct initData init_d;
+__device__ __constant__ struct iterData iter_d;
+
+
+#ifndef __CUDACC__
+void* get_init_ptr(void){
+    return &init_d;
+}
+
+void* get_iter_ptr(void){
+    return &iter_d;
+}
+
+float get_T(){
+    return expf(2*iter_d.hlog_T);
+}
+#endif
+
+__global__ void fillLDM(
+    unsigned char* iso,
+    float* v0,
+    float* da,  // pressure shift  in cm-1/atm
+    float* S0,  // initial linestrength
+    float* El,
+    float* gamma,
+    float* na,
+    float* S_klm
+    ) {
+
+    int N_G = iter_d.N_G;
+    int N_L = iter_d.N_L;
+
+    agnostic_loop(threadIdx.x, blockDim.x){
+        agnostic_loop(blockIdx.x, gridDim.x){
+            for (int n = 0; n < init_d.N_iterations_per_thread; n++) {
+
+                int i = threadIdx.x + blockDim.x * (n + blockIdx.x * init_d.N_iterations_per_thread);
+
+                if (i < init_d.N_lines) {
+                    //Calc v
+                    // ... pressure-shift
+                    float vi = v0[i] + iter_d.p * da[i];
+                    float ki = (vi - init_d.v_min) / init_d.dv;
+                    int k0i = (int)ki;
+                    int k1i = k0i + 1  ;
+
+                    if ((k0i >= 0) && (k1i < init_d.N_v)) {
+
+                        //Calc wG
+                        float log_wGi = logf(v0[i]) + init_d.log_c2Mm[iso[i]] + iter_d.hlog_T;
+                        float li = (log_wGi - iter_d.log_wG_min) / init_d.dxG;
+                        int l0i = (int)li;
+                        int l1i = l0i + 1;
+
+                        //Calc wL
+                        float log_wLi = logf(gamma[i]) + iter_d.log_2p + na[i] * iter_d.log_rT;
+                        float mi = (log_wLi - iter_d.log_wL_min) / init_d.dxL;
+                        int m0i = (int)mi;
+                        int m1i = m0i + 1;
+
+                        //Calc I
+                        // ... 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 aV00i = (1 - aGi) * (1 - aLi);
+                        float aV01i = (1 - aGi) * aLi;
+                        float aV10i = aGi * (1 - aLi);
+                        float aV11i = aGi * aLi;
+
+                        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);
+                    }
+                }
+            }
+        }
+    }
+}
+
+
+__global__ void applyLineshapes(complex<float>* S_klm_FT, complex<float>* abscoeff) {
+
+    agnostic_loop(threadIdx.x, blockDim.x){
+        agnostic_loop(blockIdx.x, gridDim.x){
+            int k = threadIdx.x + blockDim.x * blockIdx.x;
+            if (k < init_d.N_v + 1) {
+                float x = k / (2 * init_d.N_v * init_d.dv);
+                float mul = 0.0;
+                complex<float> out_complex = 0;
+                // float out_re = 0.0;
+                // float out_im = 0.0;
+                float wG, wL;
+                int index;
+
+                for (int l = 0; l < iter_d.N_G; l++) {
+                    wG = expf(iter_d.log_wG_min + l * init_d.dxG);
+                    for (int m = 0; m < iter_d.N_L; m++) {
+                        index = k + l * (init_d.N_v+1) + m * iter_d.N_G * (init_d.N_v+1);
+                        wL = expf(iter_d.log_wL_min + m * init_d.dxL);
+                        mul = expf(-r4log2 * powf(pi * x * wG, 2) - pi * x * wL) / init_d.dv;
+                        out_complex += mul * S_klm_FT[index];
+                        //out_complex += LDM[index];
+                    }
+                }
+                abscoeff[k].real(out_complex.real());
+                abscoeff[k].imag(out_complex.imag());
+            }
+        }
+    }
+}
+
+__global__ void calcTransmittanceNoslit(float* abscoeff, float* transmittance_noslit)  {
+
+    agnostic_loop(threadIdx.x, blockDim.x){
+        agnostic_loop(blockIdx.x, gridDim.x){
+            int iv = threadIdx.x + blockDim.x * blockIdx.x;
+            if (iv < init_d.N_v) {
+                transmittance_noslit[iv] = expf(-iter_d.l * abscoeff[iv]);
+            }
+            else if (iv < init_d.N_v*2){
+                transmittance_noslit[iv] = 1.0;
+            }
+        }
+    }
+}
+
+__global__ void applyGaussianSlit(complex<float>* transmittance_noslit_FT, complex<float>* transmittance_FT){
+
+    agnostic_loop(threadIdx.x, blockDim.x){
+        agnostic_loop(blockIdx.x, gridDim.x){
+            int iv = threadIdx.x + blockDim.x * blockIdx.x;
+            float x = iv / (2 * init_d.N_v * init_d.dv);
+            float window = expf(-r4log2 * powf(pi * x * iter_d.slit_FWHM, 2));
+
+            if (iv < init_d.N_v + 1) {
+                transmittance_FT[iv] = transmittance_noslit_FT[iv] * window;
+            }
+        }
+    }
+}
+
+
+#ifdef __CUDACC__
+}
+#endif
diff --git a/radis/lbl/gpu.cu b/radis/lbl/gpu.cu
deleted file mode 100644
index 15d7f22ec..000000000
--- a/radis/lbl/gpu.cu
+++ /dev/null
@@ -1,170 +0,0 @@
-#include<cupy/complex.cuh>
-extern "C"{
-
-struct initData {
-	float v_min;
-	float v_max;
-	float dv;
-	int N_v;
-	int N_wG;
-	int N_wL;
-	int N_wG_x_N_wL;
-	int N_total;
-	int Max_lines;
-	int N_lines;
-	int N_points_per_block;
-	int N_threads_per_block;
-	int N_blocks_per_grid;
-	int N_points_per_thread;
-	int	Max_iterations_per_thread;
-	int shared_size_floats;
-};
-
-struct blockData {
-	int line_offset;
-	int iv_offset;
-};
-
-struct iterData {
-	float p;
-	float log_p;
-	float hlog_T;
-	float log_rT;
-	float c2T;
-    float N;
-	float log_wG_min;
-	float log_wL_min;
-	float log_dwG;
-	float log_dwL;
-	blockData blocks[4096];
-};
-
-__device__ __constant__ initData init_params_d;
-__device__ __constant__ iterData iter_params_d;
-
-__global__ void fillDLM(
-    int* iso,
-	float* v0,
-	float* da,
-	float* S0,
-	float* El,
-	float* log_2gs,
-	float* na,
-	float* log_2vMm,
-	float* global_DLM,
-    float* Q,               // Q is an array of size max(isotopes_id) + 1
-    float* I_add_arr) {
-
-	// Some overhead for "efficient" block allocation:
-	blockData block = iter_params_d.blocks[blockIdx.x + gridDim.x * blockIdx.y];
-	int block_id = blockIdx.x + gridDim.x * blockIdx.y;
-	int N_iterations = (iter_params_d.blocks[block_id + 1].line_offset - iter_params_d.blocks[block_id].line_offset) / init_params_d.N_threads_per_block;
-	int DLM_offset = iter_params_d.blocks[block_id].iv_offset * init_params_d.N_wG_x_N_wL;
-	int iv_offset = iter_params_d.blocks[block_id].iv_offset;
-
-	int NwL = init_params_d.N_wL;
-	int NwGxNwL = init_params_d.N_wG_x_N_wL;
-
-	////Allocate and zero the Shared memory
-	//extern __shared__ float shared_DLM[];
-
-	float* DLM = global_DLM;
-
-	for (int n = 0; n < N_iterations; n++) { // eliminate for-loop
-
-		// >>: Process from left to right edge:
-		int i = iter_params_d.blocks[block_id].line_offset + threadIdx.x + n * blockDim.x;
-
-		if (i < init_params_d.N_lines) {
-			//Calc v
-			float v_dat = v0[i] + iter_params_d.p * da[i];
-			float iv = (v_dat - init_params_d.v_min) / init_params_d.dv; //- iv_offset;
-			int iv0 = (int)iv;
-			int iv1 = iv0 + 1  ;
-
-            //arr_idx[i] = iv0;
-
-			//^4
-
-			if ((iv0 >= 0) && (iv1 < init_params_d.N_v)) {
-
-				//Calc wG
-				float log_wG_dat = log_2vMm[i] + iter_params_d.hlog_T;
-				float iwG = (log_wG_dat - iter_params_d.log_wG_min) / iter_params_d.log_dwG;
-				int iwG0 = (int)iwG;
-				int iwG1 = iwG0 + 1;
-				//^8
-
-                //arr_idx[i] = iwG0;
-
-				//Calc wL
-				float log_wL_dat = log_2gs[i] + iter_params_d.log_p + na[i] * iter_params_d.log_rT;
-				float iwL = (log_wL_dat - iter_params_d.log_wL_min) / iter_params_d.log_dwL;
-				int iwL0 = (int)iwL;
-				int iwL1 = iwL0 + 1;
-				//^12
-
-                //arr_idx[i] = iwL0;
-
-				//Calc I
-				float I_add = iter_params_d.N * S0[i] * (expf(iter_params_d.c2T * El[i]) - expf(iter_params_d.c2T * (El[i] + v0[i]))) / Q[iso[i]];
-
-                I_add_arr[i] = I_add;
-
-				float av = iv - iv0;
-				float awG = (iwG - iwG0) * expf((iwG1 - iwG) * iter_params_d.log_dwG);
-				float awL = (iwL - iwL0) * expf((iwL1 - iwL) * iter_params_d.log_dwL);
-
-				float aV00 = (1 - awG) * (1 - awL);
-				float aV01 = (1 - awG) * awL;
-				float aV10 = awG * (1 - awL);
-				float aV11 = awG * awL;
-
-				float Iv0 = I_add * (1 - av);
-				float Iv1 = I_add * av;
-
-				atomicAdd(&DLM[iwL0 + iwG0 * NwL + iv0 * NwGxNwL], aV00 * Iv0);
-				atomicAdd(&DLM[iwL0 + iwG0 * NwL + iv1 * NwGxNwL], aV00 * Iv1);
-				atomicAdd(&DLM[iwL0 + iwG1 * NwL + iv0 * NwGxNwL], aV01 * Iv0);
-				atomicAdd(&DLM[iwL0 + iwG1 * NwL + iv1 * NwGxNwL], aV01 * Iv1);
-				atomicAdd(&DLM[iwL1 + iwG0 * NwL + iv0 * NwGxNwL], aV10 * Iv0);
-				atomicAdd(&DLM[iwL1 + iwG0 * NwL + iv1 * NwGxNwL], aV10 * Iv1);
-				atomicAdd(&DLM[iwL1 + iwG1 * NwL + iv0 * NwGxNwL], aV11 * Iv0);
-				atomicAdd(&DLM[iwL1 + iwG1 * NwL + iv1 * NwGxNwL], aV11 * Iv1);
-			}
-		}
-	}
-}
-
-__global__ void applyLineshapes(complex<float>* DLM, complex<float>* spectrum) {
-
-	const float pi = 3.141592653589793f;
-	const float r4log2 = 0.36067376022224085f; // = 1 / (4 * ln(2))
-	int iv = threadIdx.x + blockDim.x * blockIdx.x;
-
-	if (iv < init_params_d.N_v + 1) {
-
-		float x = iv / (2 * init_params_d.N_v * init_params_d.dv);
-		float mul = 0.0;
-        complex<float> out_complex = 0;
-        // float out_re = 0.0;
-		// float out_im = 0.0;
-		float wG, wL;
-		int index;
-
-		for (int iwG = 0; iwG < init_params_d.N_wG; iwG++) {
-			wG = expf(iter_params_d.log_wG_min + iwG * iter_params_d.log_dwG);
-			for (int iwL = 0; iwL < init_params_d.N_wL; iwL++) {
-				//index = iwG + iwL * init_params_d.N_wG + iv * init_params_d.N_wG_x_N_wL;
-                index = iv + iwG * (init_params_d.N_v+1) + iwL * init_params_d.N_wG * (init_params_d.N_v+1);
-				wL = expf(iter_params_d.log_wL_min + iwL * iter_params_d.log_dwL);
-				mul = expf(-r4log2 * powf(pi * x * wG, 2) - pi * x * wL);
-                out_complex += mul* DLM[index];
-                //out_complex += DLM[index];
-			}
-		}
-        complex<float> temp(out_complex.real(), out_complex.imag());
-		spectrum[iv] = temp;
-	}
-}
-}
diff --git a/radis/lbl/gpu.h b/radis/lbl/gpu.h
new file mode 100644
index 000000000..e3a77de36
--- /dev/null
+++ b/radis/lbl/gpu.h
@@ -0,0 +1,17 @@
+
+#include<complex>
+using namespace std;
+
+// GPU/CPU functions:
+void fillLDM(unsigned char* iso, float* v0, float* da, float* S0, float* El, float* gamma, float* na, float* S_klm);
+void applyLineshapes(complex<float>* S_klm_FT, complex<float>* abscoeff);
+void calcTransmittanceNoslit(float* abscoeff, float* transmittance_noslit);
+void applyGaussianSlit(complex<float>* transmittance_noslit_FT, complex<float>* transmittance_FT);
+
+// CPU only:
+void* get_init_ptr(void);
+void* get_iter_ptr(void);
+void set_dims(int blockDim_x, int gridDim_x);
+float get_T();
+
+
diff --git a/radis/lbl/gpu.py b/radis/lbl/gpu.py
index 3b5dfb81f..6f1dff57f 100644
--- a/radis/lbl/gpu.py
+++ b/radis/lbl/gpu.py
@@ -1,9 +1,11 @@
 import ctypes
-import pickle
 from os.path import join
 
-import cupy as cp
 import numpy as np
+from scipy.constants import N_A, c, h, k
+
+c_cm = 100 * c
+c2 = h * c_cm / k
 
 from radis.misc.utils import getProjectRoot
 
@@ -14,9 +16,8 @@ class initData(ctypes.Structure):
         ("v_max", ctypes.c_float),
         ("dv", ctypes.c_float),
         ("N_v", ctypes.c_int),
-        ("N_wG", ctypes.c_int),
-        ("N_wL", ctypes.c_int),
-        ("N_wG_x_N_wL", ctypes.c_int),
+        ("dxG", ctypes.c_float),
+        ("dxL", ctypes.c_float),
         ("N_total", ctypes.c_int),
         ("Max_lines", ctypes.c_int),
         ("N_lines", ctypes.c_int),
@@ -24,127 +25,117 @@ class initData(ctypes.Structure):
         ("N_threads_per_block", ctypes.c_int),
         ("N_blocks_per_grid", ctypes.c_int),
         ("N_points_per_thread", ctypes.c_int),
-        ("Max_iterations_per_thread", ctypes.c_int),
+        ("N_iterations_per_thread", ctypes.c_int),
         ("shared_size_floats", ctypes.c_int),
+        ("log_c2Mm", ctypes.c_float * 16),
     ]
 
 
-try:
-    from radis_cython_extensions import blockData
-
-except (ModuleNotFoundError):
-
-    class blockData(ctypes.Structure):
-        _fields_ = [("line_offset", ctypes.c_int), ("iv_offset", ctypes.c_int)]
-
-
 class iterData(ctypes.Structure):
     _fields_ = [
         ("p", ctypes.c_float),
-        ("log_p", ctypes.c_float),
+        ("log_2p", ctypes.c_float),
         ("hlog_T", ctypes.c_float),
         ("log_rT", ctypes.c_float),
         ("c2T", ctypes.c_float),
         ("N", ctypes.c_float),
+        ("l", ctypes.c_float),
+        ("slit_FWHM", ctypes.c_float),
         ("log_wG_min", ctypes.c_float),
         ("log_wL_min", ctypes.c_float),
-        ("log_dwG", ctypes.c_float),
-        ("log_dwL", ctypes.c_float),
-        ("blocks", blockData * 4096),
+        ("N_G", ctypes.c_int),
+        ("N_L", ctypes.c_int),
+        ("Q", ctypes.c_float * 16),
     ]
 
 
-init_params_h = initData()
-iter_params_h = iterData()
+init_h = initData()
+iter_h = iterData()
+
 
-host_params_h_start = cp.cuda.Event()
-host_params_h_stop = cp.cuda.Event()
-host_params_h_start_DLM = cp.cuda.Event()
-host_params_h_stop_DLM = cp.cuda.Event()
-host_params_h_data_start = cp.cuda.Event()
-host_params_h_data_stop = cp.cuda.Event()
+def py_calc_lorentzian_envelope_params(na, gamma, verbose=False):
+    """
 
-# TO-DO: read and compile CUDA code at install time, then pickle the cuda object
-cuda_fname = join(getProjectRoot(), "lbl", "gpu.cu")
-with open(cuda_fname, "rb") as f:
-    cuda_code = f.read().decode()
 
-cuda_module = cp.RawModule(code=cuda_code)
-fillDLM = cuda_module.get_function("fillDLM")
-applyLineshapes = cuda_module.get_function("applyLineshapes")
+    Parameters
+    ----------
+    na : TYPE
+        DESCRIPTION.
+    gamma : TYPE
+        DESCRIPTION.
+    verbose : TYPE, optional
+        DESCRIPTION. The default is False.
 
+    Returns
+    -------
+    TYPE
+        DESCRIPTION..
 
-def py_calc_lorentzian_envelope_params(na, log_2gs, verbose=False):
+    """
     # Remove duplicates
     unique_lines = set([])
     for i in range(len(na)):
-        unique_lines.add(str(na[i]) + " " + str(log_2gs[i]))
+        unique_lines.add(str(na[i]) + " " + str(gamma[i]))
 
     # Only keep extremes
     max_dict = {}
     min_dict = {}
     for s in unique_lines:
-        na_i, log_2gs_i = map(float, s.split())
+        na_i, gamma_i = map(float, s.split())
         try:
-            max_dict[na_i] = log_2gs_i if log_2gs_i > max_dict[na_i] else max_dict[na_i]
-        except (KeyError):
-            max_dict[na_i] = log_2gs_i
+            min_dict[na_i] = gamma_i if gamma_i < min_dict[na_i] else min_dict[na_i]
+            max_dict[na_i] = gamma_i if gamma_i > max_dict[na_i] else max_dict[na_i]
 
-        try:
-            min_dict[na_i] = log_2gs_i if log_2gs_i < min_dict[na_i] else min_dict[na_i]
         except (KeyError):
-            min_dict[na_i] = log_2gs_i
+            min_dict[na_i] = gamma_i
+            max_dict[na_i] = gamma_i
 
     # Check which ones are really at the top:
-    keys = sorted(max_dict.keys())
-
-    topA = [keys[0]]
-    topB = [max_dict[keys[0]]]
-    topX = [-np.inf]
-
-    for key in keys[1:]:
-        for i in range(len(topX)):
-            xi = (max_dict[key] - topB[i]) / (topA[i] - key)
-            if xi >= topX[i]:
-                if i < len(topX) - 1:
-                    if xi < topX[i + 1]:
+    result = []
+    for test_dict in (min_dict, max_dict):
+
+        keys = sorted(test_dict.keys(), reverse=(test_dict == min_dict))
+        A = [keys[0]]
+        B = [np.log(test_dict[keys[0]])]
+        X = [-np.inf]
+
+        for key in keys[1:]:
+            for i in range(len(X)):
+                xi = (np.log(test_dict[key]) - B[i]) / (A[i] - key)
+                if xi >= X[i]:
+                    if i < len(X) - 1:
+                        if xi < X[i + 1]:
+                            break
+                    else:
                         break
-                else:
-                    break
 
-        topA = topA[: i + 1] + [key]
-        topB = topB[: i + 1] + [max_dict[key]]
-        topX = topX[: i + 1] + [xi]
+            A = A[: i + 1] + [key]
+            B = B[: i + 1] + [np.log(test_dict[key])]
+            X = X[: i + 1] + [xi]
 
-    topX = topX[1:] + [np.inf]
+        X = X[1:] + [np.inf]
+        result.append((A, B, X))
 
-    # Check which ones are really at the bottom:
-    keys = sorted(min_dict.keys())[::-1]
+    return tuple(result)
 
-    bottomA = [keys[0]]
-    bottomB = [min_dict[keys[0]]]
-    bottomX = [-np.inf]
-
-    for key in keys[1:]:
-        for i in range(len(bottomX)):
-            xi = (min_dict[key] - bottomB[i]) / (bottomA[i] - key)
-            if xi >= bottomX[i]:
-                if i < len(bottomX) - 1:
-                    if xi < bottomX[i + 1]:
-                        break
-                else:
-                    break
 
-        bottomA = bottomA[: i + 1] + [key]
-        bottomB = bottomB[: i + 1] + [min_dict[key]]
-        bottomX = bottomX[: i + 1] + [xi]
+def py_calc_gaussian_envelope_params(log_2vMm, verbose=False):
+    """
 
-    bottomX = bottomX[1:] + [np.inf]
 
-    return (topA, topB, topX, bottomA, bottomB, bottomX)
+    Parameters
+    ----------
+    log_2vMm : TYPE
+        DESCRIPTION.
+    verbose : TYPE, optional
+        DESCRIPTION. The default is False.
 
+    Returns
+    -------
+    TYPE
+        DESCRIPTION.
 
-def py_calc_gaussian_envelope_params(log_2vMm, verbose=False):
+    """
     return np.amin(log_2vMm), np.max(log_2vMm)
 
 
@@ -152,467 +143,613 @@ def py_calc_gaussian_envelope_params(log_2vMm, verbose=False):
     from radis_cython_extensions import (
         calc_gaussian_envelope_params,
         calc_lorentzian_envelope_params,
-        prepare_blocks,
     )
 except (ModuleNotFoundError):
     calc_gaussian_envelope_params = py_calc_gaussian_envelope_params
     calc_lorentzian_envelope_params = py_calc_lorentzian_envelope_params
-    # TO-DO: currently we don't have a purely Python implementation for prepare_blocks
-    # prepare_blocks = py_prepare_blocks
 
 
-def init_gaussian_params(log_2vMm, verbose_gpu):
+def init_gaussian_params(log_2vMm, verbose):
+    """
 
-    if verbose_gpu >= 2:
-        print("Initializing Gaussian parameters")
 
-    fname = "Gaussian_minmax_" + str(len(log_2vMm)) + ".dat"
-    try:
-        param_data = pickle.load(open(fname, "rb"))
-        if verbose_gpu >= 2:
-            print(" (from cache)... ")
+    Parameters
+    ----------
+    log_2vMm : TYPE
+        DESCRIPTION.
+    verbose : TYPE
+        DESCRIPTION.
 
-    except (OSError, IOError):
-        if verbose_gpu >= 2:
+    Returns
+    -------
+    param_data : TYPE
+        DESCRIPTION.
+
+    """
+
+    if verbose >= 2:
+        print("Initializing Gaussian parameters")
+
+    ##fname = "Gaussian_minmax_" + str(len(log_2vMm)) + ".dat"
+    ##    try:
+    ##        param_data = pickle.load(open(fname, "rb"))
+    ##        if verbose >= 2:
+    ##            print(" (from cache)... ")
+    ##
+    ##    except (OSError, IOError):
+    if True:
+        if verbose >= 2:
             print("... ")
 
-        param_data = calc_gaussian_envelope_params(log_2vMm, verbose_gpu)
-        pickle.dump(param_data, open(fname, "wb"))
+        param_data = calc_gaussian_envelope_params(log_2vMm, verbose)
+    ##        pickle.dump(param_data, open(fname, "wb"))
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("done!")
 
     return param_data
 
 
-def init_lorentzian_params(log_2gs, na, verbose_gpu):
+def init_lorentzian_params(na, gamma, verbose):
+    """
 
-    if verbose_gpu >= 2:
-        print("Initializing Lorentzian parameters ")
 
-    fname = "Lorenzian_minmax_" + str(len(log_2gs)) + ".dat"
+    Parameters
+    ----------
+    na : TYPE
+        DESCRIPTION.
+    gamma : TYPE
+        DESCRIPTION.
+    verbose : TYPE
+        DESCRIPTION.
+
+    Returns
+    -------
+    param_data : TYPE
+        DESCRIPTION.
 
-    try:
-        with open(fname, "rb") as f:
-            if verbose_gpu >= 2:
-                print(" (from cache)... ")
-            param_data = pickle.load(f)
+    """
+
+    if verbose >= 2:
+        print("Initializing Lorentzian parameters ")
 
-    except:
-        if verbose_gpu >= 2:
+    ##fname = "Lorenzian_minmax_" + str(len(gamma)) + ".dat"
+    ##
+    ##    try:
+    ##        with open(fname, "rb") as f:
+    ##            if verbose >= 2:
+    ##                print(" (from cache)... ")
+    ##            param_data = pickle.load(f)
+    ##
+    ##    except:
+    if True:
+        if verbose >= 2:
             print(" ... ")
 
-        param_data = calc_lorentzian_envelope_params(log_2gs, na, verbose_gpu)
-        with open(fname, "wb") as f:
-            pickle.dump(param_data, f)
+        param_data = calc_lorentzian_envelope_params(na, gamma, verbose)
+    ##        with open(fname, "wb") as f:
+    ##            pickle.dump(param_data, f)
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("done!")
 
     return param_data
 
 
-def calc_gaussian_params(
-    gaussian_param_data, init_params_h, iter_params_h, epsilon=1e-4
-):
+def calc_gaussian_params(gaussian_param_data, init_h, iter_h, epsilon=1e-4):
+    """
+
+
+    Parameters
+    ----------
+    gaussian_param_data : TYPE
+        DESCRIPTION.
+    init_h : TYPE
+        DESCRIPTION.
+    iter_h : TYPE
+        DESCRIPTION.
+    epsilon : TYPE, optional
+        DESCRIPTION. The default is 1e-4.
+
+    Returns
+    -------
+    None.
+
+    """
+
+    log_2vMm_min, log_2vMm_max = gaussian_param_data
+    log_wG_min = log_2vMm_min + iter_h.hlog_T
+    log_wG_max = log_2vMm_max + iter_h.hlog_T
+    ##    print("wG:", log_wG_min, log_wG_max)
+    log_wG_max += epsilon
+
+    # dxG = (log_wG_max - log_wG_min) / (init_h.N_G - 1)
+    N = int(np.ceil((log_wG_max - log_wG_min) / init_h.dxG) + 1)
 
-    host_params_h_log_2vMm_min, host_params_h_log_2vMm_max = gaussian_param_data
-    log_wG_min = host_params_h_log_2vMm_min + iter_params_h.hlog_T
-    log_wG_max = host_params_h_log_2vMm_max + iter_params_h.hlog_T + epsilon
-    log_dwG = (log_wG_max - log_wG_min) / (init_params_h.N_wG - 1)
-
-    iter_params_h.log_wG_min = log_wG_min
-    iter_params_h.log_dwG = log_dwG
-
-
-def calc_lorentzian_params(param_data, init_params_h, iter_params_h, epsilon=1e-4):
-
-    (
-        top_x,
-        top_a,
-        top_b,
-        bottom_x,
-        bottom_a,
-        bottom_b,
-    ) = param_data
-
-    for i in range(bottom_x.size):
-        if iter_params_h.log_rT < bottom_x[i]:
-            log_wL_min = (
-                iter_params_h.log_rT * bottom_a[i] + bottom_b[i] + iter_params_h.log_p
-            )
-            break
-
-    for i in range(top_x.size):
-        if iter_params_h.log_rT < top_x[i]:
-            log_wL_max = (
-                iter_params_h.log_rT * top_a[i]
-                + top_b[i]
-                + iter_params_h.log_p
-                + epsilon
-            )
-            break
-
-    log_dwL = (log_wL_max - log_wL_min) / (init_params_h.N_wL - 1)
-    log_wL_min, log_dwL = -3.8011555671691895, 0.07515250146389008
-    iter_params_h.log_wL_min = log_wL_min
-    iter_params_h.log_dwL = log_dwL
-
-
-def set_pT(p, T, mole_fraction, iter_params_h):
-
-    c2 = 1.4387773538277204  # K.cm
-    k = 1.38064852e-23  # J.K-1
-    iter_params_h.p = p  # bar
-    iter_params_h.log_p = np.log(p)
-    iter_params_h.hlog_T = 0.5 * np.log(T)
-    iter_params_h.log_rT = np.log(296.0 / T)
-    iter_params_h.c2T = -c2 / T
-    iter_params_h.N = mole_fraction * p * 1e5 / (1e6 * k * T)  # cm-3
-
-    ## TO-DO: These are molecule/isotopologue specific params and should not be compiled
-    # cdef float B  = <float>     0.3902 #cm-1
-    # cdef float w1 = <float>  1354.31 #cm-1
-    # cdef float w2 = <float>   672.85 #cm-1
-    # cdef float w3 = <float>  2396.32 #cm-1
-
-    # cdef int d1 = 1
-    # cdef int d2 = 2
-    # cdef int d3 = 1
-    # cdef float gr = 0.5
-
-    # cdef float Trot = T
-    # cdef float Tv12 = T
-    # cdef float Tv3  = T
-
-    # cdef float Qr = gr * Trot/(c2 * B)*np.exp(c2*B/(<float>3.0*Trot)) #McDowell 1978
-    # cdef float Qv1 = 1 / np.power(1 - np.exp(-c2 * w1 / Tv12), d1)
-    # cdef float Qv2 = 1 / np.power(1 - np.exp(-c2 * w2 / Tv12), d2)
-    # cdef float Qv3 = 1 / np.power(1 - np.exp(-c2 * w3 / Tv3 ), d3)
-    # cdef float Q = Qr * Qv1 * Qv2 * Qv3
-
-    # iter_params_h.Q = Q
-
-
-def set_constant_memory(var_str):
-    var_h = globals()[var_str + "_h"]
-    memptr_d = cuda_module.get_global(var_str + "_d")
-    ptr = ctypes.cast(ctypes.pointer(var_h), ctypes.c_void_p)
-    struct_size = ctypes.sizeof(var_h)
-    memptr_d.copy_from_host(ptr, struct_size)
+    iter_h.log_wG_min = log_wG_min
+    # iter_h.dxG = dxG
+    iter_h.N_G = N
+
+
+def calc_lorentzian_minmax(param_data, log_rT, log_2p):
+    """
+
+
+    Parameters
+    ----------
+    param_data : TYPE
+        DESCRIPTION.
+    log_rT : TYPE
+        DESCRIPTION.
+    log_2p : TYPE
+        DESCRIPTION.
+
+    Returns
+    -------
+    TYPE
+        DESCRIPTION.
+
+    """
+
+    result = []
+    for params in param_data:
+        A, B, X = params
+        i = 0
+        while X[i] < log_rT:
+            i += 1
+        result.append(log_rT * A[i] + B[i] + log_2p)
+    return tuple(result)
+
+
+def calc_lorentzian_params(param_data, init_h, iter_h, epsilon=1e-4):
+    """
+
+
+    Parameters
+    ----------
+    param_data : TYPE
+        DESCRIPTION.
+    init_h : TYPE
+        DESCRIPTION.
+    iter_h : TYPE
+        DESCRIPTION.
+    epsilon : TYPE, optional
+        DESCRIPTION. The default is 1e-4.
+
+    Returns
+    -------
+    None.
+
+    """
+
+    log_wL_min, log_wL_max = calc_lorentzian_minmax(
+        param_data, iter_h.log_rT, iter_h.log_2p
+    )
+    log_wL_max += epsilon
+    # dxL = (log_wL_max - log_wL_min) / (init_h.N_L - 1)
+    N = int(np.ceil((log_wL_max - log_wL_min) / init_h.dxL) + 1)
+
+    iter_h.log_wL_min = log_wL_min
+    iter_h.N_L = N
+
+
+def set_pTQ(p, T, mole_fraction, iter_h, l=1.0, slit_FWHM=0.0):
+    """
+
+
+    Parameters
+    ----------
+    p : float
+        pressure [bar].
+    T : float
+        temperature [K].
+    mole_fraction : float
+    iter_h : TYPE
+        DESCRIPTION.
+    l : TYPE, optional
+        DESCRIPTION. The default is 1.0.
+    slit_FWHM : TYPE, optional
+        DESCRIPTION. The default is 0.0.
+
+    Returns
+    -------
+    None.
+
+    """
+
+    iter_h.p = p  # bar
+    iter_h.log_2p = np.log(2 * p)
+    iter_h.hlog_T = 0.5 * np.log(T)
+    iter_h.log_rT = np.log(296.0 / T)
+    iter_h.c2T = -c2 / T
+    iter_h.N = mole_fraction * p * 1e5 / (1e6 * k * T)  # cm-3
+    iter_h.l = l
+    iter_h.slit_FWHM = slit_FWHM
+
+    for i in range(len(Q_interpolator_list)):
+        iter_h.Q[i] = Q_interpolator_list[i](T)
+
+
+def constant_memory_setter(cuda_module, var_str):
+    def setter(var_h):
+        memptr_d = cuda_module.get_global(var_str)
+        ptr = ctypes.cast(ctypes.pointer(var_h), ctypes.c_void_p)
+        struct_size = ctypes.sizeof(var_h)
+        memptr_d.copy_from_host(ptr, struct_size)
+
+    return setter
 
 
 def gpu_init(
-    v_arr, N_wG, N_wL, iso, v0, da, log_2gs, na, log_2vMm, S0, El, Q, verbose_gpu
+    v_arr,
+    dxG,
+    dxL,
+    iso,
+    v0,
+    da,
+    gamma,
+    na,
+    S0,
+    El,
+    Mm_arr,
+    Q_intp_list,
+    verbose=True,
+    gpu=False,
 ):
+    """
+
+
+    Parameters
+    ----------
+    v_arr : TYPE
+        DESCRIPTION.
+    dxG : TYPE
+        DESCRIPTION.
+    dxL : TYPE
+        DESCRIPTION.
+    iso : TYPE
+        DESCRIPTION.
+    v0 : TYPE
+        DESCRIPTION.
+    da : TYPE
+        DESCRIPTION.
+    gamma : TYPE
+        DESCRIPTION.
+    na : TYPE
+        DESCRIPTION.
+    S0 : TYPE
+        DESCRIPTION.
+    El : TYPE
+        DESCRIPTION.
+    Mm_arr  : TYPE
+        DESCRIPTION.
+    Q_intp_list: TYPE
+        DESCRIPTION.
+    verbose : TYPE, optional
+        DESCRIPTION. The default is True.
+    gpu : TYPE, optional
+        DESCRIPTION. The default is False.
+
+    Returns
+    -------
+    None.
+
+    """
 
     # ----------- setup global variables -----------------
-    global init_params_h
-    global host_params_h_dec_size
-    global host_params_h_block_preparation_step_size
-    global host_params_h_iso_d
-    global host_params_h_v0_d
-    global host_params_h_v0_dec
-    global host_params_h_da_d
-    global host_params_h_da_dec
-    global host_params_h_S0_d
-    global host_params_h_El_d
-    global host_params_h_Q_d
-    global host_params_h_log_2gs_d
-    global host_params_h_na_d
-    global host_params_h_log_2vMm_d
-    global host_params_h_DLM_d_in
-    global host_params_h_spectrum_d_in
-    global host_params_h_I_add
-    global host_params_h_data_start
-    global host_params_h_data_stop
-    global host_params_h_elapsedTimeData
-
-    global lorentzian_param_data
-    global gaussian_param_data
-
-    global cuda_module
-    global database_path
-    global N_lines_to_load
+    global init_h
+    global block_preparation_step_size
+    global iso_d, v0_d, da_d, S0_d, El_d, gamma_d, na_d
+    global spectrum_in_d, transmittance_noslit_d, transmittance_FT_d
+    global lorentzian_param_data, gaussian_param_data, Q_interpolator_list
+    global cuda_module, cuda_functions
     # -----------------------------------------------------
 
-    init_params_h.v_min = np.min(v_arr)  # 2000.0
-    init_params_h.v_max = np.max(v_arr)  # 2400.0
-    init_params_h.dv = (v_arr[-1] - v_arr[0]) / (len(v_arr) - 1)  # 0.002
-    init_params_h.N_v = len(
-        v_arr
-    )  # int((init_params_h.v_max - init_params_h.v_min)/init_params_h.dv)
-
-    init_params_h.N_wG = N_wG
-    init_params_h.N_wL = N_wL
-    ##    spectrum_h = np.zeros(init_params_h.N_v, dtype=np.float32)
-
-    init_params_h.Max_iterations_per_thread = 1024
-    host_params_h_block_preparation_step_size = 128
-
-    host_params_h_shared_size = 0x8000  # Bytes - Size of the shared memory
-    ##    host_params_h_Min_threads_per_block = 128   # Ensures a full warp from each of the 4 processors
-    ##    host_params_h_Max_threads_per_block = 1024  # Maximum determined by device parameters
-    init_params_h.shared_size_floats = host_params_h_shared_size // 4  # size of float
-
-    init_params_h.N_wG_x_N_wL = init_params_h.N_wG * init_params_h.N_wL
-    init_params_h.N_total = init_params_h.N_wG_x_N_wL * init_params_h.N_v
-    init_params_h.N_points_per_block = (
-        init_params_h.shared_size_floats // init_params_h.N_wG_x_N_wL
-    )
+    if gpu:
+        from cupy import RawModule, array, complex64, float32, zeros
 
-    init_params_h.N_threads_per_block = 1024
-    init_params_h.N_blocks_per_grid = 4 * 256 * 256
-    init_params_h.N_points_per_thread = (
-        init_params_h.N_points_per_block // init_params_h.N_threads_per_block
-    )
+        cuda_fname = join(getProjectRoot(), "lbl", "gpu.cpp")
+        with open(cuda_fname, "rb") as f:
+            cuda_code = f.read().decode()
 
-    if verbose_gpu >= 2:
-        print()
-        print(
-            "Spectral points per block  : {0}".format(init_params_h.N_points_per_block)
+        cuda_module = RawModule(code=cuda_code)
+        cuda_functions = (
+            cuda_module.get_function("fillLDM"),
+            cuda_module.get_function("applyLineshapes"),
+            cuda_module.get_function("calcTransmittanceNoslit"),
+            cuda_module.get_function("applyGaussianSlit"),
         )
-        print(
-            "Threads per block          : {0}".format(init_params_h.N_threads_per_block)
-        )
-        print(
-            "Spectral points per thread : {0}".format(init_params_h.N_points_per_thread)
-        )
-        print()
 
-    host_params_h_v0_dec = np.zeros(
-        len(v0) // init_params_h.N_threads_per_block, dtype=np.float32
-    )
-    for i in range(0, len(v0) // init_params_h.N_threads_per_block):
-        host_params_h_v0_dec[i] = v0[i * init_params_h.N_threads_per_block]
-    host_params_h_dec_size = host_params_h_v0_dec.size
-    host_params_h_da_dec = np.zeros(
-        len(v0) // init_params_h.N_threads_per_block, dtype=np.float32
-    )
-    for i in range(0, len(v0) // init_params_h.N_threads_per_block):
-        host_params_h_da_dec[i] = da[i * init_params_h.N_threads_per_block]
+        set_init_params = constant_memory_setter(cuda_module, "init_d")
+
+    else:
+        from numpy import complex64, float32, zeros
+
+        array = lambda arr: arr
+        from radis_cython_extensions import set_init_params
+
+    init_h.v_min = np.min(v_arr)  # 2000.0
+    init_h.v_max = np.max(v_arr)  # 2400.0
+    init_h.dv = (v_arr[-1] - v_arr[0]) / (len(v_arr) - 1)  # 0.002
+    init_h.N_v = len(v_arr)
+    init_h.dxG = dxG
+    init_h.dxL = dxL
+
+    init_h.N_iterations_per_thread = 1024
+    block_preparation_step_size = 128
+
+    shared_size = 0x8000  # Bytes - Size of the shared memory
+    init_h.shared_size_floats = shared_size // 4  # size of float
+
+    # init_h.N_total = init_h.N_v * init_h.N_G * init_h.N_L
+    # init_h.N_points_per_block = init_h.shared_size_floats // (init_h.N_v * init_h.N_G)
+
+    init_h.N_threads_per_block = 1024
+    init_h.N_blocks_per_grid = 4 * 256 * 256
+    init_h.N_points_per_thread = init_h.N_points_per_block // init_h.N_threads_per_block
+
+    if verbose >= 2:
+        print()
+        print("Spectral points per block  : {0}".format(init_h.N_points_per_block))
+        print("Threads per block          : {0}".format(init_h.N_threads_per_block))
+        print("Spectral points per thread : {0}".format(init_h.N_points_per_thread))
+        print()
 
-    lorentzian_param_data = init_lorentzian_params(log_2gs, na, verbose_gpu)
-    gaussian_param_data = init_gaussian_params(log_2vMm, verbose_gpu)
-    init_params_h.N_lines = int(len(v0))
+    init_h.N_lines = int(len(v0))
 
-    if verbose_gpu == 1:
-        print("Number of lines loaded: {0}".format(init_params_h.N_lines))
+    if verbose == 1:
+        print("Number of lines loaded: {0}".format(init_h.N_lines))
         print()
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("Allocating device memory and copying data...")
 
-    host_params_h_data_start.record()
+    # S_klm_d = zeros(
+    #     (2 * init_h.N_v, init_h.N_G, init_h.N_L),
+    #     order="C",
+    #     dtype=float32,
+    # )
+    spectrum_in_d = zeros(init_h.N_v + 1, dtype=complex64)
+    transmittance_noslit_d = zeros(init_h.N_v * 2, dtype=float32)
+    transmittance_FT_d = zeros(init_h.N_v + 1, dtype=complex64)
 
-    host_params_h_DLM_d_in = cp.zeros(
-        (2 * init_params_h.N_v, init_params_h.N_wG, init_params_h.N_wL),
-        order="C",
-        dtype=cp.float32,
+    # #Copy spectral data to device
+    iso_d = array(iso)
+    v0_d = array(v0)
+    da_d = array(da)
+    S0_d = array(S0)
+    El_d = array(El)
+    gamma_d = array(gamma)
+    na_d = array(na)
+
+    if verbose >= 2:
+        print("done!")
+
+    log_c2Mm_arr = np.array(
+        [0]
+        + [
+            0.5 * np.log(8 * k * np.log(2) / (c ** 2 * Mm * 1e-3 / N_A))
+            for Mm in Mm_arr[1:]
+        ]
     )
-    host_params_h_spectrum_d_in = cp.zeros(init_params_h.N_v + 1, dtype=cp.complex64)
+    for i in range(len(log_c2Mm_arr)):
+        init_h.log_c2Mm[i] = log_c2Mm_arr[i]
+    Q_interpolator_list = Q_intp_list
 
-    host_params_h_I_add = cp.zeros(init_params_h.N_lines, dtype=cp.float32)
+    log_2vMm = np.log(v0) + log_c2Mm_arr.take(iso)
+    gaussian_param_data = init_gaussian_params(log_2vMm.astype(np.float32), verbose)
+    lorentzian_param_data = init_lorentzian_params(na, gamma, verbose)
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("Copying initialization parameters to device memory...")
-    set_constant_memory("init_params")
+    set_init_params(init_h)
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("done!")
         print("Copying spectral data to device memory...")
 
-    # #Copy spectral data to device
-    host_params_h_iso_d = cp.array(iso)
-    host_params_h_v0_d = cp.array(v0)
-    host_params_h_da_d = cp.array(da)
-    host_params_h_S0_d = cp.array(S0)
-    host_params_h_El_d = cp.array(El)
-    host_params_h_log_2gs_d = cp.array(log_2gs)
-    host_params_h_na_d = cp.array(na)
-    host_params_h_log_2vMm_d = cp.array(log_2vMm)
-    host_params_h_Q_d = cp.array(Q)
-
-    host_params_h_data_stop.record()
-    host_params_h_data_stop.synchronize()
-    host_params_h_elapsedTimeData = cp.cuda.get_elapsed_time(
-        host_params_h_data_start, host_params_h_data_stop
-    )
 
-    if verbose_gpu >= 2:
-        print("done!")
+##    print("wG fast:",np.min(log_2vMm),np.max(log_2vMm))
+##
+##    log_wG_debug_h = np.log(v0) + 0.5*np.log(8*k*np.log(2)/(c**2*Mm_arr[iso]))
+##    print("wG arr:",np.min(log_wG_debug_h),np.max(log_wG_debug_h))
+
+
+def gpu_iterate(p, T, mole_fraction, l=1.0, slit_FWHM=0.0, verbose=0, gpu=False):
+    """
+    Parameters
+    ----------
+    p : float
+        pressure [bar]
+    T : float
+        temperature [K]
+    mole_fraction : float
+
+    Other Parameters
+    ----------------
+    verbose : bool, optional
+        The default is True.
+    l : TYPE, optional
+        DESCRIPTION. The default is 1.0.
+    slit_FWHM : TYPE, optional
+        DESCRIPTION. The default is 0.0.
+    gpu : TYPE, optional
+        DESCRIPTION. The default is False.
+
+    Returns
+    -------
+    abscoeff_h : TYPE
+        DESCRIPTION.
+    transmittance_h : TYPE
+        DESCRIPTION.
+
+    """
 
-    if verbose_gpu >= 2:
-        print(
-            "Time to copy data from host to device = {0} ms".format(
-                host_params_h_elapsedTimeData
-            )
-        )
+    # ----------- setup global variables -----------------
+    global init_h, iter_h
+    global iso_d, v0_d, da_d, S0_d, El_d, gamma_d, na_d
+    global spectrum_in_d, transmittance_noslit_d, transmittance_FT_d
+    global cuda_module, cuda_functions
+    global start, stop, elapsedTime
+    # ------------------------------------------------------
 
+    if gpu:
+        from cupy import asnumpy, complex64, float32, zeros
+        from cupy.cuda.runtime import deviceSynchronize
+        from cupy.fft import irfft, rfft
 
-def gpu_iterate(p, T, mole_fraction, Ia_arr, molarmass_arr, verbose_gpu):
+        (
+            fillLDM,
+            applyLineshapes,
+            calcTransmittanceNoslit,
+            applyGaussianSlit,
+        ) = cuda_functions
+        set_iter_params = constant_memory_setter(cuda_module, "iter_d")
+
+    else:
+        from numpy import complex64, float32, zeros
+        from numpy.fft import irfft, rfft
+        from radis_cython_extensions import (
+            applyGaussianSlit,
+            applyLineshapes,
+            calcTransmittanceNoslit,
+            fillLDM,
+            set_iter_params,
+        )
 
-    # ----------- setup global variables -----------------
+        asnumpy = lambda arr: arr
 
-    global host_params_h_start
-
-    global init_params_h, iter_params_h
-    global host_params_h_iso
-    global host_params_h_v0_d
-    global host_params_h_da_d
-    global host_params_h_S0_d
-    global host_params_h_El_d
-    global host_params_h_Q_d
-    global host_params_h_log_2gs_d
-    global host_params_h_na_d
-    global host_params_h_log_2vMm_d
-    global host_params_h_stop
-    global host_params_h_elapsedTime
-
-    global host_params_h_DLM_d_in
-    global host_params_h_spectrum_d_in
-
-    global host_params_h_I_add
-
-    global cuda_module
-    global host_params_h_v0_dec
-    global host_params_h_da_dec
-    global DLM
-    global I_ADD
-    # ------------------------------------------------------
+    if verbose >= 2:
+        print("Copying iteration parameters to device...")
 
-    host_params_h_start.record()
-    # test comment
-    ##    cdef int n_blocks
-    set_pT(p, T, mole_fraction, iter_params_h)
+    set_pTQ(p, T, mole_fraction, iter_h, l=l, slit_FWHM=slit_FWHM)
 
     calc_gaussian_params(
         gaussian_param_data,
-        init_params_h,
-        iter_params_h,
+        init_h,
+        iter_h,
     )
 
     calc_lorentzian_params(
         lorentzian_param_data,
-        init_params_h,
-        iter_params_h,
+        init_h,
+        iter_h,
     )
 
-    n_blocks = prepare_blocks(
-        host_params_h_v0_dec,
-        host_params_h_da_dec,
-        host_params_h_dec_size,
-        host_params_h_block_preparation_step_size,
-        iter_params_h,
-        init_params_h,
-    )
-
-    if verbose_gpu >= 2:
-        print("Copying iteration parameters to device...")
-    set_constant_memory("iter_params")
+    set_iter_params(iter_h)
 
-    if verbose_gpu >= 2:
+    if verbose >= 2:
         print("done!")
+        print("Filling LDM...")
 
-    # Zero DLM:
-    host_params_h_DLM_d_in.fill(0)
-    host_params_h_spectrum_d_in.fill(0)
+    # S_klm_d.fill(0)
 
-    host_params_h_I_add.fill(0)
+    S_klm_d = zeros(
+        (2 * init_h.N_v, iter_h.N_G, iter_h.N_L),
+        order="C",
+        dtype=float32,
+    )
 
-    if verbose_gpu >= 2:
-        print("Getting ready...")
+    spectrum_in_d.fill(0)
+    transmittance_FT_d.fill(0)
 
-    host_params_h_start_DLM.record()
+    n_threads = init_h.N_threads_per_block
+    n_blocks = init_h.N_lines // (n_threads * init_h.N_iterations_per_thread) + 1
 
-    fillDLM(
+    fillLDM(
         (n_blocks,),
-        (init_params_h.N_threads_per_block,),
+        (n_threads,),
         (
-            host_params_h_iso_d,
-            host_params_h_v0_d,
-            host_params_h_da_d,
-            host_params_h_S0_d,
-            host_params_h_El_d,
-            host_params_h_log_2gs_d,
-            host_params_h_na_d,
-            host_params_h_log_2vMm_d,
-            host_params_h_DLM_d_in,
-            host_params_h_Q_d,
-            host_params_h_I_add,
+            iso_d,
+            v0_d,
+            da_d,
+            S0_d,
+            El_d,
+            gamma_d,
+            na_d,
+            S_klm_d,
         ),
     )
+    if gpu:
+        deviceSynchronize()
 
-    cp.cuda.runtime.deviceSynchronize()
+    if verbose >= 2:
+        print("Applying lineshapes...")
 
-    # This makes the DLM array available in the calling module
-    DLM = cp.asnumpy(host_params_h_DLM_d_in)
-    I_ADD = cp.asnumpy(host_params_h_I_add)
-    # DLM /= 0.8816117 #difference between current code and benchmark.
-    host_params_h_stop_DLM.record()
-    host_params_h_stop_DLM.synchronize()
-    host_params_h_elapsedTimeDLM = cp.cuda.get_elapsed_time(
-        host_params_h_start_DLM, host_params_h_stop_DLM
-    )
+    S_klm_FT_d = rfft(S_klm_d, axis=0).astype(complex64)
 
-    if verbose_gpu >= 2:
-        print("<<<LAUNCHED>>> ")
+    if gpu:
+        deviceSynchronize()
 
-    cp.cuda.runtime.deviceSynchronize()
+    n_threads = init_h.N_threads_per_block
+    n_blocks = (init_h.N_v + 1) // n_threads + 1
+    applyLineshapes(
+        (n_blocks,),
+        (n_threads,),
+        (
+            S_klm_FT_d,
+            spectrum_in_d,
+        ),
+    )
 
-    if verbose_gpu >= 2:
-        # FFT
-        print("Performing Fourier transform...", end=" ")
-    host_params_h_DLM_d_out = cp.fft.rfft(host_params_h_DLM_d_in, axis=0)
+    if gpu:
+        deviceSynchronize()
 
-    if verbose_gpu >= 2:
-        print("done!")
+    spectrum_out_d = irfft(spectrum_in_d).astype(float32)
 
-    cp.cuda.runtime.deviceSynchronize()
-    n_threads = 1024
-    n_blocks = (init_params_h.N_v + 1) // n_threads + 1
+    if gpu:
+        deviceSynchronize()
 
-    if verbose_gpu >= 2:
-        print("Applying lineshapes...")
+    if verbose >= 2:
+        print("Done!")
+        print("Calculating transmittance...")
 
-    applyLineshapes(
+    abscoeff_h = asnumpy(spectrum_out_d)[: init_h.N_v]
+
+    ## Calc transmittance_noslit:
+    n_threads = init_h.N_threads_per_block
+    n_blocks = (2 * init_h.N_v) // n_threads + 1
+    calcTransmittanceNoslit(
         (n_blocks,),
         (n_threads,),
-        (
-            host_params_h_DLM_d_out,
-            host_params_h_spectrum_d_in,
-        ),
+        (spectrum_out_d, transmittance_noslit_d),
     )
 
-    cp.cuda.runtime.deviceSynchronize()
+    if gpu:
+        deviceSynchronize()
 
-    if verbose_gpu >= 2:
-        print("done!")
+    if verbose >= 2:
+        print("Done!")
+        print("Applying slit function...")
 
-        # inverse FFT
-        print("Performing inverse Fourier transform...")
+    ## Apply slit function:
+    transmittance_noslit_FT_d = rfft(transmittance_noslit_d).astype(complex64)
 
-    host_params_h_spectrum_d_out = cp.fft.irfft(host_params_h_spectrum_d_in)
-    cp.cuda.runtime.deviceSynchronize()
+    if gpu:
+        deviceSynchronize()
 
-    if verbose_gpu >= 2:
-        print("done!")
-    spectrum_h = (
-        host_params_h_spectrum_d_out.get()[: init_params_h.N_v] / init_params_h.dv
+    n_threads = init_h.N_threads_per_block
+    n_blocks = (init_h.N_v + 1) // n_threads + 1
+    applyGaussianSlit(
+        (n_blocks,),
+        (n_threads,),
+        (transmittance_noslit_FT_d, transmittance_FT_d),
     )
 
-    host_params_h_stop.record()
-    host_params_h_stop.synchronize()
-    host_params_h_elapsedTime = cp.cuda.get_elapsed_time(
-        host_params_h_start, host_params_h_stop
-    )
+    if gpu:
+        deviceSynchronize()
+
+    transmittance_d = irfft(transmittance_FT_d).astype(float32)
+
+    if gpu:
+        deviceSynchronize()
+
+    transmittance_h = asnumpy(transmittance_d)[: init_h.N_v]
+
+    if verbose >= 2:
+        print("done!")
 
-    if verbose_gpu == 1:
-        print("[rG = {0}%".format((np.exp(iter_params_h.log_dwG) - 1) * 100))
-        print("rL = {0}%]".format((np.exp(iter_params_h.log_dwL) - 1) * 100))
-        print("Runtime: {0}".format(host_params_h_elapsedTimeDLM))
-        print(" + {0}".format(host_params_h_elapsedTime - host_params_h_elapsedTimeDLM))
-        print(" = {0} ms".format(host_params_h_elapsedTime))
+    if verbose == 1:
         print("Finished calculating spectrum!")
 
-    return spectrum_h
+    return abscoeff_h, transmittance_h, iter_h
diff --git a/radis/lbl/loader.py b/radis/lbl/loader.py
index da62c688c..97701e82c 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
+from os.path import exists, join
 from time import time
 from uuid import uuid1
 
@@ -111,7 +111,7 @@
 ]
 """list: Known formats for Line Databases:
 
-- ``'hitran'`` : [HITRAN-2016]_ original .par format
+- ``'hitran'`` : [HITRAN-2020]_ original .par format
 - ``'hitemp'`` : [HITEMP-2010]_ original format (same format as 'hitran')
 - ``'cdsd-hitemp'`` : CDSD-HITEMP original format (CO2 only, same lines as HITEMP-2010)
 - ``'cdsd-4000'`` : [CDSD-4000]_ original format (CO2 only)
@@ -157,8 +157,8 @@
 """
 
 drop_auto_columns_for_dbformat = {
-    "hitran": ["ierr", "iref", "lmix", "gp", "gpp"],
-    "hitemp": ["ierr", "iref", "lmix", "gp", "gpp"],
+    "hitran": ["ierr", "iref", "lmix", "gpp"],
+    "hitemp": ["ierr", "iref", "lmix", "gpp"],
     "cdsd-4000": ["wang2"],
     "cdsd-hitemp": ["wang2", "lsrc"],
     "hdf5-radisdb": [],
@@ -198,12 +198,14 @@
     "iso",
     "wav",
     "int",
+    "A",
     "airbrd",
     "selbrd",
     "Tdpair",
     "Tdpsel",
     "Pshft",
     "El",
+    "gp",
 ]
 """ list: drop all columns but these if using ``drop_columns='all'`` in load_databank
 Note: nonequilibrium calculations wont be possible anymore and it wont be possible
@@ -268,7 +270,7 @@ class ConditionDict(dict):
     See Also
     --------
     :py:class:`~radis.lbl.loader.Input`,
-    :py:class:`~radis.lbl.loader.Parameter`,
+    :py:class:`~radis.lbl.loader.Parameters`,
     :py:class:`~radis.lbl.loader.MiscParams`
     """
 
@@ -446,8 +448,8 @@ class Parameters(ConditionDict):
         "db_use_cached",
         "dbformat",
         "dbpath",
-        "dlm_log_pL",
-        "dlm_log_pG",
+        "dxL",
+        "dxG",
         "export_lines",
         "export_populations",
         "folding_thresh",
@@ -459,7 +461,7 @@ class Parameters(ConditionDict):
         "parfuncpath",
         "parsum_mode",
         "pseudo_continuum_threshold",
-        "sparse_dlm",
+        "sparse_ldm",
         "warning_broadening_threshold",
         "warning_linestrength_cutoff",
         "wavenum_max_calc",
@@ -493,18 +495,18 @@ def __init__(self):
         self.wavenum_min_calc = None  #: float: minimum calculated wavenumber (cm-1) initialized by SpectrumFactory
         self.waveunit = "cm-1"  #: waverange unit: should be cm-1.
         self.wstep = None  #: float: spectral resolution (cm-1)
-        self.dlm_log_pL = _lorentzian_step(
+        self.dxL = _lorentzian_step(
             0.01
-        )  #: float : Lorentzian step for DLM lineshape database. Default _lorentzian_step(0.01)
-        self.dlm_log_pG = _gaussian_step(
+        )  #: float : Lorentzian step for LDM lineshape database. Default _lorentzian_step(0.01)
+        self.dxG = _gaussian_step(
             0.01
-        )  #: float : Gaussian step DLM lineshape database. Default _gaussian_step(0.01)
+        )  #: float : Gaussian step LDM lineshape database. Default _gaussian_step(0.01)
         # self.add_at_used = None  # use Cython-accelerated code
         self.include_neighbouring_lines = True
         """bool: if ``True``, includes the contribution of off-range, neighbouring
         lines because of lineshape broadening. Default ``True``."""
         self.parsum_mode = "full summation"  #: int : "full summation" or "tabulation"  . calculation mode of parittion function. See :py:class:`~radis.levels.partfunc.RovibParFuncCalculator`
-        self.sparse_dlm = "auto"  #: str: "auto", True, False  . Sparse DLM calculation. See :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape_DLM`
+        self.sparse_ldm = "auto"  #: str: "auto", True, False  . Sparse LDM calculation. See :py:meth:`radis.lbl.broadening.BroadenFactory._apply_lineshape_LDM`
 
 
 class MiscParams(ConditionDict):
@@ -930,7 +932,7 @@ def fetch_databank(
         Parameters
         ----------
         source: ``'hitran'``, ``'hitemp'``, ``'exomol'``
-            [Download database lines from the latest HITRAN (see [HITRAN-2016]_),
+            [Download database lines from the latest HITRAN (see [HITRAN-2020]_),
             HITEMP (see [HITEMP-2010]_  )] or EXOMOL see [ExoMol-2020]_  ) databases.
         database: ``'full'``, ``'range'``, name of an ExoMol database, or ``'default'``
             if fetching from HITRAN, ``'full'`` download the full database and register
@@ -949,6 +951,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
+
 
         Other Parameters
         ----------------
@@ -1052,7 +1057,11 @@ def fetch_databank(
             if database == "default":
                 database = "full"
         elif source == "exomol":
-            dbformat = "exomol-radisdb"  # downloaded in RADIS local databases ~/.radisdb  # Note @EP : still WIP.
+            dbformat = (
+                "exomol-radisdb"  # downloaded in RADIS local databases ~/.radisdb
+            )
+
+        local_databases = config["DEFAULT_DOWNLOAD_PATH"]
 
         if [parfuncfmt, source].count("exomol") == 1:
             self.warn(
@@ -1112,10 +1121,10 @@ def fetch_databank(
         from os import environ
 
         if source == "hitran":
-            self.reftracker.add(doi["HITRAN-2016"], "line database")  # [HITRAN-2016]_
-            self.reftracker.add(doi["Astroquery"], "data retrieval")  # [Astroquery]_
+            self.reftracker.add(doi["HITRAN-2020"], "line database")  # [HITRAN-2020]_
 
             if database == "full":
+                self.reftracker.add(doi["HAPI"], "data retrieval")  # [HAPI]_
 
                 # quick fix for https://github.com/radis/radis/issues/401
                 if memory_mapping_engine == "auto":
@@ -1125,6 +1134,14 @@ def fetch_databank(
                             print(
                                 f"Spyder IDE detected. Memory-mapping-engine set to '{memory_mapping_engine}' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
                             )
+                    # temp fix for vaex not building on RTD
+                    # see https://github.com/radis/radis/issues/404
+                    elif 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"
 
@@ -1136,6 +1153,7 @@ def fetch_databank(
                 df, local_paths = fetch_hitran(
                     molecule,
                     isotope=isotope_list,
+                    local_databases=join(local_databases, "hitran"),
                     load_wavenum_min=wavenum_min,
                     load_wavenum_max=wavenum_max,
                     columns=columns,
@@ -1147,7 +1165,16 @@ def fetch_databank(
                 )
                 self.params.dbpath = ",".join(local_paths)
 
+                # ... explicitely write all isotopes based on isotopes found in the database
+                if isotope == "all":
+                    self.input.isotope = ",".join(
+                        [str(k) for k in self._get_isotope_list(df=df)]
+                    )
+
             elif database == "range":
+                self.reftracker.add(
+                    doi["Astroquery"], "data retrieval"
+                )  # [Astroquery]_
 
                 # Query one isotope at a time
                 if isotope == "all":
@@ -1200,6 +1227,14 @@ def fetch_databank(
                         print(
                             f"Spyder IDE detected. Memory-mapping-engine set to '{memory_mapping_engine}' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
                         )
+                # temp fix for vaex not building on RTD
+                # see https://github.com/radis/radis/issues/404
+                elif 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"
 
@@ -1218,6 +1253,7 @@ def fetch_databank(
             df, local_paths = fetch_hitemp(
                 molecule,
                 isotope=isotope_list,
+                local_databases=join(local_databases, "hitemp"),
                 load_wavenum_min=wavenum_min,
                 load_wavenum_max=wavenum_max,
                 columns=columns,
@@ -1246,6 +1282,14 @@ def fetch_databank(
                         print(
                             f"Spyder IDE detected. Memory-mapping-engine set to '{memory_mapping_engine}' (less powerful than 'vaex' but Spyder user experience freezes). See https://github.com/spyder-ide/spyder/issues/16183. Change this behavior by setting the radis.config['MEMORY_MAPPING_ENGINE'] key"
                         )
+                # temp fix for vaex not building on RTD
+                # see https://github.com/radis/radis/issues/404
+                elif 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"
 
@@ -1276,6 +1320,7 @@ def fetch_databank(
                     molecule,
                     database=database,
                     isotope=iso,
+                    local_databases=join(local_databases, "exomol"),
                     load_wavenum_min=wavenum_min,
                     load_wavenum_max=wavenum_max,
                     columns=columns,
@@ -1327,6 +1372,9 @@ def fetch_databank(
                 + "{0:.2f}-{1:.2f} cm-1".format(wavenum_min, wavenum_max)
             )
 
+        # Always sort line database by wavenumber (required to SPARSE_WAVERANGE mode)
+        df.sort_values("wav", ignore_index=True, inplace=True)
+
         # Post-processing of the line database
         # (note : this is now done in 'fetch_hitemp' before saving to the disk)
         # spectroscopic quantum numbers will be needed for nonequilibrium calculations, and line survey.
@@ -2169,8 +2217,8 @@ def load_and_concat(files):
                     elif dbformat in ["hitran", "hitemp"]:
                         if dbformat == "hitran":
                             self.reftracker.add(
-                                doi["HITRAN-2016"], "line database"
-                            )  # [HITRAN-2016]_
+                                doi["HITRAN-2020"], "line database"
+                            )  # [HITRAN-2020]_
                         if dbformat == "hitemp":
                             self.reftracker.add(
                                 doi["HITEMP-2010"], "line database"
@@ -2447,7 +2495,9 @@ def _retrieve_from_database(self, ignore_misc=True):
 
         conditions = {
             k: v
-            for (k, v) in self.get_conditions(ignore_misc=ignore_misc).items()
+            for (k, v) in self.get_conditions(
+                ignore_misc=ignore_misc, add_config=True
+            ).items()
             if k in self.SpecDatabase.conditions()
         }
         conditions = {
@@ -2456,6 +2506,14 @@ def _retrieve_from_database(self, ignore_misc=True):
             if k not in self._autoretrieveignoreconditions
         }
 
+        if "wstep" in conditions and conditions["wstep"] == "auto":
+            if "GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD" in conditions:
+                # ignore wstep='auto' but still make sure that 'GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD' is the same
+                del conditions["wstep"]
+                # TODO Refactor. Include this in a large Parameter metaclass where
+                # 'GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD' is a parameter from which
+                # depends 'wstep' evaluation
+
         s = self.SpecDatabase.get(**conditions)
         if len(s) == 1:
             if self.verbose:
@@ -2791,7 +2849,7 @@ def get_partition_function_molecule(self, molecule):
 
         return parsum
 
-    def get_conditions(self, ignore_misc=False):
+    def get_conditions(self, ignore_misc=False, add_config=False):
         """Get all parameters defined in the SpectrumFactory.
 
         Other Parameters
@@ -2809,6 +2867,14 @@ def get_conditions(self, ignore_misc=False):
         if not ignore_misc:
             vardict.update(self.misc.get_params())
 
+        if add_config:
+            import radis
+            from radis.spectrum.utils import CONFIG_PARAMS
+
+            vardict.update(
+                {k: v for k, v in radis.config.items() if k in CONFIG_PARAMS}
+            )
+
         return vardict
 
     def warn(self, message, category="default", level=0):
diff --git a/radis/lbl/overp.py b/radis/lbl/overp.py
index b3a5a451f..508b67bc2 100644
--- a/radis/lbl/overp.py
+++ b/radis/lbl/overp.py
@@ -247,7 +247,7 @@ def vib_lvl_name_cdsd(p, c, N):
 
         # Add levels
         def add_viblvl(row):
-            """"""
+            """ """
             #    row['vib_lvl'] = '({p},{c},{N})'.format(**dict([(k,int(row[k])) for k in ['p', 'c', 'N']]))
             #            row['viblvl'] = '({p},{c})'.format(**dict([(k,int(row[k])) for k in ['p', 'c']]))
             row["viblvl"] = vib_lvl_name_cdsd(row.p, row.c, row.N)
diff --git a/radis/levels/partfunc.py b/radis/levels/partfunc.py
index 12ebbed51..51b5e034f 100644
--- a/radis/levels/partfunc.py
+++ b/radis/levels/partfunc.py
@@ -228,7 +228,7 @@ def __init__(self, electronic_state, mode="full summation", verbose=False):
             from radis.misc.log import printwarn
 
             printwarn(
-                "Spyder IDE detected while using parsum_mode='tabulation', which depends on `vaex`. This is the fastest way to compute partition functions, but Vaex processes may be stuck if ran from Spyder. See https://github.com/radis/radis/issues/338. You may consider using another IDE, or use `parsum_mode='full summation'` for the moment (note: starting another iPython console somehow releases the freeze in Spyder) \n"
+                "Spyder IDE detected while using parsum_mode='tabulation', which depends on `vaex`. This is the fastest way to compute partition functions, but Vaex processes may be stuck if ran from Spyder. See https://github.com/radis/radis/issues/338. Quick fix: starting a new console releases the lock, usually for the rest of your session. You may consider using another IDE, or use `parsum_mode='full summation'` for the moment (note: starting another iPython console somehow releases the freeze in Spyder) \n"
             )
 
         self.mode = mode
diff --git a/radis/misc/arrays.py b/radis/misc/arrays.py
index eb183eeba..335d346c9 100644
--- a/radis/misc/arrays.py
+++ b/radis/misc/arrays.py
@@ -477,6 +477,10 @@ def numpy_add_at(LDM, k, l, m, I):
 except (ModuleNotFoundError):
     add_at = numpy_add_at
     #  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
+#     ValueError: numpy.ndarray size changed, may indicate binary incompatibility. Expected 88 from C header, got 80 from PyObject
+# we may consider escaping this.
 else:
     add_at = rcx.add_at
 
@@ -490,10 +494,10 @@ def numpy_add_at(LDM, k, l, m, I):
 #     """
 #     # # Cleaner version, but about 2x slower on real-life test cases:
 #     # #
-#     # %timeit non_zero_values_around(DLM[:, l, m], truncation)
+#     # %timeit non_zero_values_around(LDM[:, l, m], truncation)
 #     # 2.92 ms ± 99.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
 
-#     # %timeit non_zero_values_around2(DLM[:, l, m], truncation)
+#     # %timeit non_zero_values_around2(LDM[:, l, m], truncation)
 #     # 5.11 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
 #     # --------------
 
@@ -687,8 +691,13 @@ def boolean_array_from_ranges(ranges, n):
 )
 def sparse_add_at(ki0, Iv0, Iv1, weight, max_range, truncation_pts):
     """Returns (start, stop) of non-zero ranges, and intensity ``I`` (length
-    ``truncation_pts``) for all lines ``Iv0`` and ``Iv1`` summed at ``ki0``
-    and ``ki0+1`` with weights ``weight``"""
+    ``truncation_pts``) for all line intensities ``Iv0`` and ``Iv1`` at position ``ki0``
+    and ``ki0+1`` with weights ``weight``
+
+    .. warning::
+        ``ki0`` must be sorted.
+    """
+    assert is_sorted(ki0)
     I = np.zeros(max_range)
     L = [
         (0, 0) for k in range(0)
@@ -708,15 +717,15 @@ def sparse_add_at(ki0, Iv0, Iv1, weight, max_range, truncation_pts):
         if pos > end:
             # close the previous range:
             L.append((start, end))
-            # reopen new range :
+            # reopen new range as `[pos-n, pos+n]`:
             start = max(pos - n, 0)
             end = min(
-                pos + 1 + n, max_range
-            )  # TODO: check if not +2 because line is added on ki0 and ki1 ()
+                pos + 2 + n, max_range
+            )  # TODO:  +2 because line is added on ki0 and ki0+1
         else:
             # we're in the middle of a range:
             # keep the start, move the end
-            end = min(pos + 1 + n, max_range)
+            end = min(pos + 2 + n, max_range)
 
         # Sum intensity  (equivalent of "add-at")
         I[pos] += Iv0[i] * weight[i]
diff --git a/radis/misc/basics.py b/radis/misc/basics.py
index 593c8c431..91640fe27 100644
--- a/radis/misc/basics.py
+++ b/radis/misc/basics.py
@@ -486,6 +486,12 @@ def is_float(a):
     return type(a) in [float, np.float64, np.int32, np.float32, int, np.int64]
 
 
+def is_range(a):
+    from radis.tools.plot_tools import ParamRange
+
+    return isinstance(a, ParamRange)
+
+
 def is_number(s):
     """Return True if ``s`` is a number.
 
diff --git a/radis/phys/units.py b/radis/phys/units.py
index 163a07df7..42f2170a8 100644
--- a/radis/phys/units.py
+++ b/radis/phys/units.py
@@ -15,6 +15,7 @@ def Unit(st, *args, **kwargs):
     Changes compare to Astropy standards:
 
     - "µm" is accepted and converted to "um"
+    - "/molecule" or "/molec" are removed, but do not raise an error
     - we do not raise a warning if multiple slashes, e.g. "mW/cm2/sr/nm"
 
     Examples
@@ -31,6 +32,8 @@ def Unit(st, *args, **kwargs):
 
     try:
         st = st.replace("µ", "u")
+        st = st.replace("/molecule", "")
+        st = st.replace("/molec", "")
     except AttributeError:
         pass
 
diff --git a/radis/phys/units.txt b/radis/phys/units.txt
index ba4345171..d704d6e35 100644
--- a/radis/phys/units.txt
+++ b/radis/phys/units.txt
@@ -11,4 +11,4 @@ cm3 = cm ** 3
 m2 = m ** 2
 m3 = m ** 3
 
-cm_1 = 1 / cm
\ No newline at end of file
+cm_1 = 1 / cm
diff --git a/radis/spectrum/compare.py b/radis/spectrum/compare.py
index a8cffdcb2..e34896a1f 100644
--- a/radis/spectrum/compare.py
+++ b/radis/spectrum/compare.py
@@ -511,19 +511,11 @@ def get_default_units(
             var = list(params)[0]
             if var.replace("_noslit", "") in params:
                 var = var.replace("_noslit", "")
-    # ... check variable exist
+    # ... create variable if it doesnt exist
     if var not in s1.get_vars():
-        raise ValueError(
-            "{0} not defined in Spectrum {1}. Use one of : {2}".format(
-                var, s1.get_name(), s1.get_vars()
-            )
-        )
+        s1.get(var)
     if var not in s2.get_vars():
-        raise ValueError(
-            "{0} not defined in Spectrum {1}. Use one of : {2}".format(
-                var, s2.get_name(), s2.get_vars()
-            )
-        )
+        s2.get(var)
     if Iunit == "default":
         try:
             Iunit = s1.units[var]
diff --git a/radis/spectrum/rescale.py b/radis/spectrum/rescale.py
index 6fef75bf0..b4e533db4 100644
--- a/radis/spectrum/rescale.py
+++ b/radis/spectrum/rescale.py
@@ -7,6 +7,8 @@
 unload the spectrum.py file
 
 
+Equations derived from the code using `pytexit <https://pytexit.readthedocs.io/en/latest/>`__
+
 -------------------------------------------------------------------------------
 """
 
@@ -33,6 +35,7 @@
     "emissivity_noslit",
     "radiance",
     "transmittance",
+    "xsection",
 ]
 """list: List of all spectral variables sorted by priority during recomputation
 (ex: first get abscoeff, then try to calculate emisscoeff, etc.)
@@ -56,7 +59,12 @@
 
 
 def _build_update_graph(
-    spec, optically_thin=None, equilibrium=None, path_length=None, no_change=False
+    spec,
+    optically_thin=None,
+    equilibrium=None,
+    path_length=None,
+    p_T_x=None,
+    no_change=False,
 ):
     """Find inheritances properties (dependencies and equivalences) between all
     spectral variables based on the spectrum conditions (equilibrium, optically
@@ -64,27 +72,25 @@ def _build_update_graph(
 
     Parameters
     ----------
-
     spec: Spectrum
         a :class:`~radis.spectrum.spectrum.Spectrum` object
 
     Other Parameters
     ----------------
-
     optically_thin: boolean
         know whether the Spectrum should be considered optically thin to build
         the equivalence graph tree. If None, the value stored in the Spectrum is used.
         Default ``None``
-
     equilibrium: boolean
         know whether the Spectrum should be considered at equilibrium to build
         the equivalence graph tree. If None, the value stored in the Spectrum is used.
         Default ``None``
-
     path_length: boolean
         know whether the path length is given to build the equivalence graph tree.
         If None, ``path_length`` is looked up in the Spectrum condition. Default ``None``
-
+    p_T_x: boolean
+        whether all of the temperature, pressure and mole fractions are known. If None,
+        they are looked up in the Spectrum conditions. Default ``None``
     no_change: boolean
         if True, signals that we are somehow rescaling without changing path length
         nor mole fractions, i.e, all quantities can be recomputed from themselves...
@@ -141,6 +147,12 @@ def _build_update_graph(
         path_length = "path_length" in spec.conditions
     if optically_thin is None:
         optically_thin = spec.is_optically_thin()
+    if p_T_x is None:
+        p_T_x = (
+            "Tgas" in spec.conditions
+            and "pressure_mbar" in spec.conditions
+            and "mole_fraction" in spec.conditions
+        )
     if equilibrium is None:
         # Read the Spectrum conditions. By default, use False.
         equilibrium = spec.conditions.get(
@@ -161,6 +173,7 @@ def _build_update_graph(
         "radiance",
         "radiance_noslit",
         "transmittance_noslit",
+        "xsection",
     ]
     assert all_in(all_keys, CONVOLUTED_QUANTITIES + NON_CONVOLUTED_QUANTITIES)
 
@@ -221,6 +234,10 @@ def derives_from(what, *from_keys):
             derives_from("radiance_noslit", ["emisscoeff", "abscoeff"])
             derives_from("emisscoeff", ["radiance_noslit", "abscoeff"])
 
+    if p_T_x:
+        derives_from("abscoeff", ["xsection"])
+        derives_from("xsection", ["abscoeff"])
+
     if slit:
         if __debug__:
             printdbg("... build_graph: slit given > convoluted keys can be recomputed")
@@ -232,7 +249,8 @@ def derives_from(what, *from_keys):
             printdbg("... build_graph: equilibrium > all keys derive from one")
         # Anything can be recomputed from anything
         for key in all_keys:
-            if key in NON_CONVOLUTED_QUANTITIES:
+            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]
                 derives_from(key, *all_but_k)
 
@@ -349,28 +367,23 @@ def get_recompute(spec, wanted, no_change=False, true_path_length=None):
 
     Parameters
     ----------
-
     spec: Spectrum
         a :class:`~radis.spectrum.spectrum.Spectrum` object
-
     wanted: list
         list of quantities to recompute
 
 
     Other Parameters
     ----------------
-
     no_change: boolean
         if True, signals that we are somehow rescaling without changing path length
         nor mole fractions, i.e, all quantities can be recomputed from themselves...
-
     true_path_length: boolean
         know whether the path length is given to build the equivalence graph tree.
         If None, ``path_length`` is looked up in the Spectrum condition. Default ``None``
 
     Returns
     -------
-
     recompute: list
         list of quantities needed
 
@@ -482,32 +495,25 @@ def update(
     """Calculate missing quantities that can be derived from the current
     quantities and conditions.
 
-    e.g: if path_length and emisscoeff are given, radiance_noslit can be recalculated
-    if in an optically thin configuration, else if abscoeff is also given
+    e.g: if `path_length` and `emisscoeff` are given, `radiance_noslit` can be recalculated
+    if `abscoeff` is also given, or without `abscoeff` in an optically thin configuration, else
 
 
     Parameters
     ----------
-
     spec: Spectrum
-
     quantity: str, ``'same'``, ``'all'``, or list of str
         name of the spectral quantity to recompute. If ``'same'``, only the quantities
         in the Spectrum are recomputed. If ``'all'``, then all quantities that can
         be derived are recomputed. Default ``'all'``. See
         :py:data:`~radis.spectrum.utils.CONVOLUTED_QUANTITIES`
         and :py:data:`~radis.spectrum.utils.NON_CONVOLUTED_QUANTITIES`
-
     optically_thin: True, False, or ``'default'``
-        determines whether to calculate radiance with or without self absorption.
-        If 'default', the value is determined from the self_absorption key
-        in Spectrum.conditions. If not given, False is taken. Default 'default'
-        Also updates the self_absorption value in conditions (creates it if
+        determines whether to calculate radiance with or without self-absorption.
+        If ``'default'``, the value is determined from the ``'self_absorption'`` key
+        in Spectrum.conditions. If not given, ``False`` is taken. Default ``'default'``
+        Also updates the ``'self_absorption'`` key in conditions (creates it if
         doesnt exist)
-
-    Other Parameters
-    ----------------
-
     assume_equilibrium: boolean
         if ``True``, only absorption coefficient ``abscoeff`` is recomputed
         and all values are derived from a calculation under equilibrium,
@@ -581,6 +587,8 @@ def update(
         if not k in spec.units:
             raise ValueError("{0} added but unit is unknown".format(k))
 
+    return spec
+
 
 # Rescale functions
 
@@ -599,7 +607,7 @@ def rescale_abscoeff(
     extra,
     true_path_length,
 ):
-    """
+    r"""
 
     Parameters
     ----------
@@ -609,6 +617,38 @@ def rescale_abscoeff(
     old_path_length: float
         path length in cm
 
+    References
+    ----------
+
+    Scale absorption coefficient :
+
+    .. math::
+
+        k_2=k_1 \frac{x_2}{x_1}
+
+    Or recompute from the absorbance:
+
+    .. math::
+
+        k_2=\frac{A_1}{L_1} \cdot \frac{x_2}{x_1}
+
+    .. note::
+
+        scaling mole fractions is not strictly valid because the lineshape changes slightly if resonant
+        and non-resonant broadening coefficients are different
+
+    Or from the transmittance :
+
+    .. math::
+
+        k_2=\frac{-\ln(T_1)}{L_1} \cdot \frac{x_2}{x_1}
+
+    Or from the cross-sections :
+
+    .. math::
+
+        \k_2 =\sigma_1 * \frac{x p}{k_b T}  \cdot \frac{x_2}{x_1}
+
     """
 
     unit = None
@@ -626,13 +666,13 @@ def rescale_abscoeff(
         _, abscoeff_init = spec.get("abscoeff", wunit=wunit)
     elif "absorbance" in initial and true_path_length:  # aka: true path_lengths given
         if __debug__:
-            printdbg("... rescale: abscoeff k1 = A/L1")
+            printdbg("... rescale: abscoeff k_1 = A_1/L_1")
         _, A = spec.get("absorbance", wunit=wunit)
         abscoeff_init = A / old_path_length  # recalculate initial
         unit = "cm-1"
     elif "transmittance_noslit" in initial and true_path_length:
         if __debug__:
-            printdbg("... rescale: abscoeff k1 = -ln(T1)/L1")
+            printdbg("... rescale: abscoeff k_1 = -ln(T_1)/L_1")
         # Get abscoeff from transmittance
         _, T1 = spec.get("transmittance_noslit", wunit=wunit)
 
@@ -650,23 +690,41 @@ def rescale_abscoeff(
         # Else, let's calculate it
         abscoeff_init = -ln(T1) / old_path_length  # recalculate initial
         unit = "cm-1"
+    elif (
+        "xsection" in initial
+        and "Tgas" in spec.conditions
+        and "pressure_mbar" in spec.conditions
+        and "mole_fraction" in spec.conditions
+    ):
+        if __debug__:  # cm-1
+            printdbg("... rescale: abscoeff k_2 = XS_2 * (x * p) / (k_b * T)")
+        xsection = rescaled["xsection"]  # x already scaled
+        pressure_Pa = spec.conditions["pressure_mbar"] * 1e2
+        x = spec.conditions["mole_fraction"]
+        Tgas = spec.conditions["Tgas"]  # K
+        from radis.phys.constants import k_b
+
+        abscoeff_init = xsection * (x * pressure_Pa / k_b / Tgas) * 1e-6  # cm2
+        if "xsection" in spec.units:
+            assert spec.units["xsection"] == "cm2"
+        unit = "cm-1"
+
     elif "abscoeff" in extra:  # cant calculate this one but let it go
         abscoeff_init = None
     else:
         raise ValueError(
-            "Can't rescale abscoeff if transmittance_noslit ({0}) ".format(
+            "Can't rescale abscoeff if not all of the following are given : transmittance_noslit ({0}) ".format(
                 "transmittance_noslit" in initial
             )
             + "or absorbance ({0}), ".format("absorbance" in initial)
-            + "and true_path_length ({0}) ".format(true_path_length)
-            + "are not given. Use optically_thin?"
+            + "and true_path_length ({0}). ".format(true_path_length)
         )
 
     # Then export rescaled value
     # --------------------
     if abscoeff_init is not None:
         if __debug__:
-            printdbg("... rescale: abscoeff k2 = k1 * N2/N1")
+            printdbg("... rescale: abscoeff k_2 = k_1 * (x_2/x_1)")
         abscoeff = abscoeff_init * new_mole_fraction / old_mole_fraction  # rescale
         rescaled["abscoeff"] = abscoeff
     if unit is not None:
@@ -753,12 +811,43 @@ def rescale_emisscoeff(
     extra,
     true_path_length,
 ):
-    """
+    r"""
 
     Parameters
     ----------
 
     spec: Spectrum
+
+
+    References
+    ----------
+
+    If optically thin, compute from the radiance :
+
+    .. math::
+
+        j_2=\frac{I_1}{L_1} \cdot \frac{x_2}{x_1}
+
+
+    In the general case, compute from the radiance and the absorption coefficient :
+
+    .. math::
+
+        j_2=\frac{k_1 I_1}{1-\operatorname{exp}\left(-k_1 L_1\right)} \cdot \frac{x_2}{x_1}
+
+    .. note::
+
+        scaling path length is always valid ; scaling mole fractions is not
+        strictly valid because the lineshape changes slightly if resonant
+        and non-resonant broadening coefficients are different
+
+    or from the radiance and the transmittance:
+
+    .. math::
+
+        j_2=\frac{k_1 I_1}{1-T_1} \frac{x_2}{x_1}
+
+
     """
 
     unit = None
@@ -788,14 +877,14 @@ def get_unit(unit_radiance):
 
     elif "radiance_noslit" in initial and true_path_length and optically_thin:
         if __debug__:
-            printdbg("... rescale: emisscoeff j1 = I1/L1")
+            printdbg("... rescale: emisscoeff j_1 = I_1/L_1")
         _, I = spec.get("radiance_noslit", wunit=wunit, Iunit=units["radiance_noslit"])
         emisscoeff_init = I / old_path_length  # recalculate initial
         unit = get_unit(units["radiance_noslit"])
 
     elif "radiance_noslit" in initial and true_path_length and "abscoeff" in initial:
         if __debug__:
-            printdbg("... rescale: emisscoeff j1 = k1*I1/(1-exp(-k1*L1))")
+            printdbg("... rescale: emisscoeff j_1 = k_1*I_1/(1-exp(-k_1*L_1))")
         # get emisscoeff from (initial) abscoeff and (initial) radiance
         _, I = spec.get("radiance_noslit", wunit=wunit, Iunit=units["radiance_noslit"])
         _, k = spec.get("abscoeff", wunit=wunit, Iunit=units["abscoeff"])
@@ -821,7 +910,7 @@ def get_unit(unit_radiance):
         and "transmittance_noslit" in initial
     ):
         if __debug__:
-            printdbg("... rescale: emisscoeff j1 = k1*I1/(1-T1)")
+            printdbg("... rescale: emisscoeff j_1 = k_1*I_1/(1-T_1)")
         # get emisscoeff from (initial) transmittance and (initial) radiance
         _, I = spec.get("radiance_noslit", wunit=wunit, Iunit=units["radiance_noslit"])
         _, T = spec.get(
@@ -846,7 +935,7 @@ def get_unit(unit_radiance):
 
     else:
         if optically_thin:
-            msg = "Can't calculate emisscoeff if true path_length ({0})".format(
+            msg = "Can't calculate emisscoeff if path_length ({0})".format(
                 true_path_length
             ) + "and initial radiance_noslit ({0}) are not all given".format(
                 "radiance_noslit" in initial
@@ -859,14 +948,12 @@ def get_unit(unit_radiance):
                 raise ValueError(msg)
         else:
             msg = (
-                "Trying to get the emission coefficient (emisscoeff) in non optically "
-                + "thin case. True path_length ({0}), radiance_noslit ({1}) ".format(
+                "Trying to get the emission coefficient (emisscoeff) for a non-optically "
+                + "thin column. Among the following required quantities, not all of them are given : path_length ({0}), radiance_noslit ({1}) ".format(
                     true_path_length, "radiance_noslit" in initial
                 )
-                + "and abscoeff ({0}) are needed but not all given. ".format(
-                    "abscoeff" in initial
-                )
-                + "Try optically_thin? See known Spectrum conditions with "
+                + "and abscoeff ({0}). ".format("abscoeff" in initial)
+                + "See known Spectrum conditions with "
                 + "print(Spectrum)"
             )
             if "emisscoeff" in extra:  # cant calculate this one but let it go
@@ -880,7 +967,7 @@ def get_unit(unit_radiance):
     # -----------------------
     if emisscoeff_init is not None:
         if __debug__:
-            printdbg("... rescale: emisscoeff j2 = j1 * N2/N1")
+            printdbg("... rescale: emisscoeff j_2 = j_1 * (x_2/x_1)")
         # Now rescale for mole fractions
         emisscoeff = (
             emisscoeff_init * new_mole_fraction / old_mole_fraction
@@ -914,6 +1001,39 @@ def rescale_absorbance(
     ----------
 
     spec: Spectrum
+
+    References
+    ----------
+
+    Rescale the absorbance:
+
+    .. math::
+
+        A_2=A_1 \frac{x_2}{x_1} \frac{L_2}{L_1}
+
+    .. note::
+
+        scaling path length is always valid ; scaling mole fractions is not
+        strictly valid because the lineshape changes slightly if resonant
+        and non-resonant broadening coefficients are different
+
+    Or compute from the absorption coefficient :
+
+    .. math::
+
+        A_2=k_2 L_2
+
+    Or from the transmittance :
+
+    .. math::
+
+        A_2=-\ln(T1) \frac{x_2}{x_1} \frac{L_2}{L_1}
+
+    See Also
+    --------
+    :py:attr:`~radis.spectrum.rescale.rescale_abscoeff`,
+    :py:attr:`~radis.spectrum.rescale.rescale_transmittance_noslit`,
+
     """
 
     unit = None
@@ -930,22 +1050,24 @@ def rescale_absorbance(
 
     if "absorbance" in initial:
         if __debug__:
-            printdbg("... rescale: absorbance A2 = A1*N2/N1*L2/L1")
+            printdbg("... rescale: absorbance A_2 = A_1*(x_2/x_1)*(L_2/L_1)")
         _, absorbance = spec.get(
             "absorbance", wunit=waveunit, Iunit=units["absorbance"]
         )
         absorbance *= new_mole_fraction / old_mole_fraction  # rescale x
         absorbance *= new_path_length / old_path_length  # rescale L
         unit = ""
-    elif "abscoeff" in rescaled and true_path_length:
+    elif "abscoeff" in rescaled and true_path_length:  # in cm
         if __debug__:
-            printdbg("... rescale: absorbance A2 = j2*L2")
+            printdbg("... rescale: absorbance A_2 = k_2*L_2")
         abscoeff = rescaled["abscoeff"]  # x already scaled
         absorbance = abscoeff * new_path_length  # calculate L
+        if "abscoeff" in spec.units:
+            assert spec.units["abscoeff"] == "cm-1"
         unit = ""
     elif "transmittance_noslit" in initial and true_path_length:
         if __debug__:
-            printdbg("... rescale: absorbance A2 = -ln(T1)*N2/N1*L2/L1")
+            printdbg("... rescale: absorbance A_2 = -ln(T1)*(x_2/x_1)*(L_2/L_1)")
         # Get absorbance from transmittance
         _, T1 = spec.get("transmittance_noslit", wunit=waveunit)
 
@@ -1005,12 +1127,46 @@ def rescale_transmittance_noslit(
     extra,
     true_path_length,
 ):
-    """
+    r"""
 
     Parameters
     ----------
 
     spec: Spectrum
+
+
+    References
+    ----------
+
+    Compute from the absorbance :
+
+    .. math::
+
+        \tau_2=\operatorname{exp}\left(-A_2\right)
+
+    Or from the absorption coefficient and the new path length :
+
+    .. math::
+
+        \tau_2=\operatorname{exp}\left(-k_2 L_2\right)
+
+    Or from the previous transmittance :
+
+    .. math::
+
+        \tau_2=\operatorname{exp}\left(\ln(T_1) \frac{x_2}{x_1} \frac{L_2}{L_1}\right)
+
+    .. note::
+
+        scaling path length is always valid ; scaling mole fractions is not
+        strictly valid because the lineshape changes slightly if resonant
+        and non-resonant broadening coefficients are different
+
+    See Also
+    --------
+    :py:attr:`~radis.spectrum.rescale.rescale_abscoeff`,
+    :py:attr:`~radis.spectrum.rescale.rescale_absorbance`,
+    :py:attr:`~radis.spectrum.rescale.rescale_transmittance_noslit`,
     """
 
     unit = None
@@ -1031,13 +1187,13 @@ def get_unit():
     # Rescale
     if "absorbance" in rescaled:
         if __debug__:
-            printdbg("... rescale: transmittance_noslit T2 = exp(-A2)")
+            printdbg("... rescale: transmittance_noslit T_2 = exp(-A_2)")
         absorbance = rescaled["absorbance"]  # x and L already scaled
         transmittance_noslit = exp(-absorbance)  # recalculate
         unit = get_unit()
     elif "abscoeff" in rescaled and true_path_length:
         if __debug__:
-            printdbg("... rescale: transmittance_noslit T2 = exp(-j2*L2)")
+            printdbg("... rescale: transmittance_noslit T_2 = exp(-k_2*L_2)")
         abscoeff = rescaled["abscoeff"]  # x already scaled
         absorbance = abscoeff * new_path_length  # calculate
         transmittance_noslit = exp(-absorbance)  # recalculate
@@ -1045,8 +1201,8 @@ def get_unit():
     elif "transmittance_noslit" in initial:
         if __debug__:
             printdbg(
-                "... rescale: transmittance_noslit T2 = "
-                + "exp( ln(T1) * N2/N1 * L2/L1)"
+                "... rescale: transmittance_noslit T_2 = "
+                + "exp( ln(T_1) * (x_2/x_1) * (L_2/L_1))"
             )
         # get transmittance from initial transmittance
         _, T1 = spec.get(
@@ -1151,12 +1307,55 @@ def rescale_radiance_noslit(
     extra,
     true_path_length,
 ):
-    """
+    r"""
 
     Parameters
     ----------
 
     spec: Spectrum
+
+
+    References
+    ----------
+
+    If optically thin, calculate from the emission coefficient :
+
+    .. math::
+
+        I_2=j_2 L_2
+
+    or scale the existing radiance :
+
+    .. math::
+
+        I_2=\frac{\frac{I_1 x_2}{x_1} L_2}{L_1}
+
+    .. note::
+
+        scaling path length is always valid ; scaling mole fractions is not
+        strictly valid because the lineshape changes slightly if resonant
+        and non-resonant broadening coefficients are different
+
+    If not optically thin, recompute from the transmittance and absorption coefficient
+    (analytical output of the 1D-homogenous column emission without scattering) :
+
+    .. math::
+
+        I_2=\frac{j_2 \left(1-T_2\right)}{k_2}
+
+    or from the emission coefficient and absorption coefficient:
+
+    .. math::
+
+        I_2=\frac{j_2 \left(1-\operatorname{exp}\left(-k_2 L_2\right)\right)}{k_2}
+
+
+    See Also
+    --------
+    :py:attr:`~radis.spectrum.rescale.rescale_emisscoeff`,
+    :py:attr:`~radis.spectrum.rescale.rescale_abscoeff`,
+    :py:attr:`~radis.spectrum.rescale.rescale_radiance_noslit`,
+
     """
 
     unit = None
@@ -1178,7 +1377,9 @@ def get_radiance_unit(unit_emisscoeff):
     # Rescale!
     if "emisscoeff" in rescaled and true_path_length and optically_thin:
         if __debug__:
-            printdbg("... rescale: radiance_noslit I2 = j2 * L2 " + "(optically thin)")
+            printdbg(
+                "... rescale: radiance_noslit I_2 = j_2 * L_2 " + "(optically thin)"
+            )
         emisscoeff = rescaled["emisscoeff"]  # x already scaled
         radiance_noslit = emisscoeff * new_path_length  # recalculate L
         unit = get_radiance_unit(units["emisscoeff"])
@@ -1191,7 +1392,7 @@ def get_radiance_unit(unit_emisscoeff):
         and not optically_thin
     ):  # not optically thin
         if __debug__:
-            printdbg("... rescale: radiance_noslit I2 = j2*(1-T2)/k2")
+            printdbg("... rescale: radiance_noslit I_2 = j_2*(1-T_2)/k_2")
         emisscoeff = rescaled["emisscoeff"]  # x already scaled
         abscoeff = rescaled["abscoeff"]  # x already scaled
         # mole_fraction, path_length already scaled
@@ -1211,7 +1412,7 @@ def get_radiance_unit(unit_emisscoeff):
         and not optically_thin
     ):  # not optically thin
         if __debug__:
-            printdbg("... rescale: radiance_noslit I2 = j2*(1-exp(-k2*L2))/k2")
+            printdbg("... rescale: radiance_noslit I_2 = j_2*(1-exp(-k_2*L_2))/k_2")
         emisscoeff = rescaled["emisscoeff"]  # x already scaled
         abscoeff = rescaled["abscoeff"]  # x already scaled
         b = abscoeff == 0  # optically thin mask
@@ -1225,7 +1426,8 @@ def get_radiance_unit(unit_emisscoeff):
     elif "radiance_noslit" in initial and optically_thin:
         if __debug__:
             printdbg(
-                "... rescale: radiance_noslit I2 = I1*N2/N1*L2/L1 " + "(optically thin)"
+                "... rescale: radiance_noslit I_2 = I_1*x_2/x_1*L_2/L_1 "
+                + "(optically thin)"
             )
         _, radiance_noslit = spec.get(
             "radiance_noslit", wunit=waveunit, Iunit=units["radiance_noslit"]
@@ -1251,14 +1453,13 @@ def get_radiance_unit(unit_emisscoeff):
                 raise ValueError(msg)
         else:
             msg = (
-                "Missing data to recalculate radiance_noslit. You need at least "
-                + "scaled emisscoeff ({0}), scaled transmittance_noslit ({1}) ".format(
+                "Missing data to recalculate radiance_noslit for a non-optically thin column with thermal_equilibrium={0}. You need at least "
+                + "scaled emisscoeff ({0}), scaled transmittance_noslit ({1}), ".format(
                     "emisscoeff" in rescaled, "transmittance_noslit" in rescaled
                 )
                 + "scaled abscoeff ({0}) and true_path_length ({1}). ".format(
                     "abscoeff" in rescaled, true_path_length
                 )
-                + "Try in optically thin mode"
             )
             if "radiance_noslit" in extra:
                 radiance_noslit = None
@@ -1329,12 +1530,25 @@ def rescale_radiance(
 
 
 def rescale_emissivity_noslit(spec, rescaled, units, extra, true_path_length):
-    """
+    r"""
 
     Parameters
     ----------
 
     spec: Spectrum
+
+    References
+    ----------
+
+    Compute from the transmittance :
+
+    .. math::
+
+        \epsilon_2 = 1 - \tau_2
+
+    See Also
+    --------
+    :py:attr:`~radis.spectrum.rescale.rescale_transmittance_noslit`,
     """
 
     # case where we recomputed it already (somehow... ex: no_change signaled)
@@ -1347,7 +1561,7 @@ def rescale_emissivity_noslit(spec, rescaled, units, extra, true_path_length):
     # Rescale!
     if "transmittance_noslit" in rescaled:
         if __debug__:
-            printdbg("... rescale: emissivity_noslit e2 = 1 - T2")
+            printdbg("... rescale: emissivity_noslit e_2 = 1 - T_2")
         # transmittivity already scaled
         T2 = rescaled["transmittance_noslit"]
         emissivity_noslit = 1 - T2  # recalculate
@@ -1368,6 +1582,118 @@ def rescale_emissivity_noslit(spec, rescaled, units, extra, true_path_length):
     return rescaled, units
 
 
+# ... cross sections
+
+
+def rescale_xsection(
+    spec,
+    rescaled,
+    initial,
+    old_mole_fraction,
+    new_mole_fraction,
+    old_path_length,
+    new_path_length,
+    waveunit,
+    units,
+    extra,
+    true_path_length,
+):
+    r"""Rescale cross-sections
+
+    Parameters
+    ----------
+    spec: Spectrum
+    old_path_length: float
+        path length in cm
+
+    References
+    ----------
+    Either scale the cross-section directly :
+
+    .. math::
+
+        \sigma_2=\sigma_1
+
+    (by definition, cross-sections are unchanged)
+
+    or recompute from scaled absorption coefficient:
+
+    .. math::
+
+        \sigma_2=k_2 \frac{k_b T}{x p}
+
+    With ``p`` the total pressure and ``x`` the species mole fraction.
+
+
+    See Also
+    --------
+    :py:attr:`~radis.spectrum.rescale.rescale_abscoeff`,
+    """
+
+    unit = None
+
+    # case where we recomputed it already (somehow... ex: no_change signaled)
+    if "xsection" in rescaled:
+        if __debug__:
+            printdbg("... rescale: xsection was scaled already")
+        assert "xsection" in units
+        return rescaled, units
+
+    # Get scaled xsection directly from XS1
+    # -------------------------------------
+
+    if "xsection" in initial:
+        # cross-sections are unchanged
+        if __debug__:
+            printdbg("... rescale: xsection XS_2 = XS_1")
+        _, xsection = spec.get("xsection", wunit=waveunit, Iunit=units["xsection"])
+        unit = units["xsection"]  # units unchanged
+    elif (
+        "abscoeff" in rescaled
+        and "Tgas" in spec.conditions
+        and "pressure_mbar" in spec.conditions
+        and "mole_fraction" in spec.conditions
+    ):
+        if __debug__:  # cm-1
+            printdbg("... rescale: xsection XS_2 = k_2 * (k_b * T / x / p)")
+        abscoeff = rescaled["abscoeff"]  # x already scaled
+        pressure_Pa = spec.conditions["pressure_mbar"] * 1e2
+        x = spec.conditions["mole_fraction"]
+        Tgas = spec.conditions["Tgas"]  # K
+        from radis.phys.constants import k_b
+
+        xsection = abscoeff * (k_b * Tgas / x / pressure_Pa) * 1e6  # cm2
+        if "abscoeff" in spec.units:
+            assert spec.units["abscoeff"] == "cm-1"
+        unit = "cm2"
+    else:
+        msg = (
+            "Cant recalculate xsection if not all these quantities are given : "
+            + "scaled abscoeff ({0}), Tgas ({1}), pressure_mbar ({2})".format(
+                "abscoeff" in rescaled,
+                "Tgas" in spec.conditions,
+                "pressure_mbar" in spec.conditions,
+            )
+        )
+        if "xsection" in extra:  # cant calculate this one but let it go
+            xsection = None
+            if __debug__:
+                printdbg(msg)
+        else:
+            raise ValueError(msg)
+
+    # Then rescale and export
+    # -----------------------
+
+    # Export rescaled value
+    if xsection is not None:
+        rescaled["xsection"] = xsection
+    if unit is not None:
+        units["xsection"] = unit
+
+    return rescaled, units
+
+
 def _recalculate(
     spec,
     quantity,
@@ -1384,21 +1710,19 @@ def _recalculate(
     and :func:`~radis.spectrum.rescale.update`.
 
     Determines with spectral quantities should be recomputed, then scales
-    them solving the Radiative Transfer Equation in the process.
+    them solving the Radiative Transfer Equation on a 1D-homogeneous column
+    without scattering.
 
 
     Parameters
     ----------
-
     spec: Spectrum
         the Spectrum object to recompute
-
     quantity: str, ``'same'``, ``'all'``, or list of str
         name of the spectral quantity to recompute. If ``'same'``, only the quantities
         in the Spectrum are recomputed. If ``'all'``, then all quantities that can
         be derived are recomputed. See :py:data:`~radis.spectrum.utils.CONVOLUTED_QUANTITIES`
         and :py:data:`~radis.spectrum.utils.NON_CONVOLUTED_QUANTITIES`
-
     true_path_length: boolean
         if ``False``, only relative rescaling (new/old) is allowed. For instance,
         when you dont know the true path_lenth, rescaling absorbance
@@ -1407,12 +1731,10 @@ def _recalculate(
 
     Other Parameters
     ----------------
-
     assume_equilibrium: boolean
         if ``True``, only absorption coefficient ``abscoeff`` is recomputed
         and all values are derived from a calculation under equilibrium,
         using Kirchoff's Law. Default ``False``
-
     """
 
     optically_thin = spec.is_optically_thin()
@@ -1428,7 +1750,15 @@ def _recalculate(
     else:
         _check_quantity = [quantity]
     for k in _check_quantity:
-        assert k in CONVOLUTED_QUANTITIES + NON_CONVOLUTED_QUANTITIES + ["all", "same"]
+        try:
+            assert k in CONVOLUTED_QUANTITIES + NON_CONVOLUTED_QUANTITIES + [
+                "all",
+                "same",
+            ]
+        except AssertionError:
+            raise ValueError(
+                f"Unexpected spectral array `{k}`. Expected one of {CONVOLUTED_QUANTITIES+NON_CONVOLUTED_QUANTITIES}"
+            )
     # ... make sure we're not trying to rescale a Spectrum that has non scalable
     # ... quantities
     if any_in(initial, non_rescalable_keys):
@@ -1660,6 +1990,21 @@ def _recalculate(
             )
             apply_slit = apply_slit or slit_needed
 
+    if "xsection" in recompute:
+        rescaled, units = rescale_xsection(
+            spec,
+            rescaled,
+            initial,
+            old_mole_fraction,
+            new_mole_fraction,
+            old_path_length,
+            optically_thin,
+            waveunit,
+            units,
+            extra,
+            true_path_length,
+        )
+
     # delete former convoluted value if apply_slit will be used (just to be sure
     # we arent keeping a non rescaled value if something goes wrong)
     if apply_slit:
diff --git a/radis/spectrum/spectrum.py b/radis/spectrum/spectrum.py
index 3dc5cacfc..a1a6db8b2 100644
--- a/radis/spectrum/spectrum.py
+++ b/radis/spectrum/spectrum.py
@@ -651,7 +651,7 @@ class directly. Ex::
             # dimensionless
             if wunit is None:
                 raise ValueError(
-                    "``w`` must be a dimensionned array, or ``Iunit=`` must be given."
+                    "``w`` must be a dimensionned array, or ``wunit=`` must be given."
                 )
         else:
             if wunit is not None:
@@ -925,7 +925,7 @@ def get(self, var, wunit="default", Iunit="default", copy=True, trim_nan=False):
 
         Parameters
         ----------
-        var: variable ('absorbance', 'transmittance', etc.)
+        var: variable (``'absorbance'``, ``'transmittance'``, ``'xsection'`` etc.)
             Should be a defined quantity among :data:`~radis.spectrum.utils.CONVOLUTED_QUANTITIES`
             or :data:`~radis.spectrum.utils.NON_CONVOLUTED_QUANTITIES`.
             To get the full list of quantities defined in this Spectrum object use
@@ -985,9 +985,13 @@ def get(self, var, wunit="default", Iunit="default", copy=True, trim_nan=False):
                     + ". Have you used .apply_slit()?"
                 )
             else:
-                raise KeyError(
-                    "{0} not in quantity list: {1}".format(var, self.get_vars())
-                )
+                # Try to compute it automatically :
+                try:
+                    self.update(var)
+                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"
+                    ) from err
 
         # Get quantity
         I = self._q[var]
@@ -1335,9 +1339,22 @@ def update(self, quantity="all", optically_thin="default", verbose=True):
 
         Examples
         --------
+        Initialize a spectrum from the absorption coefficient, retrieve the transmittance
+        or the emission coefficient :
         ::
 
+            s = Spectrum.from_array([1900.  , 1900.01, 1900.02, 1900.03, 1900.04, 1900.05, 1900.06,
+                                     1900.07, 1900.08, 1900.09],
+                                    [2.71414065e-06, 2.88341489e-06, 3.06942277e-06, 3.27445689e-06,
+                                     3.50121831e-06, 3.75290756e-06, 4.03334037e-06, 4.34709612e-06,
+                                     4.69971017e-06, 5.09792551e-06],
+                                    'abscoeff', wunit='cm-1', Iunit='cm-1',
+                                    conditions={'path_length':1, # cm
+                                                'thermal_equilibrium':True,
+                                                'Tgas':700,  # K
+                                                })
             s.update('transmittance_noslit')
+            s.update('emisscoeff')
 
         See Also
         --------
@@ -1801,7 +1818,7 @@ def plot(
 
         Parameters
         ----------
-        var: variable (`absorbance`, `transmittance`, `transmittance_noslit`, etc.)
+        var: variable (`absorbance`, `transmittance`, `transmittance_noslit`, `xsection`, etc.)
             For full list see :py:meth:`~radis.spectrum.spectrum.Spectrum.get_vars()`.
             If ``None``, plot the first thing in the Spectrum. Default ``None``.
         wunit: ``'default'``, ``'nm'``, ``'cm-1'``, ``'nm_vac'``,
@@ -1997,9 +2014,7 @@ def clean_error_msg(string):
         plt.ticklabel_format(useOffset=False, axis="x")
         plt.xlabel(xlabel)
         plt.ylabel(ylabel)
-
         plt.yscale(yscale)
-
         if "label" in kwargs:
             plt.legend()
         fix_style()
@@ -2022,6 +2037,7 @@ def clean_error_msg(string):
 
         if show:
             plt.show()
+
         return line
 
     def get_populations(self, molecule=None, isotope=None, electronic_state=None):
@@ -3858,7 +3874,7 @@ def compare_with(
     def cite(self, format="bibentry"):
         """Prints bibliographic references used to compute this spectrum, as
         stored in the :py:attr:`~radis.spectrum.spectrum.Spectrum.references`
-        dictionary.
+        dictionary. Default references known to RADIS are listed in :py:data:`radis.db.references.doi`.
 
         Parameters
         ----------
@@ -4006,7 +4022,8 @@ def cite(self, format="bibentry"):
 
         See Also
         --------
-        :py:class:`~radis.tools.track_ref.RefTracker`
+        :py:class:`~radis.tools.track_ref.RefTracker`,
+        :py:data:`~radis.db.references.doi`
 
         """
 
@@ -4164,7 +4181,7 @@ def take(self, var, copy_lines=False):
         Parameters
         ----------
         var : str
-            spectral quantity
+            spectral quantity (``'absorbance'``, ``'transmittance'``, ``'xsection'`` etc.)
 
         Returns
         -------
@@ -4173,8 +4190,7 @@ def take(self, var, copy_lines=False):
 
         Examples
         --------
-
-        Use it to chain other commands ::
+        Use ``take`` to chain other commands ::
 
             s.take('radiance').normalize().plot()
 
@@ -4457,11 +4473,11 @@ def print_perf_profile(self, number_format="{:.3f}", precision=16):
                     calc_hwhm                        0.007s
                     generate_wavenumber_arrays       0.001s
                     calc_line_broadening             0.074s ██████
-                        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 █████
+                        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 █████
                         others                           0.001s
                     calc_other_spectral_quan         0.003s
                     generate_spectrum_obj            0.000s
diff --git a/radis/spectrum/utils.py b/radis/spectrum/utils.py
index 4d1a1851d..c105b48ee 100644
--- a/radis/spectrum/utils.py
+++ b/radis/spectrum/utils.py
@@ -38,6 +38,7 @@
     "abscoeff",  # opacity
     "abscoeff_continuum",
     "emissivity_noslit",
+    "xsection",  # cross-sections
 ]
 """list: name of spectral quantities not convolved with slit function
 
@@ -121,6 +122,17 @@
 units for these parameters are stored in Spectrum.cond_units and are defined
 by the generating class (ex: SpectrumFactory)"""
 
+CONFIG_PARAMS = [
+    "GRIDPOINTS_PER_LINEWIDTH_WARN_THRESHOLD",
+    "GRIDPOINTS_PER_LINEWIDTH_ERROR_THRESHOLD",
+    "SPARSE_WAVERANGE",
+    "DEFAULT_DOWNLOAD_PATH",
+]
+""" list: these parameters are read from radis.config and stored in the Spectrum
+objects. Should be added here only the parameters that may have an impact on the computation,
+for instance the one defining the 'auto' thresholds
+"""
+
 # %% Util functions
 
 
@@ -174,6 +186,9 @@ def make_up(label):
         label = label.replace(
             "transmittance", "Transmittance"
         )  # make a small difference between no_slit and slit while plotting
+
+    label = label.replace("xsection", "cross-section")
+
     # ... Remove _noslit
     label = label.replace("_noslit", "")
 
@@ -252,6 +267,7 @@ def print_conditions(
     units,
     phys_param_list=PHYSICAL_PARAMS,
     info_param_list=INFORMATIVE_PARAMS,
+    config_param_list=CONFIG_PARAMS,
 ):
     """Print all Spectrum calculation parameters.
 
@@ -268,6 +284,11 @@ def print_conditions(
         Parameters. See :data:`~radis.spectrum.utils.INFORMATIVE_PARAMS` for more
         information.
 
+    config_param_list: list
+        These parameters are read from radis.config file. See
+        :data:`~radis.spectrum.utils.CONFIG_PARAMS` for more
+        information.
+
     See Also
     --------
 
@@ -319,6 +340,10 @@ def print_param(k):
         lambda x: x in info_param_list, non_phys_param
     )
 
+    config_param, non_phys_param = partition(
+        lambda x: x in config_param_list, non_phys_param
+    )
+
     print("Physical Conditions")
     print("-" * 40)
     for k in sorted(phys_param):
@@ -329,6 +354,11 @@ def print_param(k):
     for k in sorted(non_phys_param):
         print_param(k)
 
+    print("Config parameters")
+    print("-" * 40)
+    for k in sorted(config_param):
+        print_param(k)
+
     if len(info_param) > 0:
         print("Information")
         print("-" * 40)
@@ -465,11 +495,11 @@ def print_perf_profile(
                 calc_hwhm                        0.007s
                 generate_wavenumber_arrays       0.001s
                 calc_line_broadening             0.074s ██████
-                    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 █████
+                    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 █████
                     others                           0.001s
                 calc_other_spectral_quan         0.003s
                 generate_spectrum_obj            0.000s
diff --git a/radis/tools/database.py b/radis/tools/database.py
index b079c4856..d1cea90cb 100644
--- a/radis/tools/database.py
+++ b/radis/tools/database.py
@@ -1092,12 +1092,28 @@ def add_condition(s):
                 return s
 
             db.map(add_condition)
-        """
 
-        # TODO: If ``function``
-        # returns a :class:`~radis.spectrum.spectrum.Spectrum` object then the
-        # database is updated.
+        .. note::
+
+            spectra are not changed on disk. If you want to update on disk
+            you may want to combine map() followed by :py:meth:`~radis.tools.database.SpecDatabase.compress_to`
+
+        Example ::
+
+            # See length of all spectra :
+
+            db.map(lambda s: print(len(s)))
+
+
+            # Resample all on spectrum of minimum wstep
+            s_wstep_min = db.get(wstep=float(db.see("wstep").min()))[0]
+
+            db.map(lambda s: s.resample(s_wstep_min))
 
+            # Export to a new database:
+            db.compress_to(db.path+'_interp')
+
+        """
         for s in self:
             function(s)
 
@@ -1204,6 +1220,7 @@ def get(self, conditions="", **kwconditions):
         --------
         :meth:`~radis.tools.database.SpecList.get_unique`,
         :meth:`~radis.tools.database.SpecList.get_closest`,
+        ;py:meth:`~radis.tools.database.SpecDatabase.interpolate`,
         :meth:`~radis.tools.database.SpecList.items`
         """
 
@@ -1321,7 +1338,8 @@ def get_unique(self, conditions="", scale_if_possible=False, **kwconditions):
         See Also
         --------
         :py:meth:`~radis.tools.database.SpecList.get`,
-        :py:meth:`~radis.tools.database.SpecList.get_closest`
+        :py:meth:`~radis.tools.database.SpecList.get_closest`,
+        ;py:meth:`~radis.tools.database.SpecDatabase.interpolate`
         """
 
         out = self.get(conditions, scale_if_possible=scale_if_possible, **kwconditions)
@@ -1338,7 +1356,7 @@ def get_unique(self, conditions="", scale_if_possible=False, **kwconditions):
                 if prevVerbose is not None:
                     kwconditions["verbose"] = prevVerbose
                 raise ValueError(
-                    "Spectrum not found. See closest above. Use get_closest()"
+                    "Spectrum not found. See closest above. Use db.get_closest(). You could also try db.interpolate()"
                 )
         elif len(out) > 1:
             raise ValueError(
@@ -1376,7 +1394,8 @@ def get_closest(self, scale_if_possible=True, **kwconditions):
         See Also
         --------
         :meth:`~radis.tools.database.SpecList.get`,
-        :meth:`~radis.tools.database.SpecList.get_unique`
+        :meth:`~radis.tools.database.SpecList.get_unique`,
+        ;py:meth:`~radis.tools.database.SpecDatabase.interpolate`
         """
         #        split_columns: list of str.
         #            slits a comma separated column in multiple columns, and number them.
@@ -1533,10 +1552,17 @@ def get_items(self, condition):
         out: dict
             {condition:Spectrum}
 
+        Examples
+        --------
+        ::
+
+            db.get_items("Tgas")
+
         See Also
         --------
         :meth:`~radis.tools.database.SpecList.to_dict`,
-        :meth:`~radis.tools.database.SpecList.get`
+        :meth:`~radis.tools.database.SpecList.get`,
+        :py:meth:`~radis.tools.database.SpecList.create_fname_grid`
         """
 
         if not self.df[condition].is_unique:
@@ -1548,6 +1574,30 @@ def get_items(self, condition):
 
         return dict(zip(self.df[condition], self.df.Spectrum))
 
+    def create_fname_grid(self, conditions):
+        """Create a 2D-grid of filenames for the list of parameters ``conditions``
+
+        Examples
+        --------
+        ::
+
+            db.create_fname_grid(["Tgas", "pressure_mbar"])
+
+        See Also
+        --------
+        :py:meth:`~radis.tools.database.SpecList.get_items`
+
+        """
+
+        gridsize = [len(self.df[cond].unique()) for cond in conditions]
+
+        return (
+            self.see(conditions)
+            .reset_index()
+            .set_index(conditions)
+            .values.reshape(gridsize)
+        )
+
     def __iter__(self):
         """Iterate over all Spectra in database.
 
@@ -1710,10 +1760,13 @@ def plot_cond(self, cond_x, cond_y, z_value=None, nfig=None):
 
         import matplotlib.pyplot as plt
 
+        from radis.misc.plot import fix_style, set_style
+
         x = self.df[cond_x]
         y = self.df[cond_y]
 
         # Default
+        set_style()
         fig = plt.figure(num=nfig)
         ax = fig.gca()
         ax.plot(x, y, "ok")
@@ -1757,10 +1810,9 @@ def plot_cond(self, cond_x, cond_y, z_value=None, nfig=None):
             )
             # lbls = plt.clabel(cs0, inline=1, fontsize=16,fmt='%.0fK',colors='k',manual=False)     # show labels
 
-            # %%
-
         plt.title(title)
         plt.tight_layout()
+        fix_style()
 
     def __len__(self):
         return len(self.df)
@@ -2038,7 +2090,7 @@ def update(self, force_reload=False, filt=".spec", update_register_only=False):
         # Print index
         self.print_index()
 
-    def compress_to(self, new_folder, compress=True):
+    def compress_to(self, new_folder, compress=True, if_exists_then="error"):
         """Saves the Database in a new folder with all Spectrum objects under
         compressed (binary) format. Read/write is much faster. After the
         operation, a new database should be initialized in the new_folder to
@@ -2052,6 +2104,11 @@ def compress_to(self, new_folder, compress=True):
         compress: boolean, or 2
             if ``True``, saves under binary format. Faster and takes less space.
             If ``2``, additionaly remove all redundant quantities.
+        if_exists_then: ``'increment'``, ``'replace'``, ``'error'``, ``'ignore'``
+            what to do if file already exists. If ``'increment'`` an incremental digit
+            is added. If ``'replace'`` file is replaced (!). If ``'ignore'`` the
+            Spectrum is not added to the database and no file is created.
+            If ``'error'`` (or anything else) an error is raised. Default ``'error'``.
 
         See Also
         --------
@@ -2066,7 +2123,9 @@ def compress_to(self, new_folder, compress=True):
             os.makedirs(new_folder)
 
         for file, s in self.items():  # loop over all spectra
-            s.store(join(new_folder, file), compress=compress)
+            s.store(
+                join(new_folder, file), compress=compress, if_exists_then=if_exists_then
+            )
 
         if self.verbose:
             print("Database compressed to {0}".format(new_folder))
@@ -2290,6 +2349,70 @@ def add(
 
         return file
 
+    def interpolate(self, **kwconditions):
+        """Interpolate existing spectra from the database to generate a new spectrum with conditions kwargs
+
+        Examples
+        --------
+        ::
+
+            db.interpolate(Tgas=300, mole_fraction=0.3)
+
+        """
+
+        # Not interpolated conditions :
+        not_interpolated_conditions = [
+            c for c in self.conditions() if c not in kwconditions
+        ]
+        not_interpolated_conditions = [
+            c
+            for c in not_interpolated_conditions
+            if c not in ["file", "last_modified", "name"]
+        ]
+        df = self.see(columns=not_interpolated_conditions)
+
+        # assert df values are unique :
+        for c in df.columns:
+            if df[c].nunique() > 1:
+                raise ValueError(
+                    f"Spectra in database {self.name} have different values for `{c}`. All conditions except from the one we are interpolating on should be the same"
+                )
+                # TODO : implement a way to get a subset of a SpecDatabase, so we can do something like db.take(molecule='C2').interpolate(... )
+
+        if len(kwconditions) > 1:
+            raise NotImplementedError(
+                "Interpolation of multiple conditions is not implemented yet"
+            )
+
+        cond = list(kwconditions.keys())[0]
+        val = kwconditions[cond]
+
+        cond_list = self.see(columns=[cond]).values[:, 0]
+        b = np.argsort(cond_list)
+
+        cond_list = cond_list[b]
+        spectra = (self.see().index.values)[b]
+
+        # Interpolate val over cond_list
+        pos = np.interp(val, cond_list, np.arange(cond_list.size))
+        index = pos.astype(int)
+        weight = pos - index
+
+        s_left = self.get_unique(file=spectra[index])
+        s_right = self.get_unique(file=spectra[index + 1])
+
+        # linear algebra on spectra directly :
+        s_interp = (1 - weight) * s_left + weight * s_right
+        # TODO : Will raise an error if multiple values in database. In this
+        # case, use s_left.take(var)   with 'var' given in parameters of interpolate()
+        # Or tell user to generate a subdatabase with only one spectral array
+
+        s_interp.conditions[
+            "interpolated_from"
+        ] = f"{spectra[index]}, {spectra[index+1]}"
+
+        return s_interp
+
     def fit_spectrum(
         self,
         s_exp,
diff --git a/radis/tools/plot_tools.py b/radis/tools/plot_tools.py
index a3f058df3..5c57cb746 100644
--- a/radis/tools/plot_tools.py
+++ b/radis/tools/plot_tools.py
@@ -26,6 +26,39 @@
 import warnings
 
 
+class ParamRange:
+    def __init__(self, valmin=0, valmax=1, valinit=None):
+        """Used in :py:func:`radis.lbl.factory.SpectrumFactory.eq_spectrum_gpu_interactive`  """
+        self.valmin = valmin
+        self.valmax = valmax
+
+        if valinit is None:
+            valinit = valmin
+        self.valinit = valinit
+        self.val = self.valinit
+        self.name = None
+        self.widget = None
+
+    def set_widget(self, w, spec, func=lambda x: 0):
+        self.widget = w
+        self.spec = spec
+        self.update_callback = func
+        w.on_changed(self.widget_callback)
+
+    def widget_callback(self, val):
+        self.val = val
+        self.spec.conditions[self.name] = val
+        self.update_callback(val)
+
+    def __repr__(self):
+        return "ParamRange({:s} .. {:s} [{:s}] @ {:s})".format(
+            self.valmin.__repr__(),
+            self.valmax.__repr__(),
+            self.valinit.__repr__(),
+            self.val.__repr__(),
+        )
+
+
 def add_ruler(fig, wunit="", Iunit="", ax=None):
 
     import matplotlib.pyplot as plt
diff --git a/setup.cfg b/setup.cfg
index 1c6c36007..c7029168a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,6 +14,7 @@ addopts = -m "not needs_cuda and not download_large_databases and not needs_db_C
 [isort]
 profile = black
 default_section = THIRDPARTY
+known_first_party = radis
 
 [flake8]
 ignore = C901
diff --git a/setup.py b/setup.py
index 87c1a21cf..026b244a8 100644
--- a/setup.py
+++ b/setup.py
@@ -137,7 +137,7 @@ class BuildFailed(Exception):
 
 
 def show_message(*lines):
-    """ Note : will only happen if user installs with `pip install -v` """
+    """Note : will only happen if user installs with `pip install -v`"""
     print("=" * 74, file=sys.stderr)
     for line in lines:
         print(line, file=sys.stderr)
@@ -146,8 +146,8 @@ def show_message(*lines):
 
 def get_ext_modules(with_binaries):
     """
-    Parameter
-    ---------
+    Parameters
+    ----------
     with_binaries: bool
         if False, do not try to build Cython extensions
     """
@@ -203,10 +203,14 @@ def build_extensions(self):
     ext_modules.append(
         Extension(
             "radis_cython_extensions",
-            sources=["./radis/cython/radis_cython_extensions.pyx"],
+            sources=[
+                "./radis/cython/radis_cython_extensions.pyx",
+                "./radis/lbl/gpu.cpp",
+            ],
             include_dirs=[get_include()],
             language="c++",
             extra_link_args=[],
+            define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
         )
     )
 
@@ -254,6 +258,7 @@ def run_setup(with_binary):
             "habanero",  # CrossRef API to retrieve data from doi
             "h5py",  # HDF5
             "hjson",
+            "ipython>=7.0.0",
             "joblib",  # for parallel loading of SpecDatabase
             "json-tricks>=3.15.0",  # to deal with non jsonable formats
             "pandas>=1.0.5",
@@ -272,7 +277,7 @@ def run_setup(with_binary):
             "seaborn",  # other matplotlib themes
             "scipy>=1.4.0",
             "tuna",  # to generate visual/interactive performance profiles
-            "vaex>=4.4.0",  # load HDF5 files  (version for custom HDF5 groups)
+            "vaex>=4.6.0",  # load HDF5 files  (version for custom HDF5 groups + avoids asyncio https://github.com/vaexio/vaex/pull/1546). #TODO : install only required sub-packages
         ],
         extras_require={
             "dev": [


Test patch:
diff --git a/radis/test/files/CO2abscoeff_300K_4150_4400nm.txt b/radis/test/files/CO2abscoeff_300K_4150_4400nm.txt
index 3afcd7bb2..1a4b09354 100644
--- a/radis/test/files/CO2abscoeff_300K_4150_4400nm.txt
+++ b/radis/test/files/CO2abscoeff_300K_4150_4400nm.txt
@@ -36,8 +36,8 @@
 #    db_assumed_sorted    True
 #    dbformat             cdsd-hitemp
 #    dbpath               D:/Dropbox/Data ECP/14_Databases/CDSD-HITEMP/cdsd_hitemp_07,D:/Dropbox/Data ECP/14_Databases/CDSD-HI...
-#    dlm_log_pG           0.1375350788016573
-#    dlm_log_pL           0.20180288881201608
+#    dxG           0.1375350788016573
+#    dxL           0.20180288881201608
 #    include_neighbouring_lines  True
 #    levelsfmt            radis
 #    parfuncfmt           cdsd
diff --git a/radis/test/files/cdsd_hitemp_09_header.txt b/radis/test/files/cdsd_hitemp_09_header.txt
index a68dbf611..90e2a2f82 100644
--- a/radis/test/files/cdsd_hitemp_09_header.txt
+++ b/radis/test/files/cdsd_hitemp_09_header.txt
@@ -2,4 +2,4 @@
  21 2250.000510 2.346E-34 6.522E-01.0666.0652 6907.50990.71-.002959 0.56  4 2 2 02       2 3 3 02 10 1  20  7 1   5  R 75e 1000
  22 2250.000510 9.572E-46 7.015E+02.0712.065212909.52510.83-.005032 0.67  0 1 1 41       0 1 1 31 13 1  66 10 1  38  R120e 1000
  25 2250.000960 2.744E-33 5.749E+02.0689.0786 5507.18620.71-.003741 0.62  0 0 0 31       0 0 0 21  9 1  29  6 1  14  R 52e 1000
- 21 2250.000990 7.019E-36 1.006E-04.0731.0953 5581.66070.68-.002050 0.69  5 1 1 02       2 4 4 02 11 1  26  8 1   8  P 24e  750
\ No newline at end of file
+ 21 2250.000990 7.019E-36 1.006E-04.0731.0953 5581.66070.68-.002050 0.69  5 1 1 02       2 4 4 02 11 1  26  8 1   8  P 24e  750
diff --git a/radis/test/files/hitran_CO2_fragment.par b/radis/test/files/hitran_CO2_fragment.par
index 98186b49e..45b2d7427 100644
--- a/radis/test/files/hitran_CO2_fragment.par
+++ b/radis/test/files/hitran_CO2_fragment.par
@@ -5,4 +5,4 @@
  21 3000.441468 1.731E-30 7.072e-04.08710.116 3011.76390.76-.001918       4 1 1 03       0 1 1 11                    R  4e     3677742029 5 4 5 7    11.0    9.0
  23 3000.456018 3.275E-30 4.324e-05.06700.075 1458.78750.74-.003744       0 2 2 11       0 1 1 01                    R 46e     3677642332 5 4 5 7    95.0   93.0
  23 3000.725649 8.155E-28 5.782e-05.06900.092  278.31510.74-.003587       0 1 1 11       0 0 0 01                    R 27e     3677642332 5 4 5 7    57.0   55.0
- 23 3000.761718 1.925E-30 4.029e-05.06670.073 1566.13420.73-.003793       0 2 2 11       0 1 1 01                    R 49f     3677742332 5 4 5 7   101.0   99.0
\ No newline at end of file
+ 23 3000.761718 1.925E-30 4.029e-05.06670.073 1566.13420.73-.003793       0 2 2 11       0 1 1 01                    R 49f     3677742332 5 4 5 7   101.0   99.0
diff --git a/radis/test/files/hitran_CO_fragment.par b/radis/test/files/hitran_CO_fragment.par
index 9358be466..d99bc71e3 100644
--- a/radis/test/files/hitran_CO_fragment.par
+++ b/radis/test/files/hitran_CO_fragment.par
@@ -7,4 +7,4 @@
  51    7.479900 8.621E-38 5.746E-08.07480.082 6354.17910.75-.000310              3              3                    R  1      477665 5 8 2 2 1 7     5.0    3.0
  51    7.549901 7.177E-33 1.867E-07.07480.082 4263.83720.75-.000310              2              2                    R  1      677665 5 8 2 2 1 7     5.0    3.0
  51    7.619908 4.436E-28 3.961E-07.07480.082 2147.08110.75-.000310              1              1                    R  1      687665 5 8 2 2 1 7     5.0    3.0
- 51    7.689920 2.566E-23 6.914E-07.07480.082    3.84500.75-.000310              0              0                    R  1      687665 5 8 2 2 1 7     5.0    3.0
\ No newline at end of file
+ 51    7.689920 2.566E-23 6.914E-07.07480.082    3.84500.75-.000310              0              0                    R  1      687665 5 8 2 2 1 7     5.0    3.0
diff --git a/radis/test/lbl/test_broadening.py b/radis/test/lbl/test_broadening.py
index 4aaed7b1c..a3fd2e8c3 100644
--- a/radis/test/lbl/test_broadening.py
+++ b/radis/test/lbl/test_broadening.py
@@ -286,11 +286,11 @@ def test_broadening_methods_different_wstep(verbose=True, plot=False, *args, **k
 
 
 @pytest.mark.fast
-def test_broadening_DLM(verbose=True, plot=False, *args, **kwargs):
+def test_broadening_LDM(verbose=True, plot=False, *args, **kwargs):
     """
     Test use of lineshape template for broadening calculation.
 
-    Ensures that results are the same with and without DLM.
+    Ensures that results are the same with and without LDM.
     """
 
     if plot:  # Make sure matplotlib is interactive so that test are not stuck in pytest
@@ -330,44 +330,44 @@ def test_broadening_DLM(verbose=True, plot=False, *args, **kwargs):
     )
     sf.load_databank("HITRAN-CO-TEST")
 
-    # Reference: calculate without DLM
+    # Reference: calculate without LDM
     assert sf.params["optimization"] is None
     s_ref = sf.eq_spectrum(Tgas=T)
     s_ref.name = "Reference ({0:.2f}s)".format(s_ref.conditions["calculation_time"])
 
-    # DLM:
+    # LDM:
     sf.params["optimization"] = "simple"
     sf.params.broadening_method = "convolve"
-    s_dlm = sf.eq_spectrum(Tgas=T)
-    s_dlm.name = "DLM ({0:.2f}s)".format(s_dlm.conditions["calculation_time"])
-    # DLM Voigt with Whiting approximation:
+    s_ldm = sf.eq_spectrum(Tgas=T)
+    s_ldm.name = "LDM ({0:.2f}s)".format(s_ldm.conditions["calculation_time"])
+    # LDM Voigt with Whiting approximation:
     sf.params.broadening_method = "voigt"
-    s_dlm_voigt = sf.eq_spectrum(Tgas=T)
-    s_dlm_voigt.name = "DLM Whiting ({0:.2f}s)".format(
-        s_dlm_voigt.conditions["calculation_time"]
+    s_ldm_voigt = sf.eq_spectrum(Tgas=T)
+    s_ldm_voigt.name = "LDM Whiting ({0:.2f}s)".format(
+        s_ldm_voigt.conditions["calculation_time"]
     )
 
     # Compare
-    res = get_residual(s_ref, s_dlm, "abscoeff")
-    res_voigt = get_residual(s_dlm, s_dlm_voigt, "abscoeff")
+    res = get_residual(s_ref, s_ldm, "abscoeff")
+    res_voigt = get_residual(s_ldm, s_ldm_voigt, "abscoeff")
 
     if verbose:
         print("Residual:", res)
 
     # plot the last one
     if plot:
-        plot_diff(s_ref, s_dlm, "abscoeff")
+        plot_diff(s_ref, s_ldm, "abscoeff")
         plt.legend()
-        plot_diff(s_dlm, s_dlm_voigt, "abscoeff")
+        plot_diff(s_ldm, s_ldm_voigt, "abscoeff")
 
     assert res < 1.2e-5
     assert res_voigt < 1e-5
 
 
 @pytest.mark.fast
-def test_broadening_DLM_FT(verbose=True, plot=False, *args, **kwargs):
+def test_broadening_LDM_FT(verbose=True, plot=False, *args, **kwargs):
     """
-    Test use of DLM with and without Fourier Transform
+    Test use of LDM with and without Fourier Transform
 
     Ensures that results are the same, and compare calculation times.
     """
@@ -409,40 +409,40 @@ def test_broadening_DLM_FT(verbose=True, plot=False, *args, **kwargs):
     )
     sf.load_databank("HITRAN-CO-TEST")
 
-    # DLM, real space
+    # LDM, real space
     if verbose:
         print("\nConvolve version \n")
     sf._broadening_method = "convolve"
-    s_dlm = sf.eq_spectrum(Tgas=T)
-    s_dlm.name = "DLM ({0:.2f}s)".format(s_dlm.conditions["calculation_time"])
+    s_ldm = sf.eq_spectrum(Tgas=T)
+    s_ldm.name = "LDM ({0:.2f}s)".format(s_ldm.conditions["calculation_time"])
 
-    # DLM , with Fourier
+    # LDM , with Fourier
     if verbose:
         print("\nFFT version \n")
     sf.params.broadening_method = "fft"
-    s_dlm_fft = sf.eq_spectrum(Tgas=T)
-    s_dlm_fft.name = "DLM FFT ({0:.2f}s)".format(
-        s_dlm_fft.conditions["calculation_time"]
+    s_ldm_fft = sf.eq_spectrum(Tgas=T)
+    s_ldm_fft.name = "LDM FFT ({0:.2f}s)".format(
+        s_ldm_fft.conditions["calculation_time"]
     )
 
     # Compare
-    res = get_residual(s_dlm, s_dlm_fft, "abscoeff")
+    res = get_residual(s_ldm, s_ldm_fft, "abscoeff")
 
     if verbose:
         print("Residual:", res)
 
     # plot
     if plot:
-        plot_diff(s_dlm, s_dlm_fft, "abscoeff")
+        plot_diff(s_ldm, s_ldm_fft, "abscoeff")
         plt.legend()
 
     assert res < 5e-6
 
 
 @pytest.mark.fast
-def test_broadening_DLM_noneq(verbose=True, plot=False, *args, **kwargs):
+def test_broadening_LDM_noneq(verbose=True, plot=False, *args, **kwargs):
     """
-    Test Noneq version of DLM and makes sure it gives the same results as the eq
+    Test Noneq version of LDM and makes sure it gives the same results as the eq
     one when used with Tvib=Trot
 
     """
@@ -482,25 +482,25 @@ def test_broadening_DLM_noneq(verbose=True, plot=False, *args, **kwargs):
     )
     sf.load_databank("HITRAN-CO2-TEST", load_columns="noneq")
 
-    # DLM:
+    # LDM:
     sf.params["optimization"] = "simple"
-    s_dlm_eq = sf.eq_spectrum(Tgas=3000)
-    s_dlm_eq.name = "DLM eq ({0:.2f}s)".format(s_dlm_eq.conditions["calculation_time"])
+    s_ldm_eq = sf.eq_spectrum(Tgas=3000)
+    s_ldm_eq.name = "LDM eq ({0:.2f}s)".format(s_ldm_eq.conditions["calculation_time"])
 
-    s_dlm_noneq = sf.non_eq_spectrum(Tvib=3000, Trot=3000)
-    s_dlm_noneq.name = "DLM noneq ({0:.2f}s)".format(
-        s_dlm_noneq.conditions["calculation_time"]
+    s_ldm_noneq = sf.non_eq_spectrum(Tvib=3000, Trot=3000)
+    s_ldm_noneq.name = "LDM noneq ({0:.2f}s)".format(
+        s_ldm_noneq.conditions["calculation_time"]
     )
 
     # Compare
-    res = get_residual(s_dlm_eq, s_dlm_noneq, "radiance_noslit")
+    res = get_residual(s_ldm_eq, s_ldm_noneq, "radiance_noslit")
 
     if verbose:
         print("Residual:", res)
 
     # plot
     if plot:
-        plot_diff(s_dlm_eq, s_dlm_noneq)
+        plot_diff(s_ldm_eq, s_ldm_noneq)
 
     assert res <= 1e-4
 
@@ -892,9 +892,9 @@ def _run_testcases(plot=False, verbose=True, *args, **kwargs):
         plot=plot, verbose=verbose, *args, **kwargs
     )
     test_broadening_methods_different_wstep(plot=plot, verbose=verbose, *args, **kwargs)
-    test_broadening_DLM(plot=plot, verbose=verbose, *args, **kwargs)
-    test_broadening_DLM_FT(plot=plot, verbose=3, *args, **kwargs)
-    test_broadening_DLM_noneq(plot=plot, verbose=verbose, *args, **kwargs)
+    test_broadening_LDM(plot=plot, verbose=verbose, *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 warnings
diff --git a/radis/test/lbl/test_calc.py b/radis/test/lbl/test_calc.py
index f4d053846..8be7e396e 100644
--- a/radis/test/lbl/test_calc.py
+++ b/radis/test/lbl/test_calc.py
@@ -79,14 +79,14 @@ def test_calc_spectrum(verbose=True, plot=True, warnings=True, *args, **kwargs):
 
     - neq 0.9.22: 8.4s
 
-    Starting from RADIS 1.0.1, the test is run on [HITRAN-2016]_, which
+    Starting from RADIS 1.0.1, the test is run on [HITRAN-2020]_, which
     is not valid for these temperatures but can be more conveniently
     downloaded automatically and thus executed everytime with `Travis CI <https://travis-ci.com/radis/radis>`_
 
     (we also expect the test to be much faster than above, but that's just
     because the database is smaller!)
 
-    - radis 0.9.20 : 2.49 s    on [HITRAN-2016]
+    - radis 0.9.20 : 2.49 s    on [HITRAN-2020]
                      4.05 s    on [CDSD-HITEMP-JMIN]
 
     """
@@ -143,7 +143,7 @@ def test_calc_spectrum(verbose=True, plot=True, warnings=True, *args, **kwargs):
     #                          0.18974631,  0.2019285 ,  0.17346687,  0.17211401,  0.15939359,
     #                          0.17240575,  0.15395179,  0.13374185,  0.11997065,  0.10858693,
     #                          0.11114162,  0.04575873,  0.00163863,  0.00062654])
-    # Updated again in RADIS 0.9.20 (19/08/19) to account for the use of DLM (difference
+    # Updated again in RADIS 0.9.20 (19/08/19) to account for the use of LDM (difference
     # not significant)
     #        I_ref = np.array([ 0.29060991,  0.29756722,  0.32972058,  0.3206278 ,  0.20696867,
     #                           0.19218358,  0.20155747,  0.17336405,  0.17218653,  0.1589136 ,
@@ -257,7 +257,7 @@ def test_calc_spectrum_overpopulations(
     Better use the assignation scheme suggested in the paper.
     But it's okay here as a non-regression test.
 
-    Starting from RADIS 1.0.1, the test is run on [HITRAN-2016]_, which
+    Starting from RADIS 1.0.1, the test is run on [HITRAN-2020]_, which
     is not valid for these temperatures but can be more conveniently
     downloaded automatically and thus executed everytime with `Travis CI <https://travis-ci.com/radis/radis>`_
 
@@ -317,7 +317,7 @@ def test_calc_spectrum_overpopulations(
     #                          0.57626389,  0.61424273,  0.52454807,  0.5200812 ,  0.47920924,
     #                          0.51843533,  0.46058817,  0.3983277 ,  0.35582979,  0.32095204,
     #                          0.32821575,  0.13525543,  0.00469489,  0.00174166])
-    # Updated again in RADIS 0.9.20 (16/08/19) to account for the use of DLM (difference
+    # Updated again in RADIS 0.9.20 (16/08/19) to account for the use of LDM (difference
     # not significant)
     #        I_ref = np.array([ 0.62134142,  0.66722021,  0.81016539,  0.79387937,  0.56974945,
     #                           0.58280035,  0.6120114 ,  0.52319075,  0.5193041 ,  0.47686282,
@@ -579,7 +579,7 @@ def test_eq_vs_noneq_isotope(verbose=True, plot=False, warnings=True, *args, **k
 
     On the old NeQ package the test used [HITEMP-2010]_
 
-    Starting from RADIS 1.0.1, the test is run on [HITRAN-2016]_, which
+    Starting from RADIS 1.0.1, the test is run on [HITRAN-2020]_, which
     is not valid for these temperatures but can be more conveniently
     downloaded automatically and thus executed everytime with `Travis CI <https://travis-ci.com/radis/radis>`_
 
diff --git a/radis/test/lbl/test_factory.py b/radis/test/lbl/test_factory.py
index b588ca25b..be5dccf6f 100644
--- a/radis/test/lbl/test_factory.py
+++ b/radis/test/lbl/test_factory.py
@@ -142,11 +142,11 @@ def test_spec_generation(
 
     - 0.9.20 (normal code) >>> 6.3 s
              (with pseudo_continuum_threshold=0.01) >>> ???
-             (with DLM) >>> 2.3 s
+             (with LDM) >>> 2.3 s
 
     - 0.9.26 (normal code) >>> 7.6 s
              (with pseudo_continuum_threshold=0.01) >>> 2.73s
-             (with DLM) >>> 0.25 s
+             (with LDM) >>> 0.25 s
 
     """
 
diff --git a/radis/test/levels/test_partfunc.py b/radis/test/levels/test_partfunc.py
index bb12b3006..bab9a1365 100644
--- a/radis/test/levels/test_partfunc.py
+++ b/radis/test/levels/test_partfunc.py
@@ -903,11 +903,11 @@ def test_parsum_mode_in_factory(verbose=True, plot=True, *args, **kwargs):
                 calc_hwhm                       0.013s █
                 generate_wavenumber_arrays      0.000s
                 calc_line_broadening            0.040s ███
-                    precompute_DLM_lineshapes       0.002s
-                    DLM_Initialized_vectors         0.000s
-                    DLM_closest_matching_line       0.000s
-                    DLM_Distribute_lines            0.001s
-                    DLM_convolve                    0.037s ██
+                    precompute_LDM_lineshapes       0.002s
+                    LDM_Initialized_vectors         0.000s
+                    LDM_closest_matching_line       0.000s
+                    LDM_Distribute_lines            0.001s
+                    LDM_convolve                    0.037s ██
                 calc_other_spectral_quan        0.004s
                 generate_spectrum_obj           0.000s
 
@@ -933,11 +933,11 @@ def test_parsum_mode_in_factory(verbose=True, plot=True, *args, **kwargs):
                 calc_hwhm                       0.017s ██
                 generate_wavenumber_arrays      0.001s
                 calc_line_broadening            0.035s █████
-                    precompute_DLM_lineshapes       0.001s
-                    DLM_Initialized_vectors         0.000s
-                    DLM_closest_matching_line       0.001s
-                    DLM_Distribute_lines            0.000s
-                    DLM_convolve                    0.032s ████
+                    precompute_LDM_lineshapes       0.001s
+                    LDM_Initialized_vectors         0.000s
+                    LDM_closest_matching_line       0.001s
+                    LDM_Distribute_lines            0.000s
+                    LDM_convolve                    0.032s ████
                     others                          0.001s
                 calc_other_spectral_quan        0.003s
                 generate_spectrum_obj           0.000s
diff --git a/radis/test/spectrum/test_rescale.py b/radis/test/spectrum/test_rescale.py
index 122a2a677..2d79da25e 100644
--- a/radis/test/spectrum/test_rescale.py
+++ b/radis/test/spectrum/test_rescale.py
@@ -45,6 +45,7 @@ def test_compression(verbose=True, warnings=True, *args, **kwargs):
         "transmittance_noslit": True,
         "absorbance": True,
         "abscoeff": False,
+        "xsection": True,
     }
 
     return True
@@ -232,12 +233,62 @@ def test_rescale_all_quantities(verbose=True, warnings=True, *args, **kwargs):
         radis.config["DEBUG_MODE"] = DEBUG_MODE
 
 
+def test_xsections(*args, **kwargs):
+    r"""Test that we do have
+
+    .. math::
+
+        exp(-\sigma \cdot N_x \cdot L) = exp(- k \cdot L)
+
+    Therefore
+
+    .. math::
+
+        k = \sigma \cdot \frac{x p}{k T}
+
+    With ``p`` total pressure and ``x`` the mole fraction.
+
+    """
+    from radis.phys.constants import k_b
+    from radis.phys.units import Unit as u
+
+    # 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")
+    T = s.c["Tgas"] * u("K")
+
+    N = p / k_b / T
+
+    assert s.c["pressure_mbar"] == 1013.25
+    assert s.c["Tgas"] == 1500
+    assert s.c["mole_fraction"] == 0.01
+
+    assert np.isclose(N.to("1/cm3").value, 4.892e18, rtol=1e-3)
+
+    N_x = N * 0.01
+
+    assert s.take("abscoeff").max() == s.take("xsection").max() * N_x
+
+    # Test again for x =1
+
+    s.rescale_mole_fraction(1)
+    assert np.isclose(
+        s.take("abscoeff").max().to("1/cm").value,
+        (s.take("xsection").max() * N).to("1/cm").value,
+    )
+
+
 def _run_all_tests(verbose=True, warnings=True, *args, **kwargs):
     test_compression(verbose=verbose, warnings=warnings, *args, **kwargs)
     test_update_transmittance(verbose=verbose, warnings=warnings, *args, **kwargs)
     test_get_recompute(verbose=verbose, warnings=warnings, *args, **kwargs)
     test_recompute_equilibrium(verbose=verbose, warnings=warnings, *args, **kwargs)
     test_rescale_all_quantities(verbose=verbose, *args, **kwargs)
+    test_xsections()
 
     return True
 
diff --git a/radis/test/spectrum/test_utils.py b/radis/test/spectrum/test_utils.py
index 93004c7cf..e58b1ca8d 100644
--- a/radis/test/spectrum/test_utils.py
+++ b/radis/test/spectrum/test_utils.py
@@ -41,11 +41,11 @@ def test_perf_profile(*args, **kwargs):
                 calc_hwhm                        0.007s
                 generate_wavenumber_arrays       0.001s
                 calc_line_broadening             0.074s ██████
-                    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 █████
+                    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 █████
                     others                           0.001s
                 calc_other_spectral_quan         0.003s
                 generate_spectrum_obj            0.000s
diff --git a/radis/test/tools/test_track_ref.py b/radis/test/tools/test_track_ref.py
index f249887b3..224796a08 100644
--- a/radis/test/tools/test_track_ref.py
+++ b/radis/test/tools/test_track_ref.py
@@ -55,7 +55,7 @@ def test_citations_in_eq_spectrum(verbose=False, *args, **kwargs):
 
     assert doi["RADIS-2018"] in s.references
     assert doi["DIT-2020"] in s.references
-    assert doi["HITRAN-2016"] in s.references
+    assert doi["HITRAN-2020"] in s.references
     assert doi["TIPS-2020"] in s.references
 
 
@@ -82,7 +82,7 @@ def test_citations_in_noneq_spectrum(verbose=False, *args, **kwargs):
 
     assert doi["RADIS-2018"] in s.references
     assert doi["DIT-2020"] in s.references
-    assert doi["HITRAN-2016"] in s.references
+    assert doi["HITRAN-2020"] in s.references
     assert doi["TIPS-2020"] in s.references
 
     assert doi["Guelachvili-1983"] in s.references
diff --git a/radis/test/validation/test_compare_torch_CO2.py b/radis/test/validation/test_compare_torch_CO2.py
index 8d0251d1f..38d04ac71 100644
--- a/radis/test/validation/test_compare_torch_CO2.py
+++ b/radis/test/validation/test_compare_torch_CO2.py
@@ -66,7 +66,7 @@ def test_compare_torch_CO2(
     - neq==0.9.22: (Parallel + continuum) Finished in 65s
       RADIS 0.9.9 == neq 0.9.24
 
-    - RADIS 0.9.26: Finished test_compare_torch_CO2 in 57s (DLM, no continuum)
+    - RADIS 0.9.26: Finished test_compare_torch_CO2 in 57s (LDM, no continuum)
 
     - RADIS 0.9.28: ParallelFactory is removed. Finished test_compare_torch_CO2 in 45s
 
@@ -117,9 +117,9 @@ def test_compare_torch_CO2(
         export_populations=False,  # saves some memory
         cutoff=1e-25,
         truncation=10,
-        # pseudo_continuum_threshold=0.01, # use pseudo-continuum, no DLM. Note : 56s on 20/08.
+        # pseudo_continuum_threshold=0.01, # use pseudo-continuum, no LDM. Note : 56s on 20/08.
         # optimization=None,
-        pseudo_continuum_threshold=0,  # use DLM, no pseudo-continuum. Note : 84s on 20/08
+        pseudo_continuum_threshold=0,  # use LDM, no pseudo-continuum. Note : 84s on 20/08
         optimization="min-RMS",
         verbose=False,
     )