Repository: igmhub/picca (language: python)

Issue:
[bump minor] Pk1d z parts
1) Option to compute P1D "chunks" according to predefined z bins, instead of 3 chunks in qso's restframe.
2) At P1D postprocessing, if the former option was set, option to compute skyline mask correction matrices, and apply them.
Possible issue with rebinning
@andreicuceu noted that there is potentially an issue with the way we are doing the rebinning.

Original message by Andrei:
I think there's a small bug in picca delta extraction when defining the wavelength grid. These are true continuum deltas for the same forest, run with both picca and qsonic. You can see that for mocks the grid is slightly shifted compared to the data, so the bin centers are at 3599.9, 3600.7, etc.  instead of 3600.0, 3600.8, etc.. QSOnic has the correct grid, while picca shifts everything by 0.1A to the data grid. This is not a big deal, but it would be good to fix both the bug in picca and the shifted grid in QQ.

![issue](https://github.com/igmhub/picca/assets/9192975/966f4734-2d6f-4a56-972b-e65dcb77aaca)



Discussion / hints:

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 0060cf7db..aa5733af3 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 8.3.2
+current_version = 9.8.0
 commit = True
 
 [bumpversion:file:py/picca/_version.py]
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index e53fe7b07..dfe5ee1eb 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -1,10 +1,10 @@
 name: Pylint
 
-on: 
+on:
   push:
     paths-ignore:
       - '**/*.md'
-  pull_request:  
+  pull_request:
     paths-ignore:
       - '**/*.md'
   merge_group:
@@ -16,10 +16,10 @@ jobs:
 
     steps:
     - uses: actions/checkout@v3
-    - name: Set up Python 3.8
+    - name: Set up Python 3.12
       uses: actions/setup-python@v4
       with:
-        python-version: 3.8
+        python-version: 3.12
     - name: Install dependencies
       run: |
         sudo apt-get -y install libbz2-dev
diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml
index 8c21a7892..c751d0cbe 100644
--- a/.github/workflows/python_package.yml
+++ b/.github/workflows/python_package.yml
@@ -22,7 +22,7 @@ jobs:
     timeout-minutes: 60
     strategy:
       matrix:
-        python-version: ["3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11", "3.12"]
 #    env:
 #      OMP_NUM_THREADS: 1
 #      MKL_NUM_THREADS: 1
@@ -40,12 +40,6 @@ jobs:
         python -m pip install --upgrade pip
         pip install wheel pytest pytest-cov coveralls
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
-    # TODO: the section below can be removed if the compile step is fixed otherwise
-    - name: Reinstall fitsio to ensure the numpy version during compilation matches the one at runtime
-      run: |
-        pip uninstall fitsio -y
-        pip cache purge
-        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
     - name: Install picca
       run: pip install -e .
     - name: Test with pytest
@@ -63,7 +57,7 @@ jobs:
     # the following bit will save results tests as an artifact if [ci save] has been in the commit msg
     - name: Archive test results
       if: ${{ failure() }}
-      uses: actions/upload-artifact@v1
+      uses: actions/upload-artifact@v4
       with:
         name: test-results_v${{ matrix.python-version }}
         path: /tmp/last_run_picca_test
@@ -71,7 +65,7 @@ jobs:
 
     - name: Archive test results delta_extraction
       if: ${{ failure() }}
-      uses: actions/upload-artifact@v1
+      uses: actions/upload-artifact@v4
       with:
         name: test-results-de_v${{ matrix.python-version }}
         path: /home/runner/work/picca/picca/py/picca/tests/delta_extraction/results
diff --git a/README.md b/README.md
index e4ee11008..11d57f546 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ This package contains tools used for the analysis of the Lyman-alpha forest samp
 The current reference is du Mas des Bourboux et al. 2020 (https://arxiv.org/abs/2007.08995).
 
 ## Installation
-First, create a clean environment:
+First, create a clean environment with `version`>=3.9:
 ```
 conda create -n my_picca_env python==version
 conda activate my_picca_env
diff --git a/bin/picca_Pk1D.py b/bin/picca_Pk1D.py
index 230b5feba..15080bccb 100755
--- a/bin/picca_Pk1D.py
+++ b/bin/picca_Pk1D.py
@@ -14,54 +14,24 @@
 from picca.pk1d.compute_pk1d import (compute_correction_reso,
                         compute_correction_reso_matrix, compute_pk_noise,
                         compute_pk_raw, fill_masked_pixels, rebin_diff_noise,
-                        split_forest)
+                        split_forest, split_forest_in_z_parts,
+                        check_linear_binning, Pk1D)
 from picca.utils import userprint
 from multiprocessing import Pool
 
 
-def check_linear_binning(delta):
-    """checks if the wavelength binning is linear or log this is stable against masking
-
-    Args:
-        delta (Delta): delta class as read with Delta.from_...
-
-    Raises:
-        ValueError: Raised if binning is neither linear nor log, or if delta.log_lambda was actually wavelength
-
-    Returns:
-        linear_binning (bool): boolean telling the binning_type
-        pixel_step (float): size of a wavelength bin in the right unit
-    """
-
-    diff_lambda = np.diff(10**delta.log_lambda)
-    diff_log_lambda = np.diff(delta.log_lambda)
-    q5_lambda, q25_lambda = np.percentile(diff_lambda, [5, 25])
-    q5_log_lambda, q25_log_lambda = np.percentile(diff_log_lambda, [5, 25])
-    if (q25_lambda - q5_lambda) < 1e-6:
-        #we can assume linear binning for this case
-        linear_binning = True
-        pixel_step = np.min(diff_lambda)
-    elif (q25_log_lambda - q5_log_lambda) < 1e-6 and q5_log_lambda < 0.01:
-        #we can assume log_linear binning for this case
-        linear_binning = False
-        pixel_step = np.min(diff_log_lambda)
-    elif (q5_log_lambda >= 0.01):
-        raise ValueError(
-            "Could not figure out if linear or log wavelength binning was used, probably submitted lambda as log_lambda"
-        )
-    else:
-        raise ValueError(
-            "Could not figure out if linear or log wavelength binning was used"
-        )
-
-    return linear_binning, pixel_step
-
-
 # loop over input files
 num_data=0
 def process_all_files(index_file_args):
     global num_data
     file_index, file, args = index_file_args
+
+    # If the healpix or tile number is given in the delta name,
+    # it names the Pk accordingly, else it takes
+    file_number = file.split("-")[-1].split(".fits.gz")[0]
+    if not file_number.isdigit():
+        file_number = file_index
+
     if file_index % 5 == 0:
         userprint("\rread {} of {} {}".format(file_index, args.len_files,
                                               num_data),
@@ -92,7 +62,7 @@ def process_all_files(index_file_args):
     delta = deltas[0]
     linear_binning, pixel_step = check_linear_binning(delta)
     if linear_binning:
-        userprint("\n\nUsing linear binning, results will have units of AA")
+        #userprint("\n\nUsing linear binning, results will have units of AA")
         if (args.disable_reso_matrix or not hasattr(delta, 'resolution_matrix')
                 or delta.resolution_matrix is None):
             userprint(
@@ -100,12 +70,12 @@ def process_all_files(index_file_args):
             )
             reso_correction = "Gaussian"
         else:
-            userprint("Using Resolution matrix for resolution correction\n")
+            #userprint("Using Resolution matrix for resolution correction\n")
             reso_correction = "matrix"
     else:
-        userprint("\n\nUsing log binning, results will have units of km/s")
+        #userprint("\n\nUsing log binning, results will have units of km/s")
         reso_correction = "Gaussian"
-        userprint("Using Gaussian resolution correction\n")
+        #userprint("Using Gaussian resolution correction\n")
 
     use_exp_diff_cut=False
     #add check if diff has ever been set
@@ -116,8 +86,8 @@ def process_all_files(index_file_args):
             break
 
     num_data += len(deltas)
-    userprint("\n ndata =  ", num_data)
-    results = None
+    #userprint("\n ndata =  ", num_data)
+    file_out = None
 
     for delta in deltas:
         if (delta.mean_snr <= args.SNR_min
@@ -127,48 +97,73 @@ def process_all_files(index_file_args):
             if np.sum(delta.exposures_diff)==0:
                 continue
 
-        # first pixel in forest
-        selected_pixels = 10**delta.log_lambda > args.lambda_obs_min
-        #this works as selected_pixels returns a bool and argmax points
-        #towards the first occurance for equal values
-        first_pixel_index = (np.argmax(selected_pixels) if
-                             np.any(selected_pixels) else len(selected_pixels))
-
-        # minimum number of pixel in forest
-        min_num_pixels = args.nb_pixel_min
-        if (len(delta.log_lambda) - first_pixel_index) < min_num_pixels:
-            continue
-
-        # Split the forest in n parts
-        max_num_parts = (len(delta.log_lambda) -
-                         first_pixel_index) // min_num_pixels
-        num_parts = min(args.nb_part, max_num_parts)
-
-        #the split_forest function works with either binning, but needs to be uniform
-        if linear_binning:
-            split_array = split_forest(
-                num_parts,
-                pixel_step,
-                10**delta.log_lambda,
-                delta.delta,
-                delta.exposures_diff,
-                delta.ivar,
-                first_pixel_index,
-                reso_matrix=(delta.resolution_matrix
-                             if reso_correction == 'matrix' else None),
-                linear_binning=True)
+        if args.parts_in_redshift:
+            # define chunks on a fixed redshift grid
+            z_grid = np.array(args.z_parts)
+            split_array = split_forest_in_z_parts(z_grid,
+                            delta.log_lambda,
+                            delta.delta,
+                            delta.exposures_diff,
+                            delta.ivar,
+                            min_num_pixels=args.nb_pixel_min,
+                            reso_matrix=(delta.resolution_matrix
+                                 if reso_correction == 'matrix' else None),
+                            linear_binning=linear_binning)
+            num_parts = len(split_array[0])
             if reso_correction == 'matrix':
                 (mean_z_array, lambda_array, delta_array, exposures_diff_array,
-                 ivar_array, reso_matrix_array) = split_array
+                ivar_array, reso_matrix_array) = split_array
             else:
                 (mean_z_array, lambda_array, delta_array, exposures_diff_array,
-                 ivar_array) = split_array
+                    ivar_array) = split_array
+            if not linear_binning:
+                log_lambda_array = lambda_array
+
         else:
-            (mean_z_array, log_lambda_array, delta_array, exposures_diff_array,
-             ivar_array) = split_forest(num_parts, pixel_step,
-                                        delta.log_lambda, delta.delta,
-                                        delta.exposures_diff, delta.ivar,
-                                        first_pixel_index)
+            # define chunks in the forest rest-frame
+
+            # first pixel in forest
+            selected_pixels = 10**delta.log_lambda > args.lambda_obs_min
+            #this works as selected_pixels returns a bool and argmax points
+            #towards the first occurance for equal values
+            first_pixel_index = (np.argmax(selected_pixels) if
+                                np.any(selected_pixels) else len(selected_pixels))
+
+            # minimum number of pixel in forest
+            min_num_pixels = args.nb_pixel_min
+            if (len(delta.log_lambda) - first_pixel_index) < min_num_pixels:
+                continue
+
+            # Split the forest in n parts
+            max_num_parts = (len(delta.log_lambda) -
+                            first_pixel_index) // min_num_pixels
+            num_parts = min(args.nb_part, max_num_parts)
+
+            #the split_forest function works with either binning, but needs to be uniform
+            if linear_binning:
+                split_array = split_forest(
+                    num_parts,
+                    pixel_step,
+                    10**delta.log_lambda,
+                    delta.delta,
+                    delta.exposures_diff,
+                    delta.ivar,
+                    first_pixel_index,
+                    reso_matrix=(delta.resolution_matrix
+                                if reso_correction == 'matrix' else None),
+                    linear_binning=True)
+                if reso_correction == 'matrix':
+                    (mean_z_array, lambda_array, delta_array, exposures_diff_array,
+                    ivar_array, reso_matrix_array) = split_array
+                else:
+                    (mean_z_array, lambda_array, delta_array, exposures_diff_array,
+                    ivar_array) = split_array
+            else:
+                (mean_z_array, log_lambda_array, delta_array, exposures_diff_array,
+                ivar_array) = split_forest(num_parts, pixel_step,
+                                            delta.log_lambda, delta.delta,
+                                            delta.exposures_diff, delta.ivar,
+                                            first_pixel_index)
 
         #the rebin_diff_noise function works with either binning, but needs to be uniform
         for part_index in range(num_parts):
@@ -203,59 +198,53 @@ def process_all_files(index_file_args):
                 continue
 
             # Compute pk_raw, needs uniform binning
-            if linear_binning:
-                k, pk_raw = compute_pk_raw(pixel_step,
-                                           delta_new,
-                                           linear_binning=True)
-            else:
-                k, pk_raw = compute_pk_raw(pixel_step,
-                                           delta_new,
-                                           linear_binning=False)
+            k, fft_delta, pk_raw = compute_pk_raw(pixel_step,
+                                                  delta_new,
+                                                  linear_binning=linear_binning)
 
             # Compute pk_noise
-            run_noise = False
-            if args.noise_estimate == 'pipeline':
-                run_noise = True
-            if linear_binning and not running_on_raw_transmission:
-                pk_noise, pk_diff = compute_pk_noise(pixel_step,
-                                                     ivar_new,
-                                                     exposures_diff_new,
-                                                     run_noise,
-                                                     linear_binning=True,
-                                                     num_noise_exposures=args.num_noise_exp)
-            elif not running_on_raw_transmission:
-                pk_noise, pk_diff = compute_pk_noise(pixel_step,
-                                                     ivar_new,
-                                                     exposures_diff_new,
-                                                     run_noise,
-                                                     linear_binning=False,
-                                                     num_noise_exposures=args.num_noise_exp)
+            if (args.noise_estimate == 'pipeline')|(args.noise_estimate == 'mean_pipeline'):
+                run_gaussian_noise = True
+            else:
+                run_gaussian_noise = False
+            if not running_on_raw_transmission:
+                pk_noise, pk_diff, fft_delta_noise, fft_delta_diff = compute_pk_noise(
+                    pixel_step,
+                    ivar_new,
+                    exposures_diff_new,
+                    run_gaussian_noise,
+                    linear_binning=linear_binning,
+                    num_noise_exposures=args.num_noise_exp,
+                )
             else:
                 pk_noise=pk_diff=np.zeros(pk_raw.shape)
+                fft_delta_noise = np.zeros(pk_raw.shape,dtype=np.complex128)
+                fft_delta_diff = np.zeros(pk_raw.shape,dtype=np.complex128)
 
             # Compute resolution correction, needs uniform binning
-            if linear_binning and not running_on_raw_transmission:
-                #in this case all is in AA space
-                if reso_correction == 'matrix':
-                    correction_reso = compute_correction_reso_matrix(
-                        reso_matrix=np.mean(reso_matrix_array[part_index],
-                                            axis=1),
-                        k=k,
-                        delta_pixel=pixel_step,
-                        num_pixel=len(lambda_new),
-                        pixelization_correction = args.add_pixelization_correction)
-                elif reso_correction == 'Gaussian':
-                    #this is roughly converting the mean resolution estimate back to pixels
-                    #and then multiplying with pixel size
-                    mean_reso_AA = pixel_step * delta.mean_reso_pix
+            if not running_on_raw_transmission:
+                if linear_binning:
+                    #in this case all is in AA space
+                    if reso_correction == 'matrix':
+                        correction_reso = compute_correction_reso_matrix(
+                            reso_matrix=np.mean(reso_matrix_array[part_index],
+                                                axis=1),
+                            k=k,
+                            delta_pixel=pixel_step,
+                            num_pixel=len(lambda_new),
+                            pixelization_correction = args.add_pixelization_correction)
+                    elif reso_correction == 'Gaussian':
+                        #this is roughly converting the mean resolution estimate back to pixels
+                        #and then multiplying with pixel size
+                        mean_reso_AA = pixel_step * delta.mean_reso_pix
+                        correction_reso = compute_correction_reso(
+                            delta_pixel=pixel_step, mean_reso=mean_reso_AA, k=k)
+                else:
+                    #in this case all is in velocity space
+                    delta_pixel = (pixel_step * np.log(10.) *
+                                constants.speed_light / 1000.)
                     correction_reso = compute_correction_reso(
-                        delta_pixel=pixel_step, mean_reso=mean_reso_AA, k=k)
-            elif not running_on_raw_transmission:
-                #in this case all is in velocity space
-                delta_pixel = (pixel_step * np.log(10.) *
-                               constants.speed_light / 1000.)
-                correction_reso = compute_correction_reso(
-                    delta_pixel=delta_pixel, mean_reso=delta.mean_reso, k=k)
+                        delta_pixel=delta_pixel, mean_reso=delta.mean_reso, k=k)
             else:
                 correction_reso= compute_correction_reso(delta_pixel=pixel_step, mean_reso=0., k=k)
 
@@ -272,7 +261,7 @@ def process_all_files(index_file_args):
                     selection = (((k > args.kmin_noise_avg) if args.kmax_noise_avg is not None else 1) &
                                  ((k < args.kmax_noise_avg) if args.kmax_noise_avg is not None else 1))
                 mean_pk_noise = np.mean(pk_noise[selection])
-                pk = (pk_raw - pk_noise) / correction_reso
+                pk = (pk_raw - mean_pk_noise) / correction_reso
 
 
 
@@ -290,102 +279,40 @@ def process_all_files(index_file_args):
                 mean_pk_diff = np.mean(pk_diff[selection])
                 pk = (pk_raw - mean_pk_diff) / correction_reso
 
-            if args.force_output_in_velocity and linear_binning:
-                #division by 1000 to convert speed_light from m/s to km/s
-                c_kms=constants.speed_light / 1000
-                lambda_mean=np.mean(lambda_new)
-                pk *= c_kms / lambda_mean
-                pk_raw *= c_kms / lambda_mean
-                pk_noise *= c_kms / lambda_mean
-                pk_diff *= c_kms / 1000 / lambda_mean
-                k /= c_kms / lambda_mean
-
             # save in fits format
             if args.out_format == 'fits':
-                header = [{
-                    'name': 'RA',
-                    'value': delta.ra,
-                    'comment': "QSO's Right Ascension [degrees]"
-                }, {
-                    'name': 'DEC',
-                    'value': delta.dec,
-                    'comment': "QSO's Declination [degrees]"
-                }, {
-                    'name': 'Z',
-                    'value': delta.z_qso,
-                    'comment': "QSO's redshift"
-                }, {
-                    'name': 'MEANZ',
-                    'value': mean_z_array[part_index],
-                    'comment': "Absorbers mean redshift"
-                }, {
-                    'name': 'MEANRESO',
-                    'value': delta.mean_reso,
-                    'comment': 'Mean resolution [km/s]'
-                }, {
-                    'name': 'MEANSNR',
-                    'value': delta.mean_snr,
-                    'comment': 'Mean signal to noise ratio'
-                }, {
-                    'name': 'NBMASKPIX',
-                    'value': num_masked_pixels,
-                    'comment': 'Number of masked pixels in the section'
-                }, {
-                    'name': 'LIN_BIN',
-                    'value': linear_binning,
-                    'comment': "analysis was performed on delta with linear binned lambda"
-                }, {
-                    'name': 'LOS_ID',
-                    'value': delta.los_id,
-                    'comment': "line of sight identifier, e.g. THING_ID or TARGETID"
-                }, {
-                    'name': 'CHUNK_ID',
-                    'value': part_index,
-                    'comment': "Chunk (sub-forest) identifier"
-                },
-                ]
-
-                cols = [k, pk_raw, pk_noise, pk_diff, correction_reso, pk]
-                names = [
-                    'K', 'PK_RAW', 'PK_NOISE', 'PK_DIFF', 'COR_RESO', 'PK'
-                ]
-                comments = [
-                    'Wavenumber', 'Raw power spectrum',
-                    "Noise's power spectrum",
-                    'Noise coadd difference power spectrum',
-                    'Correction resolution function',
-                    'Corrected power spectrum (resolution and noise)'
-                ]
-                if linear_binning and not args.force_output_in_velocity:
-                    baseunit = "AA"
-                else:
-                    baseunit = "km/s"
-                units = [
-                    f'({baseunit})^-1', f'{baseunit}', f'{baseunit}',
-                    f'{baseunit}', f'{baseunit}', f'{baseunit}'
-                ]
-
-                try:
-                    results.write(cols,
-                                  names=names,
-                                  header=header,
-                                  comments=comments,
-                                  units=units)
-                except AttributeError:
-                    userprint("writing to " + args.out_dir + '/Pk1D-' +
-                                           str(file_index) + '.fits.gz')
-                    results = fitsio.FITS((args.out_dir + '/Pk1D-' +
-                                           str(file_index) + '.fits.gz'),
+                pk1d_class = Pk1D(
+                    ra=delta.ra,
+                    dec=delta.dec,
+                    z_qso=delta.z_qso,
+                    mean_z=mean_z_array[part_index],
+                    mean_snr=delta.mean_snr,
+                    mean_reso=delta.mean_reso,
+                    num_masked_pixels=num_masked_pixels,
+                    linear_bining=linear_binning,
+                    los_id=delta.los_id,
+                    chunk_id=part_index,
+                    k=k,
+                    pk_raw=pk_raw,
+                    pk_noise=pk_noise,
+                    pk_diff=pk_diff,
+                    correction_reso=correction_reso,
+                    pk=pk,
+                    fft_delta=fft_delta,
+                    fft_delta_noise = fft_delta_noise,
+                    fft_delta_diff = fft_delta_diff,
+                )
+                
+                if file_out is None:
+                    file_out = fitsio.FITS((args.out_dir + '/Pk1D-' +
+                                           str(file_number) + '.fits.gz'),
                                           'rw',
                                           clobber=True)
-                    results.write(cols,
-                                  names=names,
-                                  header=header,
-                                  comment=comments,
-                                  units=units)
+                                    
+                pk1d_class.write_fits(file_out)
 
-    if (args.out_format == 'fits' and results is not None):
-        results.close()
+    if (args.out_format == 'fits' and file_out is not None):
+        file_out.close()
 
     return 0
 
@@ -445,7 +372,19 @@ def main(cmdargs):
                         type=int,
                         default=3,
                         required=False,
-                        help='Number of parts in forest')
+                        help='Number of parts (chunks) in forest')
+
+    parser.add_argument('--parts-in-redshift',
+                        action='store_true',
+                        default=False,
+                        required=False,
+                        help='Define forest parts (chunks) as a function of redshift (observer frame) instead of rest frame')
+
+    parser.add_argument('--z-parts',
+                        nargs='+', type=float,
+                        default=[2.1, 2.3, 2.5, 2.7, 2.9, 3.1, 3.3, 3.5, 3.7, 3.9, 4.1],
+                        required=False,
+                        help='Redshift bin edges for forest parts (chunks) when parts-in-redshift is set')
 
     parser.add_argument('--nb-pixel-min',
                         type=int,
@@ -517,16 +456,6 @@ def main(cmdargs):
                               'matrix was doubly pixelized. Only use this option in '
                               'quickquasars mocks'))
 
-
-    parser.add_argument(
-        '--force-output-in-velocity',
-        default=False,
-        action='store_true',
-        required=False,
-        help=
-        ('store outputs in units of velocity even for linear binning computations'
-         ))
-
     parser.add_argument(
         '--seed',
         default=4,
@@ -562,18 +491,18 @@ def main(cmdargs):
         seed=args.seed
     # Read deltas
     if args.in_format == 'fits':
-        files = sorted(glob.glob(args.in_dir + "/*.fits.gz"))
+        files = sorted(glob.glob(args.in_dir + "/*.fits.gz") +
+                       glob.glob(args.in_dir + "/*.fits"))
     elif args.in_format == 'ascii':
         files = sorted(glob.glob(args.in_dir + "/*.txt"))
 
     # initialize randoms
     np.random.seed(seed)
-    userprint(f"Computing Pk1d for {args.in_dir}")
+    userprint(f"Computing Pk1d for {args.in_dir} ({len(files)} files to process)")
     args.len_files = len(files)
     #create output dir if it does not exist
     os.makedirs(args.out_dir, exist_ok=True)
 
-    print([[i, f] for i, f in enumerate(files)])
     if args.num_processors > 1:
         pool = Pool(args.num_processors)
         index_file_args = [(i, f, args) for i, f in enumerate(files)]
diff --git a/bin/picca_Pk1D_average_mean.py b/bin/picca_Pk1D_average_mean.py
new file mode 100644
index 000000000..ffee5a2d5
--- /dev/null
+++ b/bin/picca_Pk1D_average_mean.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+"""Compute the averaged 1D power spectrum
+"""
+
+import sys, glob, argparse, ast
+from picca.pk1d import postproc_pk1d
+from picca.utils import userprint
+
+
+def main(cmdargs):
+    """Compute the averaged 1D power spectrum averages"""
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        description="Compute the averaged 1D power spectrum averages",
+    )
+
+    parser.add_argument(
+        "--in-dir",
+        type=str,
+        default=None,
+        required=False,
+        help="String to glob all mean p1d to average",
+    )
+
+    parser.add_argument(
+        "--in-filenames",
+        type=str,
+        default=None,
+        required=False,
+        help="String of all mean p1d to average",
+    )
+
+    parser.add_argument(
+        "--output-path",
+        type=str,
+        default=None,
+        required=False,
+        help="Output path",
+    )
+
+    parser.add_argument(
+        "--weighted-mean",
+        action="store_true",
+        default=False,
+        required=False,
+    )
+
+    args = parser.parse_args(cmdargs)
+
+    if (args.in_dir is None and args.in_filenames is None) or (
+        args.in_dir is not None and args.in_filenames is not None
+    ):
+        userprint(
+            (
+                "ERROR: No transmisson input files or both 'in_dir' and "
+                "'in_filenames' given"
+            )
+        )
+        sys.exit()
+    elif args.in_dir is not None:
+        mean_p1d_names = glob.glob(args.in_dir)
+    else:
+        mean_p1d_names = ast.literal_eval(args.in_filenames)
+    userprint("INFO: Found {} files".format(len(mean_p1d_names)))
+
+    postproc_pk1d.average_mean_pk1d_files(
+        mean_p1d_names,
+        args.output_path,
+        weighted_mean=False,
+    )
+
+
+if __name__ == "__main__":
+    cmdargs = sys.argv[1:]
+    main(cmdargs)
diff --git a/bin/picca_Pk1D_cross_exposure.py b/bin/picca_Pk1D_cross_exposure.py
new file mode 100755
index 000000000..0b9cae4c3
--- /dev/null
+++ b/bin/picca_Pk1D_cross_exposure.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python
+"""Compute the individual cross-exposure 1D power spectra
+"""
+
+import sys, os, argparse, glob
+import fitsio
+import numpy as np
+from picca.pk1d.compute_pk1d import compute_pk_cross_exposure, Pk1D
+from picca.utils import userprint
+import multiprocessing as mp
+from functools import partial
+
+
+def treat_pk_file(out_dir, filename):
+    """
+    Takes a single file containing the FFT of delta for
+    multiple exposures and computes the cross-exposure power spectrum. The function
+    returns nothing, but writes to disk a new fits file with all the information needed
+    to compute Pk_cross_exposure. This is done by looping over each targetid and chunkid, 
+    and computing Pk_cross_exposure for each pair of exposures.
+    
+    Arguments
+    ---------
+    out_dir: string
+    The directory path where the cross-exposure will be written
+
+    filename: string
+    The file for which to compute cross-exposure
+
+    Return
+    ------
+    None
+    """
+    fft_delta_list = []
+    file_out = None
+    file_number = filename.split("-")[-1].split(".fits.gz")[0]
+    with fitsio.FITS(filename, "r") as hdus:
+        for _, hdu in enumerate(hdus[1:]):
+            fft_delta_list.append(Pk1D.from_fitsio(hdu))
+
+    targetid_list = np.array(
+        [fft_delta_list[i].los_id for i in range(len(fft_delta_list))]
+    )
+    chunkid_list = np.array(
+        [fft_delta_list[i].chunk_id for i in range(len(fft_delta_list))]
+    )
+
+    unique_targetid = np.unique(targetid_list)
+    unique_chunkid = np.unique(chunkid_list)
+    for los_id in unique_targetid:
+        for chunk_id in unique_chunkid:
+            index = np.argwhere((targetid_list == los_id) & (chunkid_list == chunk_id))
+            if len(index) < 2:
+                continue
+
+            index = np.concatenate(index, axis=0)
+
+            len_delta_list = [fft_delta_list[i].fft_delta.size for i in index]
+            if len(np.unique(len_delta_list)) != 1:
+                userprint(
+                    f"""The exposures of the sub-forest with LOS_ID {los_id}, and CHUNK_ID {chunk_id}"""
+                    """ have different lenghts, they will not be used."""
+                )
+                continue
+
+            ra = fft_delta_list[index[0]].ra
+            dec = fft_delta_list[index[0]].dec
+            z_qso = fft_delta_list[index[0]].z_qso
+            mean_z = fft_delta_list[index[0]].mean_z
+
+            num_masked_pixels = fft_delta_list[index[0]].num_masked_pixels
+            linear_bining = fft_delta_list[index[0]].linear_bining
+
+            k = fft_delta_list[index[0]].k
+
+            mean_snr = np.sqrt(len(index)) * np.mean(
+                [fft_delta_list[i].mean_snr for i in index]
+            )
+            mean_reso = np.mean([fft_delta_list[i].mean_reso for i in index])
+            fft_delta = np.array([fft_delta_list[i].fft_delta for i in index])
+
+            pk_raw_cross_exposure = compute_pk_cross_exposure(
+                fft_delta,
+                fft_delta,
+            )
+
+            # Computation of the noise term with decomposition delta_F = delta_lya + delta_n
+            fft_delta_noise = np.array(
+                [fft_delta_list[i].fft_delta_noise for i in index]
+            )
+            pk_noise_cross_exposure_auto = compute_pk_cross_exposure(
+                fft_delta_noise,
+                fft_delta_noise,
+            )
+            pk_noise_cross_exposure_cross = compute_pk_cross_exposure(
+                fft_delta,
+                fft_delta_noise,
+            )
+
+            pk_noise_cross_exposure = (
+                2 * pk_noise_cross_exposure_cross - pk_noise_cross_exposure_auto
+            )
+
+            # Since diff is a method using exposure differences, it cannot be computed
+            # in a general matter here.
+            pk_diff = np.zeros_like(pk_raw_cross_exposure)
+
+            correction_reso = np.mean(
+                [fft_delta_list[i].correction_reso for i in index], axis=0
+            )
+
+            # Computing pk by taking exposure dependent resolution correction
+            fft_delta_resocorr = np.array(
+                [
+                    fft_delta_list[i].fft_delta
+                    / np.sqrt(fft_delta_list[i].correction_reso)
+                    for i in index
+                ]
+            )
+
+            pk_cross_exposure = compute_pk_cross_exposure(
+                fft_delta_resocorr,
+                fft_delta_resocorr,
+            )
+
+            pk1d_class = Pk1D(
+                ra=ra,
+                dec=dec,
+                z_qso=z_qso,
+                mean_z=mean_z,
+                mean_snr=mean_snr,
+                mean_reso=mean_reso,
+                num_masked_pixels=num_masked_pixels,
+                linear_bining=linear_bining,
+                los_id=los_id,
+                chunk_id=chunk_id,
+                k=k,
+                pk_raw=pk_raw_cross_exposure,
+                pk_noise=pk_noise_cross_exposure,
+                pk_diff=pk_diff,
+                correction_reso=correction_reso,
+                pk=pk_cross_exposure,
+            )
+            if file_out is None:
+                file_out = fitsio.FITS(
+                    (out_dir + "/Pk1D-" + str(file_number) + ".fits.gz"),
+                    "rw",
+                    clobber=True,
+                )
+
+            pk1d_class.write_fits(file_out)
+
+
+def main(cmdargs):
+    """Compute the averaged 1D power spectrum"""
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        description="Compute the averaged 1D power spectrum",
+    )
+
+    parser.add_argument(
+        "--in-dir",
+        type=str,
+        default=None,
+        required=True,
+        help="Directory to individual fft delta files",
+    )
+
+    parser.add_argument(
+        "--out-dir",
+        type=str,
+        default=None,
+        required=True,
+        help="Directory to individual P1D files",
+    )
+    parser.add_argument(
+        "--num-processors",
+        type=int,
+        default=1,
+        required=False,
+        help="Number of processors to use for computation",
+    )
+
+    args = parser.parse_args(cmdargs)
+
+    os.makedirs(args.out_dir, exist_ok=True)
+
+    files = glob.glob(os.path.join(args.in_dir, f"Pk1D-*.fits.gz"))
+
+    func = partial(treat_pk_file, args.out_dir)
+
+    if args.num_processors <= 1:
+        for filename in files:
+            func(filename)
+    else:
+        with mp.Pool(args.num_processors) as pool:
+            pool.map(func, files)
+
+
+if __name__ == "__main__":
+    cmdargs = sys.argv[1:]
+    main(cmdargs)
diff --git a/bin/picca_Pk1D_postprocess.py b/bin/picca_Pk1D_postprocess.py
index f5001837d..62c0417b3 100755
--- a/bin/picca_Pk1D_postprocess.py
+++ b/bin/picca_Pk1D_postprocess.py
@@ -146,6 +146,31 @@ def main(cmdargs):
                         default=None,
                         help='Value of the SNR cut if snr-cut-scheme=fixed')
 
+    parser.add_argument('--skymask-file',
+                        type=str,
+                        default=None,
+                        help='Name of the ASCII file containing skyline masks, which was used to'
+                             'compute the deltas. If set, a correction for this mask will be computed,'
+                             'with "*_skycorr" columns in the output.')
+
+    parser.add_argument('--minwave-skymask',
+                        type=float,
+                        default=utils.DEFAULT_MINWAVE_SKYMASK,
+                        help='Parameter for the wavelength grid used for the skymask,'
+                             'if --skymask-file is set.')
+
+    parser.add_argument('--maxwave-skymask',
+                        type=float,
+                        default=utils.DEFAULT_MAXWAVE_SKYMASK,
+                        help='Parameter for the wavelength grid used for the skymask,'
+                             'if --skymask-file is set.')
+
+    parser.add_argument('--dwave-skymask',
+                        type=float,
+                        default=utils.DEFAULT_DWAVE_SKYMASK,
+                        help='Parameter for the wavelength grid used for the skymask,'
+                             'if --skymask-file is set.')
+
     parser.add_argument('--overwrite',
                         action='store_true',
                         default=False,
@@ -229,6 +254,14 @@ def main(cmdargs):
     else:
         output_file = args.output_file
 
+    if args.skymask_file is not None:
+        skymask_matrices = utils.skyline_mask_matrices_desi(z_edges, args.skymask_file,
+                                minwave=args.minwave_skymask,
+                                maxwave=args.maxwave_skymask,
+                                dwave=args.dwave_skymask)
+    else:
+        skymask_matrices = None
+
     postproc_pk1d.run_postproc_pk1d(args.in_dir, output_file,
                                     z_edges,
                                     k_edges,
@@ -236,6 +269,7 @@ def main(cmdargs):
                                     apply_z_weights=args.apply_z_weights,
                                     output_snrfit=args.output_snrfit,
                                     snrcut=snrcut,
+                                    skymask_matrices=skymask_matrices,
                                     zbins_snrcut=zbins_snrcut,
                                     nomedians=args.no_median,
                                     velunits=args.velunits,
diff --git a/bin/picca_dmat.py b/bin/picca_dmat.py
index 4fe7f11de..959d6bf37 100755
--- a/bin/picca_dmat.py
+++ b/bin/picca_dmat.py
@@ -150,11 +150,13 @@ def main(cmdargs):
         help=('Name of the absorption in picca.constants defining the redshift '
               'of the 2nd delta'))
 
-    parser.add_argument('--z-ref',
-                        type=float,
-                        default=2.25,
-                        required=False,
-                        help='Reference redshift')
+    # remove this option because it has no effect on the output
+    # and hence can lead to confusions
+    # parser.add_argument('--z-ref',
+    #                    type=float,
+    #                    default=2.25,
+    #                    required=False,
+    #                    help='Reference redshift')
 
     parser.add_argument(
         '--z-evol',
@@ -197,11 +199,6 @@ def main(cmdargs):
         required=False,
         help='Equation of state of dark energy of fiducial LambdaCDM cosmology')
 
-    parser.add_argument('--no-project',
-                        action='store_true',
-                        required=False,
-                        help='Do not project out continuum fitting modes')
-
     parser.add_argument(
         '--remove-same-half-plate-close-pairs',
         action='store_true',
@@ -248,6 +245,9 @@ def main(cmdargs):
                         help='Rebin factor for deltas. If not None, deltas will '
                              'be rebinned by that factor')
 
+    parser.add_argument('--no-redshift-evolution',
+                        action='store_true',
+                        help='Ignore redshift evolution when computing distortion matrix')
 
     args = parser.parse_args(cmdargs)
 
@@ -267,8 +267,21 @@ def main(cmdargs):
     cf.num_model_bins_r_par = args.np * args.coef_binning_model
     cf.num_model_bins_r_trans = args.nt * args.coef_binning_model
     cf.nside = args.nside
-    cf.z_ref = args.z_ref
-    cf.alpha = args.z_evol
+
+    if args.no_redshift_evolution :
+        cf.redshift_evolution_in_distortion_matrix = False
+        userprint("ignore redshift evolution in the distortion matrix")
+
+    # this value has no effect because it scales the weights that are both at the numerator and denominator of the estimator
+    # it is also used as a TEMPORARY VARIABLE to compute the distortion matrix scaling
+    # but this is rescaled to the actual effective redshift of the data (zeff) in the following
+    cf.z_ref = 2.25
+
+    cf.alpha  = args.z_evol
+    # by default, for autocorrelations, we are setting cf.alpha2 to the same value
+    # it is overwritten by the value of args.z_evol2 if a second set of delta is given in input
+    cf.alpha2 = args.z_evol
+
     cf.reject = args.rej
     cf.lambda_abs = constants.ABSORBER_IGM[args.lambda_abs]
     cf.remove_same_half_plate_close_pairs = args.remove_same_half_plate_close_pairs
@@ -294,7 +307,6 @@ def main(cmdargs):
                                                   cf.z_ref,
                                                   cosmo,
                                                   max_num_spec=args.nspec,
-                                                  no_project=args.no_project,
                                                   nproc=args.nproc,
                                                   rebin_factor=args.rebin_factor,
                                                   z_min_qso=args.z_min_sources,
@@ -326,7 +338,6 @@ def main(cmdargs):
             cf.z_ref,
             cosmo,
             max_num_spec=args.nspec,
-            no_project=args.no_project,
             nproc=args.nproc,
             rebin_factor=args.rebin_factor,
             z_min_qso=args.z_min_sources,
@@ -369,7 +380,7 @@ def main(cmdargs):
     dmat = np.array([item[1] for item in dmat_data]).sum(axis=0)
     r_par = np.array([item[2] for item in dmat_data]).sum(axis=0)
     r_trans = np.array([item[3] for item in dmat_data]).sum(axis=0)
-    z = np.array([item[4] for item in dmat_data]).sum(axis=0)
+    zeff = np.array([item[4] for item in dmat_data]).sum(axis=0)
     weights = np.array([item[5] for item in dmat_data]).sum(axis=0)
     num_pairs = np.array([item[6] for item in dmat_data]).sum(axis=0)
     num_pairs_used = np.array([item[7] for item in dmat_data]).sum(axis=0)
@@ -378,10 +389,20 @@ def main(cmdargs):
     w = weights > 0.
     r_par[w] /= weights[w]
     r_trans[w] /= weights[w]
-    z[w] /= weights[w]
+    zeff[w] /= weights[w]
+    mean_zeff = np.mean(zeff[w])
+
     w = weights_dmat > 0
     dmat[w] /= weights_dmat[w, None]
 
+    if cf.redshift_evolution_in_distortion_matrix :
+        # now that we have the effective redshift of the input model considered
+        # for the distortion matrix, we do rescale the whole matrix
+        # we first consider the same effective redshift for all the model bins
+        zeff[:]   = mean_zeff
+        zfac = ((1+cf.z_ref)/(1+mean_zeff))**((cf.alpha-1)+(cf.alpha2-1))
+        dmat *= zfac
+
     # save results
     results = fitsio.FITS(args.out, 'rw', clobber=True)
     header = [
@@ -470,7 +491,7 @@ def main(cmdargs):
                   units=['', ''],
                   header=header,
                   extname='DMAT')
-    results.write([r_par, r_trans, z],
+    results.write([r_par, r_trans, zeff],
                   names=['RP', 'RT', 'Z'],
                   comment=['R-parallel', 'R-transverse', 'Redshift'],
                   units=['h^-1 Mpc', 'h^-1 Mpc', ''],
diff --git a/bin/picca_export.py b/bin/picca_export.py
index 3484481c6..337dea8c7 100755
--- a/bin/picca_export.py
+++ b/bin/picca_export.py
@@ -12,6 +12,9 @@
 from picca.utils import smooth_cov, compute_cov
 from picca.utils import userprint
 
+# TODO: add tags here when we are allowed to unblind them
+UNBLINDABLE_STRATEGIES = ["none", "desi_m2", "desi_y1"]
+
 
 def main(cmdargs):
     """Export auto and cross-correlation for the fitter."""
@@ -220,8 +223,7 @@ def main(cmdargs):
         'name': "BLINDING",
         'value': blinding,
         'comment': 'String specifying the blinding strategy'
-    },
-    {
+    }, {
         'name': 'RPMIN',
         'value': r_par_min,
         'comment': 'Minimum r-parallel'
@@ -264,14 +266,17 @@ def main(cmdargs):
         'Covariance matrix', 'Distortion matrix', 'Number of pairs'
     ]
 
+    # Check if we need to unblind
+    if blinding in UNBLINDABLE_STRATEGIES:
+        userprint(f"'{blinding}' correlations are not blinded.")
+        blinding = 'none'
+        data_name = 'DA'
+        dmat_name = 'DM'
+
     # Check if we need blinding and apply it
     if 'BLIND' in data_name or blinding != 'none':
         blinding_dir = '/global/cfs/projectdirs/desi/science/lya/y1-kp6/blinding/'
-        blinding_templates = {'desi_m2': {'standard': 'm2_blinding_v1.2_standard_29_03_2022.h5',
-                                          'grid': 'm2_blinding_v1.2_regular_grid_29_03_2022.h5'},
-                              'desi_y1': {'standard': 'y1_blinding_v2_standard_17_12_2022.h5',
-                                          'grid': 'y1_blinding_v2_regular_grid_17_12_2022.h5'},
-                              'desi_y3': {'standard': 'y3_blinding_v3_standard_18_12_2022.h5',
+        blinding_templates = {'desi_y3': {'standard': 'y3_blinding_v3_standard_18_12_2022.h5',
                                           'grid': 'y3_blinding_v3_regular_grid_18_12_2022.h5'}}
 
         if blinding in blinding_templates:
diff --git a/bin/picca_fast_metal_dmat.py b/bin/picca_fast_metal_dmat.py
index 7377a6647..9dc71e9eb 100755
--- a/bin/picca_fast_metal_dmat.py
+++ b/bin/picca_fast_metal_dmat.py
@@ -58,6 +58,16 @@ def calc_fast_metal_dmat(in_lambda_abs_1,
         Note the global picca.cf contains the cosmology and the rp grid
     """
 
+    # stack_table_1 and _2 are the results of the function
+    # picca.delta_extraction.expected_flux.get_stack_delta,
+    # called in picca.delta_extraction.expected_flux.hdu_stack_deltas
+    # It is the stack of weights =
+    # picca.delta_extraction.expected_flux.compute_forest_weights(forest, forest.continuum).
+    # This function is implemented dr16_expected_flux.Dr16ExpectedFlux.compute_forest_weights
+    # and contains the terms  eta * var_pipe + self.var_lss_mod*var_lss + fudge / var_pipe
+    # some being set at 0 or 1 depending on the configuration.
+    # It does NOT contain the term of redshift evolution (1+z)^(apha-1).
+
     loglam1 = stack_table_1["LOGLAM"]
     weight1 = stack_table_1["WEIGHT"]
     loglam2 = stack_table_2["LOGLAM"]
@@ -85,6 +95,8 @@ def calc_fast_metal_dmat(in_lambda_abs_1,
         input_r2[None, :]).ravel()  # same sign as line 676 of cf.py (1-2)
     if not cf.x_correlation:
         input_rp = np.abs(input_rp)
+    input_dist  = ((input_r1[:, None]+input_r2[None, :])/2).ravel()
+
     # output
     output_z1 = (10**loglam1) / constants.ABSORBER_IGM[out_lambda_abs_1] - 1.
     output_z2 = (10**loglam2) / constants.ABSORBER_IGM[out_lambda_abs_2] - 1.
@@ -96,6 +108,7 @@ def calc_fast_metal_dmat(in_lambda_abs_1,
         output_r2[None, :]).ravel()  # same sign as line 676 of cf.py (1-2)
     if not cf.x_correlation:
         output_rp = np.abs(output_rp)
+    output_dist = ((output_r1[:, None]+output_r2[None, :])/2).ravel()
 
     # weights
     # alpha_in: in (1+z)^(alpha_in-1) is a scaling used to model how the metal contribution
@@ -114,23 +127,64 @@ def calc_fast_metal_dmat(in_lambda_abs_1,
         (weight2 *
          ((1 + input_z2)**(alpha_in_2 + alpha_out_2 - 2)))[None, :]).ravel()
 
-    # distortion matrix
+    # r_par distortion matrix
     rpbins   = cf.r_par_min \
         + (cf.r_par_max-cf.r_par_min)/cf.num_bins_r_par*np.arange(cf.num_bins_r_par+1)
 
     # I checked the orientation of the matrix
-    dmat, _, _ = np.histogram2d(output_rp,
-                                input_rp,
-                                bins=(rpbins, rpbins),
-                                weights=weights)
-
-    # normalize (sum of weight should be one for each input rp,rt)
-    sum_in_weight, _ = np.histogram(input_rp, bins=rpbins, weights=weights)
-    dmat *= ((sum_in_weight > 0) / (sum_in_weight +
-                                    (sum_in_weight == 0)))[None, :]
-
-    # mean outputs
-    sum_out_weight, _ = np.histogram(output_rp, bins=rpbins, weights=weights)
+    rp_1d_dmat, _, _ = np.histogram2d(output_rp,
+                                      input_rp,
+                                      bins=(rpbins, rpbins),
+                                      weights=weights)
+    # normalize
+    sum_rp_1d_dmat = np.sum(rp_1d_dmat,axis=0)
+    rp_1d_dmat    /= (sum_rp_1d_dmat+(sum_rp_1d_dmat==0))
+
+    # independently, we compute the r_trans distortion matrix
+    rtbins   = cf.r_trans_max/cf.num_bins_r_trans*np.arange(cf.num_bins_r_trans+1)
+    # we have input_dist , output_dist and weight.
+    # we don't need to store the absolute comoving distances but the ratio between output and input.
+    # we rebin that to compute the rest faster.
+    # histogram of distance scaling with proper weights:
+    # dist*theta = r_trans
+    # theta_max  = r_trans_max/dist
+    # solid angle contibuting for each distance propto theta_max**2 = (r_trans_max/dist)**2 propto 1/dist**2
+    # we weight the distances with this additional factor
+    # using the input or the output distance in the solid angle weight gives virtually the same result
+    #distance_ratio_weights,distance_ratio_bins = np.histogram(output_dist/input_dist,bins=4*rtbins.size,weights=weights/input_dist**2*(input_rp<cf.r_par_max)*(input_rp>cf.r_par_min))
+    # we also select only distance ratio for which the input_rp (that of the true separation of the absorbers) is small, so that this
+    # fast matrix calculation is accurate where it matters the most
+    distance_ratio_weights,distance_ratio_bins = np.histogram(output_dist/input_dist,bins=4*rtbins.size,weights=weights/input_dist**2*(np.abs(input_rp)<20.))
+    distance_ratios=(distance_ratio_bins[1:]+distance_ratio_bins[:-1])/2.
+
+    # now we need to scan as a function of separation angles, or equivalently rt.
+    rt_bin_centers = (rtbins[:-1]+rtbins[1:])/2.
+    rt_bin_half_size = (rtbins[1]-rtbins[0])/2.
+    # we are oversampling the correlation function rt grid to correctly compute bin migration.
+    oversample  = 7
+    delta_rt    = np.linspace(-rt_bin_half_size,rt_bin_half_size*(1-2./oversample),oversample)[None,:] # the -2/oversample term is needed to get a even-spaced grid
+    rt_1d_dmat  = np.zeros((cf.num_bins_r_trans,cf.num_bins_r_trans))
+    for i,rt in enumerate(rt_bin_centers) :
+        # the weight is proportional to rt+delta_rt to get the correct solid angle effect inside the bin (but it's almost a negligible effect)
+        rt_1d_dmat[:,i],_ = np.histogram((distance_ratios[:,None]*(rt+delta_rt)[None,:]).ravel(),bins=rtbins,weights=(distance_ratio_weights[:,None]*(rt+delta_rt)[None,:]).ravel())
+
+    # normalize
+    sum_rt_1d_dmat = np.sum(rt_1d_dmat,axis=0)
+    rt_1d_dmat    /= (sum_rt_1d_dmat+(sum_rt_1d_dmat==0))
+
+    # now that we have both distortion along r_par and r_trans, we have to combine them
+    # we just multiply the two matrices, with indices splitted for rt and rp
+    # full_index = rt_index + cf.num_bins_r_trans * rp_index
+    # rt_index   = full_index%cf.num_bins_r_trans
+    # rp_index  = full_index//cf.num_bins_r_trans
+    num_bins_total = cf.num_bins_r_par * cf.num_bins_r_trans
+    dmat = np.zeros((num_bins_total,num_bins_total))
+    pp   = np.arange(num_bins_total,dtype=int)
+    for k in range(num_bins_total) :
+        dmat[k,pp] = rt_1d_dmat[k%cf.num_bins_r_trans,pp%cf.num_bins_r_trans] * rp_1d_dmat[k//cf.num_bins_r_trans,pp//cf.num_bins_r_trans]
+
+    # compute effective z,rp,rt
+    sum_out_weight, _    = np.histogram(output_rp, bins=rpbins, weights=weights)
     sum_out_weight_rp, _ = np.histogram(output_rp,
                                         bins=rpbins,
                                         weights=weights *
@@ -143,34 +197,27 @@ def calc_fast_metal_dmat(in_lambda_abs_1,
         bins=rpbins,
         weights=weights *
         (((input_z1[:, None] + input_z2[None, :]) / 2.).ravel()))
-    r_par_eff = sum_out_weight_rp / (sum_out_weight + (sum_out_weight == 0))
-    z_eff = sum_out_weight_z / (sum_out_weight + (sum_out_weight == 0))
 
-    # we could return the quantities computed as a function of rp only (and not rt):
-    # return dmat, r_par_eff, r_trans_eff, z_eff
-    # but for now we will return the full dmat to be consistent with the other computation
-    # it consists in duplicating the result found to all rt, with output_rt = input_rt
-    num_bins_total = cf.num_bins_r_par * cf.num_bins_r_trans
+    r_par_eff_1d = sum_out_weight_rp / (sum_out_weight + (sum_out_weight == 0))
+    z_eff_1d     = sum_out_weight_z / (sum_out_weight + (sum_out_weight == 0))
+
+    # r_trans has no weights here
+    r1 = np.arange(cf.num_bins_r_trans)     * cf.r_trans_max / cf.num_bins_r_trans
+    r2 = (1+np.arange(cf.num_bins_r_trans)) * cf.r_trans_max / cf.num_bins_r_trans
+    r_trans_eff_1d = (2*(r2**3-r1**3))/(3*(r2**2-r1**2)) # this is to account for the solid angle effect on the mean
+
+    full_index = np.arange(num_bins_total)
+    rt_index   = full_index%cf.num_bins_r_trans
+    rp_index   = full_index//cf.num_bins_r_trans
+
+    r_par_eff_2d   = r_par_eff_1d[rp_index]
+    z_eff_2d       = z_eff_1d[rp_index]
+    r_trans_eff_2d = r_trans_eff_1d[rt_index]
+
+    return dmat, r_par_eff_2d, r_trans_eff_2d, z_eff_2d
+
 
-    full_dmat = np.zeros((num_bins_total, num_bins_total))
-    full_r_par_eff = np.zeros(num_bins_total)
-    full_r_trans_eff = np.zeros(num_bins_total)
-    full_z_eff = np.zeros(num_bins_total)
-    r_par_indices = np.arange(cf.num_bins_r_par)
-    r_trans = (0.5 + np.arange(
-        cf.num_bins_r_trans)) * cf.r_trans_max / cf.num_bins_r_trans
-    for j in range(cf.num_bins_r_trans):
-        indices = j + cf.num_bins_r_trans * r_par_indices
-        for k, i in zip(indices, r_par_indices):
-            full_dmat[indices, k] = dmat[r_par_indices, i]
-        full_r_par_eff[indices] = r_par_eff
-        full_z_eff[indices] = z_eff
-        full_r_trans_eff[indices] = r_trans[j]
-
-    return full_dmat, full_r_par_eff, full_r_trans_eff, full_z_eff
-
-
-def main():
+def main(cmdargs):
     # pylint: disable-msg=too-many-locals,too-many-branches,too-many-statements
     """Compute the auto and cross-correlation of delta fields for a list of IGM
     absorption."""
@@ -386,7 +433,7 @@ def main():
                     help='compute only the metal correlations used by Vega'
                        'i.e. 4 LyaxSi matrices and CIVxCIV')
 
-    args = parser.parse_args()
+    args = parser.parse_args(cmdargs)
 
     # setup variables in module cf
     cf.r_par_max = args.rp_max
@@ -621,4 +668,5 @@ def main():
 
 
 if __name__ == '__main__':
-    main()
+    cmdargs=sys.argv[1:]
+    main(cmdargs)
diff --git a/bin/picca_fast_metal_xdmat.py b/bin/picca_fast_metal_xdmat.py
index 4c91a6fa1..78125c5b1 100755
--- a/bin/picca_fast_metal_xdmat.py
+++ b/bin/picca_fast_metal_xdmat.py
@@ -76,6 +76,7 @@ def calc_fast_metal_dmat(in_lambda_abs,
     input_rp = (
         input_rf[:, None] -
         r_qso[None, :]).ravel()  # same sign as line 528 of xcf.py (forest-qso)
+    input_dist  = ((input_rf[:, None]+r_qso[None, :])/2).ravel()
 
     # output
     output_zf = (10**loglam) / constants.ABSORBER_IGM[out_lambda_abs] - 1.
@@ -85,6 +86,7 @@ def calc_fast_metal_dmat(in_lambda_abs,
     output_rp = (
         output_rf[:, None] -
         r_qso[None, :]).ravel()  # same sign as line 528 of xcf.py (forest-qso)
+    output_dist = ((output_rf[:, None]+r_qso[None, :])/2).ravel()
 
     # weights
     # alpha_in: in (1+z)^(alpha_in-1) is a scaling used to model how the metal contribution
@@ -100,21 +102,62 @@ def calc_fast_metal_dmat(in_lambda_abs,
                 ((1 + input_zf)**(alpha_in + alpha_out - 2)))[:, None] *
                weight_qso[None, :]).ravel()
 
-    # distortion matrix
+    # r_par distortion matrix
     rpbins = xcf.r_par_min + (
         xcf.r_par_max -
         xcf.r_par_min) / xcf.num_bins_r_par * np.arange(xcf.num_bins_r_par + 1)
 
     # I checked the orientation of the matrix
-    dmat, _, _ = np.histogram2d(output_rp,
+    rp_1d_dmat, _, _ = np.histogram2d(output_rp,
                                 input_rp,
                                 bins=(rpbins, rpbins),
                                 weights=weights)
+    # normalize
+    sum_rp_1d_dmat = np.sum(rp_1d_dmat,axis=0)
+    rp_1d_dmat    /= (sum_rp_1d_dmat+(sum_rp_1d_dmat==0))
+
+    # independently, we compute the r_trans distortion matrix
+    rtbins   = xcf.r_trans_max/xcf.num_bins_r_trans*np.arange(xcf.num_bins_r_trans+1)
+    # we have input_dist , output_dist and weight.
+    # we don't need to store the absolute comoving distances but the ratio between output and input.
+    # we rebin that to compute the rest faster.
+    # histogram of distance scaling with proper weights:
+    # dist*theta = r_trans
+    # theta_max  = r_trans_max/dist
+    # solid angle contibuting for each distance propto theta_max**2 = (r_trans_max/dist)**2 propto 1/dist**2
+    # we weight the distances with this additional factor
+    # using the input or the output distance in the solid angle weight gives virtually the same result
+    # we also select only distance ratio for which the input_rp (that of the true separation of the absorbers) is small, so that this
+    # fast matrix calculation is accurate where it matters the most
+    distance_ratio_weights,distance_ratio_bins = np.histogram(output_dist/input_dist,bins=4*rtbins.size,weights=weights/input_dist**2*(np.abs(input_rp)<20.))
+    distance_ratios=(distance_ratio_bins[1:]+distance_ratio_bins[:-1])/2.
+
+    # now we need to scan as a function of separation angles, or equivalently rt.
+    rt_bin_centers = (rtbins[:-1]+rtbins[1:])/2.
+    rt_bin_half_size = (rtbins[1]-rtbins[0])/2.
+    # we are oversampling the correlation function rt grid to correctly compute bin migration.
+    oversample  = 7
+    delta_rt    = np.linspace(-rt_bin_half_size,rt_bin_half_size*(1-2./oversample),oversample)[None,:] # the -2/oversample term is needed to get a even-spaced grid
+    rt_1d_dmat  = np.zeros((xcf.num_bins_r_trans,xcf.num_bins_r_trans))
+    for i,rt in enumerate(rt_bin_centers) :
+        # the weight is proportional to rt to get the correct solid angle effect (it's almost a negligible effect)
+        rt_1d_dmat[:,i],_ = np.histogram((distance_ratios[:,None]*(rt+delta_rt)[None,:]).ravel(),bins=rtbins,weights=(distance_ratio_weights[:,None]*(rt+delta_rt)[None,:]).ravel())
+
+    # normalize
+    sum_rt_1d_dmat = np.sum(rt_1d_dmat,axis=0)
+    rt_1d_dmat    /= (sum_rt_1d_dmat+(sum_rt_1d_dmat==0))
+
+    # now that we have both distortion along r_par and r_trans, we have to combine them
+    # we just multiply the two matrices, with indices splitted for rt and rp
+    # full_index = rt_index + xcf.num_bins_r_trans * rp_index
+    # rt_index   = full_index%xcf.num_bins_r_trans
+    # rp_index  = full_index//xcf.num_bins_r_trans
+    num_bins_total = xcf.num_bins_r_par * xcf.num_bins_r_trans
+    dmat = np.zeros((num_bins_total,num_bins_total))
+    pp   = np.arange(num_bins_total,dtype=int)
+    for k in range(num_bins_total) :
+        dmat[k,pp] = rt_1d_dmat[k%xcf.num_bins_r_trans,pp%xcf.num_bins_r_trans] * rp_1d_dmat[k//xcf.num_bins_r_trans,pp//xcf.num_bins_r_trans]
 
-    # normalize (sum of weight should be one for each input rp,rt)
-    sum_in_weight, _ = np.histogram(input_rp, bins=rpbins, weights=weights)
-    dmat *= ((sum_in_weight > 0) / (sum_in_weight +
-                                    (sum_in_weight == 0)))[None, :]
 
     # mean outputs
     sum_out_weight, _ = np.histogram(output_rp, bins=rpbins, weights=weights)
@@ -130,34 +173,27 @@ def calc_fast_metal_dmat(in_lambda_abs,
         bins=rpbins,
         weights=weights *
         (((input_zf[:, None] + z_qso[None, :]) / 2.).ravel()))
-    r_par_eff = sum_out_weight_rp / (sum_out_weight + (sum_out_weight == 0))
-    z_eff = sum_out_weight_z / (sum_out_weight + (sum_out_weight == 0))
 
-    # we could return the quantities computed as a function of rp only (and not rt):
-    # return dmat, r_par_eff, r_trans_eff, z_eff
-    # but for now we will return the full dmat to be consistent with the other computation
-    # it consists in duplicating the result found to all rt, with output_rt = input_rt
-    num_bins_total = xcf.num_bins_r_par * xcf.num_bins_r_trans
+    r_par_eff_1d = sum_out_weight_rp / (sum_out_weight + (sum_out_weight == 0))
+    z_eff_1d     = sum_out_weight_z / (sum_out_weight + (sum_out_weight == 0))
+
+    # r_trans has no weights here
+    r1 = np.arange(xcf.num_bins_r_trans)     * xcf.r_trans_max / xcf.num_bins_r_trans
+    r2 = (1+np.arange(xcf.num_bins_r_trans)) * xcf.r_trans_max / xcf.num_bins_r_trans
+    r_trans_eff_1d = (2*(r2**3-r1**3))/(3*(r2**2-r1**2)) # this is to account for the solid angle effect on the mean
+
+    full_index = np.arange(num_bins_total)
+    rt_index   = full_index%xcf.num_bins_r_trans
+    rp_index   = full_index//xcf.num_bins_r_trans
+
+    r_par_eff_2d   = r_par_eff_1d[rp_index]
+    z_eff_2d       = z_eff_1d[rp_index]
+    r_trans_eff_2d = r_trans_eff_1d[rt_index]
+
+    return dmat, r_par_eff_2d, r_trans_eff_2d, z_eff_2d
+
 
-    full_dmat = np.zeros((num_bins_total, num_bins_total))
-    full_r_par_eff = np.zeros(num_bins_total)
-    full_r_trans_eff = np.zeros(num_bins_total)
-    full_z_eff = np.zeros(num_bins_total)
-    r_par_indices = np.arange(xcf.num_bins_r_par)
-    r_trans = (0.5 + np.arange(
-        xcf.num_bins_r_trans)) * xcf.r_trans_max / xcf.num_bins_r_trans
-    for j in range(xcf.num_bins_r_trans):
-        indices = j + xcf.num_bins_r_trans * r_par_indices
-        for k, i in zip(indices, r_par_indices):
-            full_dmat[indices, k] = dmat[r_par_indices, i]
-        full_r_par_eff[indices] = r_par_eff
-        full_z_eff[indices] = z_eff
-        full_r_trans_eff[indices] = r_trans[j]
-
-    return full_dmat, full_r_par_eff, full_r_trans_eff, full_z_eff
-
-
-def main():
+def main(cmdargs):
     """Compute the metal matrix of the cross-correlation delta x object for
      a list of IGM absorption."""
     parser = argparse.ArgumentParser(
@@ -372,7 +408,7 @@ def main():
                         required=False,
                         help='Bins for the distribution of QSO redshifts')
 
-    args = parser.parse_args()
+    args = parser.parse_args(cmdargs)
 
     # setup variables in module xcf
     xcf.r_par_max = args.rp_max
@@ -579,4 +615,5 @@ def main():
 
 
 if __name__ == '__main__':
-    main()
+    cmdargs=sys.argv[1:]
+    main(cmdargs)
diff --git a/bin/picca_plot_pk1d.py b/bin/picca_plot_pk1d.py
deleted file mode 100755
index ff9b50383..000000000
--- a/bin/picca_plot_pk1d.py
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/usr/bin/env python
-"""Plots the 1D Power Spectrum
-"""
-import argparse
-import glob
-import sys
-import fitsio
-import numpy as np
-import matplotlib.pyplot as plt
-
-from picca.utils import userprint
-from picca.pk1d import Pk1D
-
-
-def main(cmdargs):
-    """Plots the 1D Power Spectrum"""
-    parser = argparse.ArgumentParser(
-        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-
-    parser.add_argument('--in-dir',
-                        type=str,
-                        default=None,
-                        required=True,
-                        help='data directory')
-
-    parser.add_argument('--SNR-min',
-                        type=float,
-                        default=2.,
-                        required=False,
-                        help='minimal mean SNR per pixel ')
-
-    parser.add_argument('--z-min',
-                        type=float,
-                        default=2.1,
-                        required=False,
-                        help='minimal mean absorption redshift ')
-
-    parser.add_argument('--reso-max',
-                        type=float,
-                        default=85.,
-                        required=False,
-                        help='maximal resolution in km/s ')
-
-    parser.add_argument('--out-fig',
-                        type=str,
-                        default='Pk1D.png',
-                        required=False,
-                        help='data directory')
-
-    args = parser.parse_args(cmdargs)
-
-    # Binning corresponding to BOSS paper
-    num_z_bins = 13
-    num_k_bins = 35
-    k_min = 0.000813
-    k_max = k_min + num_k_bins * 0.000542
-
-    # initialize arrays
-    sum_pk = np.zeros([num_z_bins, num_k_bins], dtype=np.float64)
-    sum_pk_square = np.zeros([num_z_bins, num_k_bins], dtype=np.float64)
-    counts = np.zeros([num_z_bins, num_k_bins], dtype=np.float64)
-    k = np.zeros([num_k_bins], dtype=np.float64)
-    for index_k in range(num_k_bins):
-        k[index_k] = k_min + (index_k + 0.5) * 0.000542
-
-    # list of Pk(1D)
-    files = sorted(glob.glob(args.in_dir + "/*.fits.gz"))
-
-    num_data = 0
-
-    # loop over input files
-    for index, file in enumerate(files):
-        if index % 1 == 0:
-            sys.stderr.write("\rread {} of {} {}".format(
-                index, len(files), num_data))
-
-        # read fits files
-        hdul = fitsio.FITS(file)
-        pk1ds = [Pk1D.from_fitsio(hdu) for hdu in hdul[1:]]
-        num_data += len(pk1ds)
-        userprint("\n ndata =  ", num_data)
-
-        # loop over pk1ds
-        for pk in pk1ds:
-
-            # Selection over the SNR and the resolution
-            if pk.mean_snr <= args.SNR_min or pk.mean_reso >= args.reso_max:
-                continue
-
-            if pk.mean_z <= args.z_min:
-                continue
-
-            index_z = int((pk.mean_z - args.z_min) / 0.2)
-            if index_z >= num_z_bins or index_z < 0:
-                continue
-
-            for index2 in range(len(pk.k)):
-                index_k = int(
-                    (pk.k[index2] - k_min) / (k_max - k_min) * num_k_bins)
-                if index_k >= num_k_bins or index_k < 0:
-                    continue
-                sum_pk[index_z, index_k] += pk.pk[index2] * pk.k[index2] / np.pi
-                sum_pk_square[index_z, index_k] += (pk.pk[index2] *
-                                                    pk.k[index2] / np.pi)**2
-                counts[index_z, index_k] += 1.0
-
-    # compute mean and error on Pk
-    mean_pk = np.where(counts != 0, sum_pk / counts, 0.0)
-    error_pk = np.where(
-        counts != 0, np.sqrt(((sum_pk_square / counts) - mean_pk**2) / counts),
-        0.0)
-
-    # plot settings
-    colors = [
-        'm', 'r', 'b', 'k', 'chartreuse', 'gold', 'aqua', 'slateblue', 'orange',
-        'mediumblue', 'darkgrey', 'olive', 'firebrick'
-    ]
-    markersize = 6
-    fontsize = 16
-    labelsize = 10
-    legendsize = 12
-
-    fig = plt.figure(figsize=(16, 8))
-    ax = fig.add_subplot(1, 1, 1)
-    ax.set_yscale('log')
-
-    for index_z in range(num_z_bins):
-        z = 2.2 + index_z * 0.2
-        #ax.errorbar(k, mean_pk[index_z,:], yerr=error_pk[index_z,:], fmt='o',
-        #            color=colors[index_z], markersize=markersize,
-        #            label =r('\bf {:1.1f} $\displaystyle <$ '
-        #                     'z $\displaystyle <$ {:1.1f}').format(z - 0.1,
-        #                                                           z + 0.1))
-        ax.errorbar(k,
-                    mean_pk[index_z, :],
-                    yerr=error_pk[index_z, :],
-                    fmt='o',
-                    color=colors[index_z],
-                    markersize=markersize,
-                    label=r' z = {:1.1f}'.format(z))
-
-    #ax.set_xlabel(r'\bf $\displaystyle k [km.s^{-1}]$', fontsize=fontsize)
-    ax.set_xlabel(r' k [km/s]', fontsize=fontsize)
-    #ax.set_ylabel(r'\bf $\displaystyle \Delta^2_{\mathrm{1d}}(k) $',
-    #              fontsize=fontsize, labelpad=-1)
-    ax.set_ylabel(r'P(k)k/pi ', fontsize=fontsize, labelpad=-1)
-    ax.xaxis.set_ticks_position('both')
-    ax.xaxis.set_tick_params(direction='in')
-    ax.yaxis.set_ticks_position('both')
-    ax.yaxis.set_tick_params(direction='in')
-    ax.xaxis.set_tick_params(labelsize=labelsize)
-    ax.yaxis.set_tick_params(labelsize=labelsize)
-
-    handles, labels = ax.get_legend_handles_labels()
-    ax.legend(handles,
-              labels,
-              loc=2,
-              bbox_to_anchor=(1.03, 0.98),
-              borderaxespad=0.,
-              fontsize=legendsize)
-    fig.subplots_adjust(top=0.98,
-                        bottom=0.114,
-                        left=0.078,
-                        right=0.758,
-                        hspace=0.2,
-                        wspace=0.2)
-
-    plt.show()
-    fig.savefig(args.out_fig, transparent=False)
-
-    userprint("all done ")
-
-
-if __name__ == '__main__':
-    cmdargs=sys.argv[1:]
-    main(cmdargs)
diff --git a/bin/picca_xdmat.py b/bin/picca_xdmat.py
index a4f18b546..1da22d49b 100755
--- a/bin/picca_xdmat.py
+++ b/bin/picca_xdmat.py
@@ -163,11 +163,13 @@ def main(cmdargs):
         help=('Name of the absorption in picca.constants defining the redshift '
               'of the delta'))
 
-    parser.add_argument('--z-ref',
-                        type=float,
-                        default=2.25,
-                        required=False,
-                        help='Reference redshift')
+    # remove this option because it has no effect on the output
+    # and hence can lead to confusions
+    # parser.add_argument('--z-ref',
+    #                    type=float,
+    #                    default=2.25,
+    #                    required=False,
+    #                    help='Reference redshift')
 
     parser.add_argument(
         '--z-evol-del',
@@ -242,6 +244,10 @@ def main(cmdargs):
                         help='Rebin factor for deltas. If not None, deltas will '
                              'be rebinned by that factor')
 
+    parser.add_argument('--no-redshift-evolution',
+                        action='store_true',
+                        help='Ignore redshift evolution when computing distortion matrix')
+
     args = parser.parse_args(cmdargs)
     if args.nproc is None:
         args.nproc = cpu_count() // 2
@@ -259,8 +265,18 @@ def main(cmdargs):
     xcf.num_model_bins_r_par = args.np * args.coef_binning_model
     xcf.num_model_bins_r_trans = args.nt * args.coef_binning_model
     xcf.nside = args.nside
-    xcf.z_ref = args.z_ref
+
+    if args.no_redshift_evolution :
+        xcf.redshift_evolution_in_distortion_matrix = False
+        userprint("ignore redshift evolution in the distortion matrix")
+
+    # this value has no effect because it scales the weights that are both at the numerator and denominator of the estimator
+    # it is also used as a TEMPORARY VARIABLE to compute the distortion matrix scaling
+    # but this is rescaled to the actual effective redshift of the data (zeff) in the following
+    xcf.z_ref = 2.25
+
     xcf.alpha = args.z_evol_del
+    xcf.alpha_obj = args.z_evol_obj
     xcf.lambda_abs = constants.ABSORBER_IGM[args.lambda_abs]
     xcf.reject = args.rej
 
@@ -281,7 +297,7 @@ def main(cmdargs):
                                                   args.nside,
                                                   xcf.lambda_abs,
                                                   args.z_evol_del,
-                                                  args.z_ref,
+                                                  xcf.z_ref,
                                                   cosmo=cosmo,
                                                   max_num_spec=args.nspec,
                                                   nproc=args.nproc,
@@ -307,7 +323,7 @@ def main(cmdargs):
 
     ### Read objects
     objs, z_min2 = io.read_objects(args.drq, args.nside, args.z_min_obj,
-                                   args.z_max_obj, args.z_evol_obj, args.z_ref,
+                                   args.z_max_obj, args.z_evol_obj, xcf.z_ref,
                                    cosmo, mode=args.mode)
     userprint("\n")
     xcf.objs = objs
@@ -346,7 +362,7 @@ def main(cmdargs):
     dmat = np.array([item[1] for item in dmat_data]).sum(axis=0)
     r_par = np.array([item[2] for item in dmat_data]).sum(axis=0)
     r_trans = np.array([item[3] for item in dmat_data]).sum(axis=0)
-    z = np.array([item[4] for item in dmat_data]).sum(axis=0)
+    zeff = np.array([item[4] for item in dmat_data]).sum(axis=0)
     weights = np.array([item[5] for item in dmat_data]).sum(axis=0)
     num_pairs = np.array([item[6] for item in dmat_data]).sum(axis=0)
     num_pairs_used = np.array([item[7] for item in dmat_data]).sum(axis=0)
@@ -355,10 +371,21 @@ def main(cmdargs):
     w = weights > 0.
     r_par[w] /= weights[w]
     r_trans[w] /= weights[w]
-    z[w] /= weights[w]
+    zeff[w] /= weights[w]
+    mean_zeff = np.mean(zeff[w])
+
     w = weights_dmat > 0.
     dmat[w, :] /= weights_dmat[w, None]
 
+
+    if xcf.redshift_evolution_in_distortion_matrix :
+        # now that we have the effective redshift of the input model considered
+        # for the distortion matrix, we do rescale the whole matrix
+        # we first consider the same effective redshift for all the model bins
+        zeff[:]   = mean_zeff
+        zfac = ((1+xcf.z_ref)/(1+mean_zeff))**((xcf.alpha-1)+(xcf.alpha_obj-1))
+        dmat *= zfac
+
     # save results
     results = fitsio.FITS(args.out, 'rw', clobber=True)
     header = [
@@ -447,7 +474,7 @@ def main(cmdargs):
                   units=['', ''],
                   header=header,
                   extname='DMAT')
-    results.write([r_par, r_trans, z],
+    results.write([r_par, r_trans, zeff],
                   names=['RP', 'RT', 'Z'],
                   comment=['R-parallel', 'R-transverse', 'Redshift'],
                   units=['h^-1 Mpc', 'h^-1 Mpc', ''],
diff --git a/py/picca/_version.py b/py/picca/_version.py
index 8356705df..3cd81c284 100644
--- a/py/picca/_version.py
+++ b/py/picca/_version.py
@@ -1,1 +1,1 @@
-__version__ = '8.3.2'
+__version__ = '9.8.0'
diff --git a/py/picca/cf.py b/py/picca/cf.py
index 559793cd6..7243891bf 100644
--- a/py/picca/cf.py
+++ b/py/picca/cf.py
@@ -57,6 +57,9 @@
 ang_correlation = False
 remove_same_half_plate_close_pairs = False
 
+# variables for distortion matrix
+redshift_evolution_in_distortion_matrix = True
+
 # variables used in the 1D correlation function analysis
 num_pixels = None
 log_lambda_min = None
@@ -458,6 +461,15 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
 
             weights12 = weights1[i] * weights2[j]
             z = (z1[i] + z2[j]) / 2
+            # default cf.alpha = args.z_evol = 2.9
+            # cf.alpha2 = cf.alpha for auto-correlation
+            # and cf.alpha2 = args.z_evol2 = 2.9 for cross-correlation with other deltas
+            # this scale factor applies to all of the eta terms
+            # it depends on i and j so it cannot be factored out
+            if redshift_evolution_in_distortion_matrix :
+                zfac = (((1+z1[i])/(1+z_ref))**(alpha-1))*(((1+z2[j])/(1+z_ref))**(alpha2-1))
+            else :
+                zfac = 1.
 
             bins_r_par = np.floor(
                 (r_par - r_par_min) / (r_par_max - r_par_min) * num_bins_r_par)
@@ -492,15 +504,15 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
 
             # first eta, first term: kronecker delta
             # second eta, second term: weight/sum(weights)
-            eta1[i + num_pixels1 * model_bins] += weights2[j] / sum_weights2
+            eta1[i + num_pixels1 * model_bins] += zfac *weights2[j] / sum_weights2
 
             # first eta, second term: weight/sum(weights)
             # second eta, first term: kronecker delta
-            eta2[j + num_pixels2 * model_bins] += weights1[i] / sum_weights1
+            eta2[j + num_pixels2 * model_bins] += zfac *weights1[i] / sum_weights1
 
             # first eta, second term: weight/sum(weights)
             # second eta, second term: weight/sum(weights)
-            eta5[model_bins] += weights12 / sum_weights1 / sum_weights2
+            eta5[model_bins] += zfac * weights12 / sum_weights1 / sum_weights2
 
             if order2 == 1:
                 # first eta, first term: kronecker delta
@@ -508,7 +520,7 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
                 #   weight*(Lambda-bar(Lambda))*(Lambda-bar(Lambda))/
                 #   sum(weight*(Lambda-bar(Lambda)**2))
                 eta3[i + num_pixels1 * model_bins] += (
-                    weights2[j] * log_lambda_minus_mean2[j] /
+                    zfac * weights2[j] * log_lambda_minus_mean2[j] /
                     sum_weights_square_log_lambda_minus_mean2)
 
                 # first eta, second term: weight/sum(weights)
@@ -516,7 +528,7 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
                 #   weight*(Lambda-bar(Lambda))*(Lambda-bar(Lambda))/
                 #   sum(weight*(Lambda-bar(Lambda)**2))
                 eta6[model_bins] += (
-                    weights1[i] / sum_weights1 *
+                    zfac * weights1[i] / sum_weights1 *
                     (weights2[j] * log_lambda_minus_mean2[j] /
                      sum_weights_square_log_lambda_minus_mean2))
             if order1 == 1:
@@ -525,14 +537,14 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
                 #   sum(weight*(Lambda-bar(Lambda)**2))
                 # second eta, first term: kronecker delta
                 eta4[j + num_pixels2 * model_bins] += (
-                    weights1[i] * log_lambda_minus_mean1[i] /
+                    zfac * weights1[i] * log_lambda_minus_mean1[i] /
                     sum_weights_square_log_lambda_minus_mean1)
                 # first eta, third term: (non-zero only for order=1)
                 #   weight*(Lambda-bar(Lambda))*(Lambda-bar(Lambda))/
                 #   sum(weight*(Lambda-bar(Lambda)**2))
                 # second eta, second term: weight/sum(weights)
                 eta7[model_bins] += (
-                    weights2[j] / sum_weights2 *
+                    zfac * weights2[j] / sum_weights2 *
                     (weights1[i] * log_lambda_minus_mean1[i] /
                      sum_weights_square_log_lambda_minus_mean1))
                 if order2 == 1:
@@ -543,7 +555,7 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
                     #   weight*(Lambda-bar(Lambda))*(Lambda-bar(Lambda))/
                     #   sum(weight*(Lambda-bar(Lambda)**2)
                     eta8[model_bins] += (
-                        weights1[i] * log_lambda_minus_mean1[i] * weights2[j] *
+                        zfac * weights1[i] * log_lambda_minus_mean1[i] * weights2[j] *
                         log_lambda_minus_mean2[j] /
                         sum_weights_square_log_lambda_minus_mean1 /
                         sum_weights_square_log_lambda_minus_mean2)
@@ -559,7 +571,17 @@ def compute_dmat_forest_pairs_fast(log_lambda1, log_lambda2, r_comov1, r_comov2,
         # first eta, first term: kronecker delta
         # second eta, first term: kronecker delta
         dmat_bin = model_bins + num_model_bins_r_par * num_model_bins_r_trans * bins
-        dmat[dmat_bin] += weights12
+        # default cf.alpha = args.z_evol = 2.9
+        # cf.alpha2 = cf.alpha for auto-correlation
+        # and cf.alpha2 = args.z_evol2 = 2.9 for cross-correlation with other deltas
+        # this scale factor was applied to all of the eta terms above
+        # but not yet to the first term
+        if redshift_evolution_in_distortion_matrix :
+            zfac = (((1+z1[i])/(1+z_ref))**(alpha-1))*(((1+z2[j])/(1+z_ref))**(alpha2-1))
+        else :
+            zfac = 1
+        dmat[dmat_bin] += weights12 * zfac
+
         # rest of the terms
         for k in unique_bins_model:
             dmat_bin = k + num_model_bins_r_par * num_model_bins_r_trans * bins
diff --git a/py/picca/constants.py b/py/picca/constants.py
index 1dd00378d..cdbe5ef0d 100644
--- a/py/picca/constants.py
+++ b/py/picca/constants.py
@@ -160,7 +160,7 @@ def get_dist_v(self, z):
         """
         raise NotImplementedError("Function should be specified at run-time")
 
-    def __init__(self,Om,Ok=0.,Or=0.,wl=-1.,blinding=False):
+    def __init__(self,Om,Ok=0.,Or=0.,wl=-1.,blinding=False,verbose=True):
         """Initializes the methods for this instance
 
         Args:
@@ -184,12 +184,14 @@ def __init__(self,Om,Ok=0.,Or=0.,wl=-1.,blinding=False):
 
         # Blind data
         if blinding == "none":
-            userprint("ATTENTION: Analysis is not blinded!")
+            if verbose:
+                userprint("ATTENTION: Analysis is not blinded!")
         else:
             userprint(f"ATTENTION: Analysis is blinded with strategy {blinding}")
 
         if blinding not in  ["strategyB", "strategyBC"]:
-            userprint(f"Om={Om}, Or={Or}, wl={wl}")
+            if verbose:
+                userprint(f"Om={Om}, Or={Or}, wl={wl}")
         else:
             userprint("The specified cosmology is "
                       f"not used: Om={Om}, Or={Or}, wl={wl}")
diff --git a/py/picca/delta_extraction/astronomical_objects/desi_pk1d_forest.py b/py/picca/delta_extraction/astronomical_objects/desi_pk1d_forest.py
index b699e0187..64dd3f39e 100644
--- a/py/picca/delta_extraction/astronomical_objects/desi_pk1d_forest.py
+++ b/py/picca/delta_extraction/astronomical_objects/desi_pk1d_forest.py
@@ -145,8 +145,8 @@ def coadd(self, other):
     def get_data(self):
         """Get the data to be saved in a fits file.
 
-        Extends the get_data method of Forest to also include data for
-        ivar and exposures_diff.
+        Extends the get_data method of Pk1dForest to also include data for
+        the resolution matrix.
 
         Return
         ------
diff --git a/py/picca/delta_extraction/astronomical_objects/forest.py b/py/picca/delta_extraction/astronomical_objects/forest.py
index c09fb81fb..fab6c42e3 100644
--- a/py/picca/delta_extraction/astronomical_objects/forest.py
+++ b/py/picca/delta_extraction/astronomical_objects/forest.py
@@ -537,6 +537,15 @@ def get_data(self):
                      "spectra for units"]
         units += [Forest.flux_units]
 
+        if self.ivar is None:
+            cols += [np.zeros(array_size, dtype=float)]
+        else:
+            cols += [self.ivar]
+        names += ["IVAR"]
+        comments += ["Inverse variance. Check input "
+                     "spectra for units"]
+        units += [f"({Forest.flux_units})^-1"]
+
         return cols, names, units, comments
 
     def get_header(self):
diff --git a/py/picca/delta_extraction/astronomical_objects/pk1d_forest.py b/py/picca/delta_extraction/astronomical_objects/pk1d_forest.py
index 4eb80d5a9..fd826b8c9 100644
--- a/py/picca/delta_extraction/astronomical_objects/pk1d_forest.py
+++ b/py/picca/delta_extraction/astronomical_objects/pk1d_forest.py
@@ -154,7 +154,7 @@ def get_data(self):
         """Get the data to be saved in a fits file.
 
         Extends the get_data method of Forest to also include data for
-        ivar and exposures_diff.
+        exposures_diff and resolution estimates.
 
         Return
         ------
@@ -172,15 +172,14 @@ def get_data(self):
         """
         cols, names, units, comments = super().get_data()
 
-        cols += [self.ivar, self.exposures_diff, self.reso, self.reso_pix]
-        names += ["IVAR", "DIFF", "RESO", "RESO_PIX"]
+        cols += [self.exposures_diff, self.reso, self.reso_pix]
+        names += ["DIFF", "RESO", "RESO_PIX"]
         comments += [
-            "Inverse variance. Check input spectra for units",
             "Difference. Check input spectra for units",
             "Resolution estimate (FWHM) for each pixel in units of km/s"
             "Resolution estimate (sigma) for each pixel in units of pixel size"
         ]
-        units += [f"({Forest.flux_units})^-1", Forest.flux_units, "", ""]
+        units += [Forest.flux_units, "", ""]
 
         return cols, names, units, comments
 
diff --git a/py/picca/delta_extraction/config.py b/py/picca/delta_extraction/config.py
index 9ab9a8fa1..a1391a3f2 100644
--- a/py/picca/delta_extraction/config.py
+++ b/py/picca/delta_extraction/config.py
@@ -11,6 +11,7 @@
 import git
 from git.exc import InvalidGitRepositoryError
 
+from picca._version import __version__ as picca_version
 from picca.delta_extraction.correction import Correction
 from picca.delta_extraction.data import Data
 from picca.delta_extraction.errors import ConfigError
@@ -24,7 +25,6 @@
     git_hash = git.Repo(PICCA_BASE).head.object.hexsha
 except InvalidGitRepositoryError:  # pragma: no cover
     git_hash = metadata.metadata('picca')['Summary'].split(':')[-1]
-picca_version = metadata.metadata('picca')['Version']
 
 accepted_corrections_options = [
     "num corrections", "type {int}", "module name {int}"
diff --git a/py/picca/delta_extraction/corrections/calibration_correction.py b/py/picca/delta_extraction/corrections/calibration_correction.py
index 1d55cb26d..6fdda4994 100644
--- a/py/picca/delta_extraction/corrections/calibration_correction.py
+++ b/py/picca/delta_extraction/corrections/calibration_correction.py
@@ -46,6 +46,8 @@ def __init__(self, config):
             raise CorrectionError(
                 "Missing argument 'filename' required by SdssCalibrationCorrection"
             )
+        stack_log_lambda = None
+        stack_lambda = None
         try:
             hdu = fitsio.FITS(filename)["STACK_DELTAS"]
             if "LOGLAM" in [name.upper() for name in hdu.get_colnames()]:
diff --git a/py/picca/delta_extraction/data.py b/py/picca/delta_extraction/data.py
index c20854fd8..d8ac92023 100644
--- a/py/picca/delta_extraction/data.py
+++ b/py/picca/delta_extraction/data.py
@@ -37,6 +37,7 @@
     "rejection log file",
     "save format",
     "num processors",
+    "delta extraction single exposure",
 ]
 
 defaults = {
@@ -51,6 +52,7 @@
     "save format": "ImageHDU",
     "minimal snr pk1d": 1,
     "minimal snr bao3d": 0,
+    "delta extraction single exposure" : "None",
 }
 
 accepted_analysis_type = ["BAO 3D", "PK 1D"]
@@ -263,6 +265,45 @@ def _save_deltas_one_healpix(out_dir, healpix, forests, save_format):
                     f" Found: {save_format}")
 
 
+def _coadd_exposures(forests):
+    """
+    Takes a list of forest objects and coadds them together.
+
+    Arguments
+    ---------
+    forests: List of Forests
+    List of forests to coadd.
+
+    Returns:
+    ---------
+    coadded_forest: Forest coadded
+    """
+    coadded_forest = forests[0]
+    if len(forests) == 1:
+        coadded_forest.rebin()
+    else:
+        for forest_coadd in forests[1:]:
+            coadded_forest.coadd(forest_coadd)
+    return coadded_forest
+
+
+def _rebin_forest(forest):
+    """
+    Rebin a Forest to match coadd gridding.
+
+    Arguments
+    ---------
+    forest: Forest object
+    Forest to rebin.
+
+    Returns:
+    ---------
+    None
+    """
+    forest.rebin()
+    return forest
+
+
 class Data:
     """Abstract class from which all classes loading data must inherit.
     Classes that inherit from this should be initialized using
@@ -490,6 +531,7 @@ def __parse_config(self, config):
                 "Missing argument 'minimal snr bao3d' (if 'analysis type' = "
                 "'BAO 3D') or ' minimal snr pk1d' (if 'analysis type' = 'Pk1d') "
                 "required by Data")
+        self.delta_extraction_single_exposure = config.get("delta extraction single exposure")
 
     def filter_bad_cont_forests(self):
         """Remove forests where continuum could not be computed"""
@@ -573,6 +615,11 @@ def find_nside(self):
 
     def save_deltas(self):
         """Save the deltas."""
+
+        if isinstance(self.forests[0].los_id, str):
+            for forest in self.forests:
+                forest.los_id = np.int64(forest.los_id.split("_")[0])
+
         healpixs = np.array([forest.healpix for forest in self.forests])
         unique_healpixs = np.unique(healpixs)
 
@@ -600,3 +647,66 @@ def save_deltas(self):
             self.rejection_log.add_to_rejection_log(forest, "accepted")
 
         self.rejection_log.save_rejection_log()
+
+
+    def select_forests_expected_flux_estimation(self):
+        """In case we are using non-coadded forests,
+        chose the way continuum fitting is computed.
+        If individual exposures are present and no option is chosen,
+        return an error."""
+        if self.delta_extraction_single_exposure == "None":
+            return self.forests
+        if self.delta_extraction_single_exposure.startswith("indiv"):
+            self.rename_exposures()
+            return self.forests
+        if self.delta_extraction_single_exposure.startswith("coadd"):
+            return self.return_coadded_forests()
+        raise ValueError("""Wrong method chosen for fitting the continuum of """
+                             """individual exposure, please chose between indiv or coadd """
+                             """for the "delta extraction single exposure" variable""")
+
+
+    def rename_exposures(self):
+        """In case there are not coadded forest,
+        rename them to make independent the delta extraction"""
+        los_id_list = np.array([forest.los_id for forest in self.forests])
+        unique_los_id_list = np.unique(los_id_list)
+        new_forest_id_list = np.zeros(los_id_list.shape).astype(str)
+        for los_id in unique_los_id_list:
+            mask_los_id = los_id_list == los_id
+            new_forest_id = [f"{los_id}_{i}" for i in range(len(los_id_list[mask_los_id]))]
+            new_forest_id_list[mask_los_id] = new_forest_id
+        for i, forest in enumerate(self.forests):
+            forest.los_id = new_forest_id_list[i]
+
+
+    def return_coadded_forests(self):
+        """In case the forest are not coadded,
+        return the coadd."""
+        los_id_list = np.array([forest.los_id for forest in self.forests])
+        unique_los_id_list = np.unique(los_id_list)
+
+        # Coadd all the exposures of the same quasar for continuum fitting
+        all_forests = np.array(self.forests)
+        forest_list_to_coadd = []
+        for los_id in unique_los_id_list:
+            mask_los_id = los_id_list == los_id
+            forest_list_to_coadd.append(all_forests[mask_los_id])
+
+        if self.num_processors > 1:
+            with multiprocessing.Pool(processes=self.num_processors) as pool:
+                coadded_forests = pool.map(_coadd_exposures,forest_list_to_coadd)
+        else:
+            coadded_forests = []
+            for forest_list in forest_list_to_coadd:
+                coadded_forests.append(_coadd_exposures(forest_list))
+
+        # Rebin all individual forests to have the same wavelength gridding than coadd
+        if self.num_processors > 1:
+            with multiprocessing.Pool(processes=self.num_processors) as pool:
+                forests_rebinned = pool.map(_rebin_forest,self.forests)
+            self.forests = forests_rebinned
+        else:
+            for forest in self.forests:
+                _rebin_forest(forest)
+        return coadded_forests
diff --git a/py/picca/delta_extraction/data_catalogues/desi_data.py b/py/picca/delta_extraction/data_catalogues/desi_data.py
index e7f891380..37f5a84e8 100644
--- a/py/picca/delta_extraction/data_catalogues/desi_data.py
+++ b/py/picca/delta_extraction/data_catalogues/desi_data.py
@@ -15,25 +15,64 @@
 from picca.delta_extraction.quasar_catalogues.desi_quasar_catalogue import (
     defaults as defaults_quasar_catalogue)
 from picca.delta_extraction.utils import (
-    ACCEPTED_BLINDING_STRATEGIES, UNBLINDABLE_STRATEGIES)
+    ACCEPTED_BLINDING_STRATEGIES)
 from picca.delta_extraction.utils_pk1d import spectral_resolution_desi, exp_diff_desi
 from picca.delta_extraction.utils import (
     ABSORBER_IGM, update_accepted_options, update_default_options)
 
 accepted_options = update_accepted_options(accepted_options, accepted_options_quasar_catalogue)
-accepted_options = update_accepted_options(
-    accepted_options,
-    ["unblind", "use non-coadded spectra", "wave solution"])
+accepted_options = update_accepted_options(accepted_options,
+    ["use non-coadded spectra",
+     "uniquify night targetid",
+     "keep single exposures", 
+     "wave solution"])
 
 defaults = update_default_options(defaults, {
     "delta lambda": 0.8,
     "delta log lambda": 3e-4,
-    "unblind": False,
     "use non-coadded spectra": False,
+    "uniquify night targetid": False,
+    "keep single exposures": False,
     "wave solution": "lin",
 })
 defaults = update_default_options(defaults, defaults_quasar_catalogue)
 
+def verify_exposures_shape(forests_by_targetid):
+    """Verify that the exposures have the same shape. 
+    If not, it removes them from the dictionnary of forests by targetid.
+    Only works for use_non_coadded_spectra and keep_single_exposures options.
+
+    Arguments
+    ---------
+    forests_by_targetid: dict
+    Dictionary were forests are stored. Its content is modified by this
+    function.
+
+    """
+    full_los_ids = np.array(list(forests_by_targetid.keys()))
+    if isinstance(full_los_ids[0],int):
+        raise ValueError(
+            "The key of this dictionnary should be str"
+            "in order to verify if exposures have "
+            "different shapes"
+        )
+    los_ids = [int(key.split("_")[1]) for key in full_los_ids]
+    unique_los_ids = np.unique(los_ids)
+    full_los_ids_to_remove = []
+    for los_id in unique_los_ids:
+        select_forest = full_los_ids[los_ids == los_id]
+        size_forests = np.unique(
+            [forests_by_targetid[key].flux.size for key in select_forest]
+        )
+        if len(size_forests) > 1:
+            full_los_ids_to_remove.append(select_forest)
+
+    if len(full_los_ids_to_remove) != 0:
+        full_los_ids_to_remove = np.concatenate(full_los_ids_to_remove, axis=0)
+        for los_id in full_los_ids_to_remove:
+            forests_by_targetid.pop(los_id)
+    return forests_by_targetid
+
 
 def merge_new_forest(forests_by_targetid, new_forests_by_targetid):
     """Merge new_forests_by_targetid and forests_by_targetid as Forest instances.
@@ -88,8 +127,15 @@ class DesiData(Data):
     Logger object
 
     use_non_coadded_spectra: bool
-    If True, load data from non-coadded spectra and coadd them here. Otherwise,
+    If True, load data from non-coadded spectra. Otherwise,
     load coadded data
+
+    uniquify_night_targetid: bool
+    If True, remove the quasars taken on the same night.
+
+    keep_single_exposures: bool
+    If True, the date loadded from non-coadded spectra are not coadded. 
+    Otherwise, coadd the spectra here.
     """
 
     def __init__(self, config):
@@ -106,8 +152,6 @@ def __init__(self, config):
         super().__init__(config)
 
         # load variables from config
-        self.unblind = None
-        self.use_non_coadded_spectra = None
         self.__parse_config(config)
 
         # load z_truth catalogue
@@ -141,9 +185,12 @@ def __parse_config(self, config):
         DataError upon missing required variables
         """
         # instance variables
-        self.unblind = config.getboolean("unblind")
-        if self.unblind is None:
-            raise DataError("Missing argument 'unblind' required by DesiData")
+        self.keep_single_exposures = config.getboolean(
+            "keep single exposures")
+        if self.keep_single_exposures is None:
+            raise DataError(
+                "Missing argument 'keep single exposures' required by DesiData"
+            )
 
         self.use_non_coadded_spectra = config.getboolean(
             "use non-coadded spectra")
@@ -152,6 +199,15 @@ def __parse_config(self, config):
                 "Missing argument 'use non-coadded spectra' required by DesiData"
             )
 
+        self.uniquify_night_targetid = config.getboolean(
+            "uniquify night targetid")
+        if self.uniquify_night_targetid is None:
+            raise DataError(
+                "Missing argument 'uniquify night targetid' required by DesiData"
+            )
+
+
+
     def read_data(self):
         """Read the spectra and formats its data as Forest instances.
 
@@ -197,14 +253,6 @@ def set_blinding(self, is_mock):
             else:
                 self.blinding = "desi_y3"
 
-            if self.unblind:
-                if self.blinding not in UNBLINDABLE_STRATEGIES:
-                    raise DataError(
-                        "In DesiData: Requested unblinding but data requires blinding strategy "
-                        f"{self.blinding} and this strategy do not support "
-                        "unblinding. If you believe this is an error, contact "
-                        "picca developers")
-
         if self.blinding not in ACCEPTED_BLINDING_STRATEGIES:
             raise DataError(
                 "Unrecognized blinding strategy. Accepted strategies "
@@ -240,11 +288,23 @@ class DesiDataFileHandler():
     Logger object
 
     use_non_coadded_spectra: bool
-    If True, load data from non-coadded spectra and coadd them here. Otherwise,
+    If True, load data from non-coadded spectra. Otherwise,
     load coadded data
+
+    uniquify_night_targetid: bool
+    If True, remove the quasars taken on the same night.
+
+    keep_single_exposures: bool
+    If True, the date loadded from non-coadded spectra are not coadded. 
+    Otherwise, coadd the spectra here.
     """
 
-    def __init__(self, analysis_type, use_non_coadded_spectra, logger):
+    def __init__(self,
+                 analysis_type,
+                 use_non_coadded_spectra,
+                 uniquify_night_targetid,
+                 keep_single_exposures,
+                 logger):
         """Initialize file handler
 
         Arguments
@@ -253,8 +313,15 @@ def __init__(self, analysis_type, use_non_coadded_spectra, logger):
         Selected analysis type. See class Data from py/picca/delta_extraction/data.py
         for details
 
+        keep_single_exposures: bool
+        If True, the date loadded from non-coadded spectra are not coadded. 
+        Otherwise, coadd the spectra here.
+
+        uniquify_night_targetid: bool
+        If True, remove the quasars taken on the same night.
+
         use_non_coadded_spectra: bool
-        If True, load data from non-coadded spectra and coadd them here. Otherwise,
+        If True, load data from non-coadded spectra. Otherwise,
         load coadded data
 
         logger: logging.Logger
@@ -265,6 +332,8 @@ def __init__(self, analysis_type, use_non_coadded_spectra, logger):
         # self.logger = logging.getLogger(__name__)
         self.logger = logger
         self.analysis_type = analysis_type
+        self.keep_single_exposures = keep_single_exposures
+        self.uniquify_night_targetid = uniquify_night_targetid
         self.use_non_coadded_spectra = use_non_coadded_spectra
 
     def __call__(self, args):
@@ -313,7 +382,6 @@ def format_data(self,
         """
         num_data = 0
         forests_by_targetid = {}
-
         # Loop over quasars in catalogue fragment
         for row in catalogue:
             # Find which row in tile contains this quasar
@@ -325,78 +393,168 @@ def format_data(self,
                     f"Error reading {targetid}. Ignoring object")
                 continue
             if len(w_t) > 1:
-                self.logger.warning(
-                    "Warning: more than one spectrum in this file "
-                    f"for {targetid}")
+                if not self.use_non_coadded_spectra:
+                    self.logger.warning(
+                        "Warning: more than one spectrum in this file "
+                        f"for {targetid}")
             else:
                 w_t = w_t[0]
             # Construct DesiForest instance
             # Fluxes from the different spectrographs will be coadded
             for spec in spectrographs_data.values():
-                if self.use_non_coadded_spectra:
+                if self.use_non_coadded_spectra and not self.keep_single_exposures:
                     ivar = np.atleast_2d(spec['IVAR'][w_t])
                     ivar_coadded_flux = np.atleast_2d(
-                        ivar * spec['FLUX'][w_t]).sum(axis=0)
+                            ivar * spec['FLUX'][w_t]).sum(axis=0)
                     ivar = ivar.sum(axis=0)
                     flux = ivar_coadded_flux / ivar
                 else:
                     flux = spec['FLUX'][w_t].copy()
                     ivar = spec['IVAR'][w_t].copy()
 
-                args = {
-                    "flux": flux,
-                    "ivar": ivar,
-                    "targetid": targetid,
-                    "ra": row['RA'],
-                    "dec": row['DEC'],
-                    "z": row['Z'],
-                }
-                args["log_lambda"] = np.log10(spec['WAVELENGTH'])
-
-                if self.analysis_type == "BAO 3D":
-                    forest = DesiForest(**args)
-                elif self.analysis_type == "PK 1D":
-                    if self.use_non_coadded_spectra:
-                        exposures_diff = exp_diff_desi(spec, w_t)
-                        if exposures_diff is None:
-                            continue
-                    else:
-                        exposures_diff = np.zeros(spec['WAVELENGTH'].shape)
-                    if reso_from_truth:
-                        reso_sum = spec['RESO'][:, :]
-                    else:
-                        if len(spec['RESO'][w_t].shape) < 3:
-                            reso_sum = spec['RESO'][w_t].copy()
-                        else:
-                            reso_sum = spec['RESO'][w_t].sum(axis=0)
-                    reso_in_pix, reso_in_km_per_s = spectral_resolution_desi(
-                        reso_sum, spec['WAVELENGTH'])
-                    args["exposures_diff"] = exposures_diff
-                    args["reso"] = reso_in_km_per_s
-                    args["resolution_matrix"] = reso_sum
-                    args["reso_pix"] = reso_in_pix
-
-                    forest = DesiPk1dForest(**args)
-                # this should never be entered added here in case at some point
-                # we add another analysis type
-                else:  # pragma: no cover
-                    raise DataError("Unkown analysis type. Expected 'BAO 3D'"
-                                    f"or 'PK 1D'. Found '{self.analysis_type}'")
-
-                # rebin arrays
-                # this needs to happen after all arrays are initialized by
-                # Forest constructor
-                forest.rebin()
-
-                # keep the forest
-                if targetid in forests_by_targetid:
-                    existing_forest = forests_by_targetid[targetid]
-                    existing_forest.coadd(forest)
-                    forests_by_targetid[targetid] = existing_forest
+                # If keep_single_exposures is activated, all exposures are kept.
+                # To avoid coadding exposures, we store the forest with
+                # targetid_i (i = exposure index) instead of targetid
+                # The variable forests_by_targetid is modified by update_forest_dictionary.
+                if self.use_non_coadded_spectra & self.keep_single_exposures:
+                    # One exposure case
+                    if len(flux.shape) != 2:
+                        flux , ivar = np.array([flux]), np.array([ivar])
+                    for i, (flux_i,ivar_i) in enumerate(zip(flux,ivar)):
+                        forests_by_targetid, num_data = self.update_forest_dictionary(
+                                forests_by_targetid,
+                                f"{targetid}_{i}",
+                                targetid,
+                                spec,
+                                row,
+                                flux_i,
+                                ivar_i,
+                                w_t,
+                                reso_from_truth,
+                                num_data)
+                else:
+                    forests_by_targetid, num_data  = self.update_forest_dictionary(
+                            forests_by_targetid,
+                            targetid,
+                            targetid,
+                            spec,
+                            row,
+                            flux,
+                            ivar,
+                            w_t,
+                            reso_from_truth,
+                            num_data)
+        return forests_by_targetid, num_data
+
+    def update_forest_dictionary(self,
+                                 forests_by_targetid,
+                                 key_update,
+                                 targetid,
+                                 spec,
+                                 row,
+                                 flux,
+                                 ivar,
+                                 w_t,
+                                 reso_from_truth,
+                                 num_data):
+        """Add new forests to the current forest dictonary
+
+        Arguments
+        ---------
+        forests_by_targetid: dict
+        Dictionary were forests are stored.
+
+        key_update: int or str
+        The key to update in the forest dictionary
+
+        targetid: int
+        The los_id of the forest
+
+        spec: dict
+        Dictionary containing the spectrum information.
+
+        row: dict
+        Metadata of the spectrum to treat.
+
+        flux: numpy.array
+        Flux of the spectrum to add.
+
+        ivar: numpy.array
+        Inverse variance of the spectrum to add.
+
+        w_t: numpy.array
+        The index of spectrum with the same targetid.
+
+        reso_from_truth: bool - Default: False
+        Specifies whether resolution matrixes are read from truth files (True)
+        or directly from data (False)
+
+        num_data: int
+        The number of instances loaded
+        
+        Return
+        ------
+        forests_by_targetid: dict
+        Dictionary were forests are stored.
+
+        num_data: int
+        The number of instances loaded
+        """
+        args = {
+            "flux": flux,
+            "ivar": ivar,
+            "targetid": targetid,
+            "ra": row['RA'],
+            "dec": row['DEC'],
+            "z": row['Z'],
+        }
+        args["log_lambda"] = np.log10(spec['WAVELENGTH'])
+
+
+        if self.analysis_type == "BAO 3D":
+            forest = DesiForest(**args)
+        elif self.analysis_type == "PK 1D":
+            if (self.use_non_coadded_spectra) & (not self.keep_single_exposures):
+                exposures_diff = exp_diff_desi(spec, w_t)
+                if exposures_diff is None:
+                    return forests_by_targetid, num_data
+            else:
+                exposures_diff = np.zeros(spec['WAVELENGTH'].shape)
+            if reso_from_truth:
+                reso_sum = spec['RESO'][:, :]
+            else:
+                if len(spec['RESO'][w_t].shape) < 3:
+                    reso_sum = spec['RESO'][w_t].copy()
                 else:
-                    forests_by_targetid[targetid] = forest
+                    reso_sum = spec['RESO'][w_t].sum(axis=0)
+            reso_in_pix, reso_in_km_per_s = spectral_resolution_desi(
+                reso_sum, spec['WAVELENGTH'])
+            args["exposures_diff"] = exposures_diff
+            args["reso"] = reso_in_km_per_s
+            args["resolution_matrix"] = reso_sum
+            args["reso_pix"] = reso_in_pix
+
+            forest = DesiPk1dForest(**args)
+        # this should never be entered added here in case at some point
+        # we add another analysis type
+        else:  # pragma: no cover
+            raise DataError("Unkown analysis type. Expected 'BAO 3D'"
+                            f"or 'PK 1D'. Found '{self.analysis_type}'")
+
+        # rebin arrays
+        # this needs to happen after all arrays are initialized by
+        # Forest constructor
+        forest.rebin()
+
+        if key_update in forests_by_targetid:
+            existing_forest = forests_by_targetid[key_update]
+            existing_forest.coadd(forest)
+            forests_by_targetid[key_update] = existing_forest
+        else:
+            forests_by_targetid[key_update] = forest
+
+        num_data += 1
 
-                num_data += 1
         return forests_by_targetid, num_data
 
     def read_file(self, filename, catalogue):
diff --git a/py/picca/delta_extraction/data_catalogues/desi_healpix.py b/py/picca/delta_extraction/data_catalogues/desi_healpix.py
index 78b9cd805..250d06705 100644
--- a/py/picca/delta_extraction/data_catalogues/desi_healpix.py
+++ b/py/picca/delta_extraction/data_catalogues/desi_healpix.py
@@ -1,17 +1,20 @@
 """This module defines the class DesiData to load DESI data
 """
 import logging
-import os
 import multiprocessing
+import os
 
 import fitsio
 import numpy as np
-
 from picca.delta_extraction.astronomical_objects.desi_pk1d_forest import DesiPk1dForest
-from picca.delta_extraction.data_catalogues.desi_data import (
-    DesiData, DesiDataFileHandler, merge_new_forest)
 from picca.delta_extraction.data_catalogues.desi_data import (  # pylint: disable=unused-import
-    defaults, accepted_options)
+    DesiData,
+    DesiDataFileHandler,
+    accepted_options,
+    defaults,
+    merge_new_forest,
+    verify_exposures_shape,
+)
 from picca.delta_extraction.errors import DataError
 
 
@@ -112,15 +115,22 @@ def read_data(self):
             arguments.append((filename, group))
 
         self.logger.info(f"reading data from {len(arguments)} files")
-
         if self.num_processors > 1:
             context = multiprocessing.get_context('fork')
             with context.Pool(processes=self.num_processors) as pool:
                 imap_it = pool.imap(
                     DesiHealpixFileHandler(self.analysis_type,
                                            self.use_non_coadded_spectra,
+                                           self.uniquify_night_targetid,
+                                           self.keep_single_exposures,
                                            self.logger), arguments)
-                for forests_by_targetid_aux, _ in imap_it:
+                for index, output_imap in enumerate(imap_it):
+                    forests_by_targetid_aux = output_imap[0]
+                    if self.use_non_coadded_spectra & self.keep_single_exposures:
+                        # Change the dictionary key to prevent coadding.
+                        # exposures on different files.
+                        forests_by_targetid_aux= {f"{index}_{key}": items
+                                                  for key, items in forests_by_targetid_aux.items()}
                     # Merge each dict to master forests_by_targetid
                     merge_new_forest(forests_by_targetid,
                                      forests_by_targetid_aux)
@@ -128,14 +138,23 @@ def read_data(self):
         else:
             reader = DesiHealpixFileHandler(self.analysis_type,
                                             self.use_non_coadded_spectra,
+                                            self.uniquify_night_targetid,
+                                            self.keep_single_exposures,
                                             self.logger)
             num_data = 0
             for index, this_arg in enumerate(arguments):
                 forests_by_targetid_aux, num_data_aux = reader(this_arg)
+                if self.use_non_coadded_spectra & self.keep_single_exposures:
+                    # Change the dictionary key to prevent coadding
+                    # exposures on different files.
+                    forests_by_targetid_aux= {f"{index}_{key}": items
+                                              for key, items in forests_by_targetid_aux.items()}
                 merge_new_forest(forests_by_targetid, forests_by_targetid_aux)
                 num_data += num_data_aux
                 self.logger.progress(f"Read {index} of {len(arguments)}. "
                                      f"num_data: {num_data}")
+        if self.use_non_coadded_spectra & self.keep_single_exposures:
+            forests_by_targetid = verify_exposures_shape(forests_by_targetid)
 
         if len(forests_by_targetid) == 0:
             raise DataError("No quasars found, stopping here")
@@ -189,6 +208,14 @@ def read_file(self, filename, catalogue):
             return {}, 0
         # Read targetid from fibermap to match to catalogue later
         fibermap = hdul['FIBERMAP'].read()
+
+        index_unique = np.full(fibermap.shape,True)
+        if self.uniquify_night_targetid:
+            if "NIGHT" in fibermap.dtype.names:
+                _, index_unique = np.unique(
+                    np.vstack([fibermap["TARGETID"],fibermap["NIGHT"]]),axis=1,return_index=True
+                    )
+
         # First read all wavelength, flux, ivar, mask, and resolution
         # from this file
         spectrographs_data = {}
@@ -213,9 +240,9 @@ def read_file(self, filename, catalogue):
                 hdul_truth = fitsio.FITS(filename_truth)
                 reso_from_truth = True
 
-        def _read_resolution(color):
+        def _read_resolution(color, indices):
             if f"{color}_RESOLUTION" in hdul:
-                return hdul[f"{color}_RESOLUTION"].read()
+                return hdul[f"{color}_RESOLUTION"].read()[indices]
             if hdul_truth is not None:
                 return hdul_truth[f"{color}_RESOLUTION"].read()
 
@@ -229,15 +256,15 @@ def _read_resolution(color):
             spec = {}
             try:
                 spec["WAVELENGTH"] = hdul[f"{color}_WAVELENGTH"].read()
-                spec["FLUX"] = hdul[f"{color}_FLUX"].read()
+                spec["FLUX"] = hdul[f"{color}_FLUX"].read()[index_unique]
                 spec["IVAR"] = (hdul[f"{color}_IVAR"].read() *
-                                (hdul[f"{color}_MASK"].read() == 0))
+                                (hdul[f"{color}_MASK"].read() == 0))[index_unique]
                 w = np.isnan(spec["FLUX"]) | np.isnan(spec["IVAR"])
                 for key in ["FLUX", "IVAR"]:
                     spec[key][w] = 0.
 
                 if self.analysis_type == "PK 1D":
-                    spec["RESO"] = _read_resolution(color)
+                    spec["RESO"] = _read_resolution(color,index_unique)
 
                 spectrographs_data[color] = spec
             except OSError:
@@ -251,7 +278,7 @@ def _read_resolution(color):
         forests_by_targetid, num_data = self.format_data(
             catalogue,
             spectrographs_data,
-            fibermap["TARGETID"],
+            fibermap["TARGETID"][index_unique],
             reso_from_truth=reso_from_truth)
 
         return forests_by_targetid, num_data
diff --git a/py/picca/delta_extraction/data_catalogues/desi_healpix_fast.py b/py/picca/delta_extraction/data_catalogues/desi_healpix_fast.py
new file mode 100644
index 000000000..0e75d2a38
--- /dev/null
+++ b/py/picca/delta_extraction/data_catalogues/desi_healpix_fast.py
@@ -0,0 +1,266 @@
+"""This module defines the class DesiData to load DESI data
+"""
+import logging
+import multiprocessing
+import time
+import itertools
+
+import fitsio
+import numpy as np
+
+from picca.delta_extraction.astronomical_objects.desi_forest import DesiForest
+from picca.delta_extraction.data_catalogues.desi_data import (  # pylint: disable=unused-import
+    DesiData,
+    DesiDataFileHandler,
+    accepted_options,
+    defaults,
+    merge_new_forest,
+    verify_exposures_shape,
+)
+from picca.delta_extraction.errors import DataError
+
+
+class DesiHealpixFast(DesiData):
+    """Reads the spectra from DESI using healpix mode and formats its data as a
+    list of Forest instances.
+
+    Methods
+    -------
+    (see DesiData in py/picca/delta_extraction/data_catalogues/desi_data.py)
+    __init__
+    __parse_config
+    get_filename
+    read_data
+    read_file
+
+    Attributes
+    ----------
+    (see DesiData in py/picca/delta_extraction/data_catalogues/desi_data.py)
+
+    logger: logging.Logger
+    Logger object
+
+    num_processors: int
+    Number of processors to be used for parallel reading
+    """
+
+    def __init__(self, config):
+        """Initialize class instance
+
+        Arguments
+        ---------
+        config: configparser.SectionProxy
+        Parsed options to initialize class
+        """
+        self.logger = logging.getLogger(__name__)
+
+        super().__init__(config)
+
+        #TODO: remove exception when this is implemented
+        if self.analysis_type == "PK 1D":
+            raise NotImplementedError("fast healpix reading is not implemented for PK 1D analyses")
+
+    def get_filename(self, survey, healpix, coadd_name):
+        """Get the name of the file to read
+
+        Arguments
+        ---------
+        survey: str
+        Name of the survey (sv, sv1, sv2, sv3, main, special)
+
+        healpix: int
+        Healpix of observations
+
+        Return
+        ------
+        filename: str
+        The name of the file to read
+
+        is_mock: bool
+        False, as we are reading DESI data
+        """
+        filename = (
+            f"{self.input_directory}/{survey}/dark/{healpix//100}/{healpix}/{coadd_name}-{survey}-"
+            f"dark-{healpix}.fits")
+        # TODO: not sure if we want the dark survey to be hard coded
+        # in here, probably won't run on anything else, but still
+        return filename
+
+    def read_data(self, is_mock=False):
+        """Read the data.
+
+        Method used to read healpix-based survey data.
+
+        Return
+        ------
+        is_mock: bool
+        False for DESI data and True for mocks
+
+        Raise
+        -----
+        DataError if no quasars were found
+        """
+        coadd_name = "spectra" if self.use_non_coadded_spectra else "coadd"
+
+        grouped_catalogue = self.catalogue.group_by(["HEALPIX", "SURVEY"])
+
+        arguments = [
+            (self.get_filename(group["SURVEY"][0], group["HEALPIX"][0], coadd_name), group)
+            for group in grouped_catalogue.groups
+        ]
+
+        self.logger.info(f"reading data from {len(arguments)} files")
+        if self.num_processors > 1:
+            context = multiprocessing.get_context('fork')
+            with context.Pool(processes=self.num_processors) as pool:
+                imap_it = pool.imap(
+                    DesiHealpixFileHandler(self.logger), arguments)
+                t0 = time.time()
+                self.logger.progress("Merging threads")
+
+
+                self.forests = combine_results(imap_it)
+                t1 = time.time()
+                self.logger.progress(f"Time spent meerging threads: {t1-t0}")
+
+        if len(self.forests) == 0:
+            raise DataError("No quasars found, stopping here")
+
+        return is_mock
+
+
+# Class to read in parallel
+# Seems lightweight to copy all these 3 arguments
+class DesiHealpixFileHandler():
+    """File handler for class DesiHealpix
+
+    Methods
+    -------
+    (see DesiDataFileHandler in py/picca/delta_extraction/data_catalogues/desi_data.py)
+    read_file
+
+    Attributes
+    ----------
+    (see DesiDataFileHandler in py/picca/delta_extraction/data_catalogues/desi_data.py)
+    """
+    def __init__(self, logger):
+        """Initialize file handler
+
+        Arguments
+        ---------
+        logger: logging.Logger
+        Logger object from parent class. Trying to initialize it here
+        without copying failed data_tests.py
+        """
+        # The next line gives failed tests
+        # self.logger = logging.getLogger(__name__)
+        self.logger = logger
+
+    def __call__(self, args):
+        """Call method read_file. Note imap can be called with
+        only one argument, hence tuple as argument.
+
+        Arguments
+        ---------
+        args: tuple
+        Arguments to be passed to read_file. Should contain a string with the
+        filename and a astropy.table.Table with the quasar catalogue
+        """
+        return self.read_file(*args)
+
+    def read_file(self, filename, catalogue):
+        """Read the spectra and formats its data as Forest instances.
+
+        Arguments
+        ---------
+        filename: str
+        Name of the file to read
+
+        catalogue: astropy.table.Table
+        The quasar catalogue fragment associated with this file
+
+        Returns:
+        ---------
+        forests_by_targetid: dict
+        Dictionary were forests are stored.
+
+        num_data: int
+        The number of instances loaded
+
+        Raise
+        -----
+        DataError if the analysis type is PK 1D and resolution data is not present
+        """
+        try:
+            hdul = fitsio.FITS(filename)
+        except IOError:
+            self.logger.warning(f"Error reading '{filename}'. Ignoring file")
+            return {}, 0
+        # Read targetid from fibermap to match to catalogue later
+        fibermap = hdul['FIBERMAP'].read()
+
+        colors = ["B", "R"]
+        if "Z_FLUX" in hdul:
+            colors.append("Z")
+
+        # read wavelength
+        wave = np.hstack([hdul[f"{color}_WAVELENGTH"].read() for color in colors])
+        log_lambda = np.log10(wave)
+        # read flux and ivar
+        flux_colors = {
+            color: hdul[f"{color}_FLUX"].read() for color in colors
+        }
+        ivar_colors = {
+            color: (hdul[f"{color}_IVAR"].read() * (hdul[f"{color}_MASK"].read() == 0))
+            for color in colors
+        }
+
+        # Loop over quasars in catalogue fragment
+        forests = []
+        for row in catalogue:
+            # Find which row in tile contains this quasar
+            # It should be there by construction
+            targetid = row["TARGETID"]
+            w_t = np.where(fibermap["TARGETID"] == targetid)[0]
+            if len(w_t) == 0:
+                self.logger.warning(
+                    f"Error reading {targetid}. Ignoring object")
+                continue
+
+            w_t = w_t[0]
+            # Construct DesiForest instance
+            # Fluxes from the different spectrographs will be coadded
+            flux = np.hstack([item[w_t] for item in flux_colors.values()])
+            ivar = np.hstack([item[w_t] for item in ivar_colors.values()])
+
+            args = {
+                "flux": flux,
+                "ivar": ivar,
+                "targetid": targetid,
+                "ra": row['RA'],
+                "dec": row['DEC'],
+                "z": row['Z'],
+                "log_lambda": log_lambda,
+            }
+            forest = DesiForest(**args)
+            forest.rebin()
+            forests.append(forest)
+
+        return forests
+
+
+def combine_results(lists):
+    """Combine the content of all the lists into a single one
+
+    Arguments
+    ---------
+    lists: list of lists
+    The content to be merged
+
+    Return
+    ------
+    combined_list: list
+    The combined list
+    """
+    # Combine a list of lists into a single list
+    return list(itertools.chain(*lists))
diff --git a/py/picca/delta_extraction/data_catalogues/desi_tile.py b/py/picca/delta_extraction/data_catalogues/desi_tile.py
index 6f06df3ab..c14e8fd22 100644
--- a/py/picca/delta_extraction/data_catalogues/desi_tile.py
+++ b/py/picca/delta_extraction/data_catalogues/desi_tile.py
@@ -8,8 +8,9 @@
 import fitsio
 import numpy as np
 
+from picca.delta_extraction.astronomical_objects.desi_pk1d_forest import DesiPk1dForest
 from picca.delta_extraction.data_catalogues.desi_data import (
-    DesiData, DesiDataFileHandler, merge_new_forest)
+    DesiData, DesiDataFileHandler, merge_new_forest, verify_exposures_shape)
 from picca.delta_extraction.data_catalogues.desi_data import (  # pylint: disable=unused-import
     defaults, accepted_options)
 from picca.delta_extraction.errors import DataError
@@ -46,6 +47,10 @@ def __init__(self, config):
         self.logger = logging.getLogger(__name__)
         super().__init__(config)
 
+        if self.analysis_type == "PK 1D":
+            DesiPk1dForest.update_class_variables()
+
+
     def read_data(self):
         """Read the spectra and formats its data as Forest instances.
 
@@ -96,9 +101,17 @@ def read_data(self):
                 imap_it = pool.imap(
                     DesiTileFileHandler(self.analysis_type,
                                         self.use_non_coadded_spectra,
+                                        self.uniquify_night_targetid,
+                                        self.keep_single_exposures,
                                         self.logger, self.input_directory),
                     arguments)
-                for forests_by_targetid_aux, _ in imap_it:
+                for index, output_imap in enumerate(imap_it):
+                    forests_by_targetid_aux = output_imap[0]
+                    if self.use_non_coadded_spectra & self.keep_single_exposures:
+                        # Change the dictionary key to prevent coadding.
+                        # exposures on different files.
+                        forests_by_targetid_aux= {f"{index}_{key}": items
+                                                  for key, items in forests_by_targetid_aux.items()}
                     # Merge each dict to master forests_by_targetid
                     merge_new_forest(forests_by_targetid,
                                      forests_by_targetid_aux)
@@ -106,10 +119,17 @@ def read_data(self):
             num_data = 0
             reader = DesiTileFileHandler(self.analysis_type,
                                          self.use_non_coadded_spectra,
+                                         self.uniquify_night_targetid,
+                                         self.keep_single_exposures,
                                          self.logger, self.input_directory)
             for index, filename in enumerate(filenames):
                 forests_by_targetid_aux, num_data_aux = reader(
                     (filename, self.catalogue))
+                if self.use_non_coadded_spectra & self.keep_single_exposures:
+                    # Change the dictionary key to prevent coadding.
+                    # exposures on different files.
+                    forests_by_targetid_aux= {f"{index}_{key}": items
+                                              for key, items in forests_by_targetid_aux.items()}
                 merge_new_forest(forests_by_targetid, forests_by_targetid_aux)
                 num_data += num_data_aux
                 self.logger.progress(
@@ -117,6 +137,9 @@ def read_data(self):
 
                 self.logger.progress(f"Found {num_data} quasars in input files")
 
+        if self.use_non_coadded_spectra & self.keep_single_exposures:
+            verify_exposures_shape(forests_by_targetid)
+
         if len(forests_by_targetid) == 0:
             raise DataError("No Quasars found, stopping here")
 
@@ -140,7 +163,12 @@ class DesiTileFileHandler(DesiDataFileHandler):
     (see DesiDataFileHandler in py/picca/delta_extraction/data_catalogues/desi_data.py)
     """
 
-    def __init__(self, analysis_type, use_non_coadded_spectra, logger,
+    def __init__(self,
+                 analysis_type,
+                 use_non_coadded_spectra,
+                 uniquify_night_targetid,
+                 keep_single_exposures,
+                 logger,
                  input_directory):
         """Initialize file handler
 
@@ -151,9 +179,16 @@ def __init__(self, analysis_type, use_non_coadded_spectra, logger,
         for details
 
         use_non_coadded_spectra: bool
-        If True, load data from non-coadded spectra and coadd them here. Otherwise,
+        If True, load data from non-coadded spectra. Otherwise,
         load coadded data
 
+        uniquify_night_targetid: bool
+        If True, remove the quasars taken on the same night.
+
+        keep_single_exposures: bool
+        If True, the date loadded from non-coadded spectra are not coadded. 
+        Otherwise, coadd the spectra here.
+
         logger: logging.Logger
         Logger object
 
@@ -161,7 +196,11 @@ def __init__(self, analysis_type, use_non_coadded_spectra, logger,
         Directory where input data is stored.
         """
         self.input_directory = input_directory
-        super().__init__(analysis_type, use_non_coadded_spectra, logger)
+        super().__init__(analysis_type,
+                         use_non_coadded_spectra,
+                         uniquify_night_targetid,
+                         keep_single_exposures,
+                         logger)
 
     def read_file(self, filename, catalogue):
         """Read the spectra and formats its data as Forest instances.
diff --git a/py/picca/delta_extraction/data_catalogues/desisim_mocks.py b/py/picca/delta_extraction/data_catalogues/desisim_mocks.py
index dd61d2857..6c7ee83e3 100644
--- a/py/picca/delta_extraction/data_catalogues/desisim_mocks.py
+++ b/py/picca/delta_extraction/data_catalogues/desisim_mocks.py
@@ -49,6 +49,14 @@ def __init__(self, config):
             self.logger.warning(
                 'the "use_non_coadded_spectra" option was set, '
                 'but has no effect on Mocks, will proceed as normal')
+        if self.keep_single_exposures:
+            self.logger.warning(
+                'the "keep_single_exposures" option was set, '
+                'but has no effect on Mocks, will proceed as normal')
+        if self.uniquify_night_targetid:
+            self.logger.warning(
+                'the "uniquify_night_targetid" option was set, '
+                'but has no effect on Mocks, will proceed as normal')
 
     def get_filename(self, survey, healpix):
         """Get the name of the file to read
diff --git a/py/picca/delta_extraction/data_catalogues/desisim_mocks_fast.py b/py/picca/delta_extraction/data_catalogues/desisim_mocks_fast.py
new file mode 100644
index 000000000..b6ef81be7
--- /dev/null
+++ b/py/picca/delta_extraction/data_catalogues/desisim_mocks_fast.py
@@ -0,0 +1,87 @@
+"""This module defines the class DesiData to load DESI data
+"""
+import logging
+
+from picca.delta_extraction.data_catalogues.desi_healpix_fast import DesiHealpixFast
+from picca.delta_extraction.data_catalogues.desi_healpix_fast import (# pylint: disable=unused-import
+    defaults, accepted_options)
+
+class DesisimMocksFast(DesiHealpixFast):
+    """Reads the spectra from DESI using healpix mode and formats its data as a
+    list of Forest instances.
+
+    Should work for both data and mocks. This is specified using the 'mode'
+    keyword. It is required to set the in_nside member.
+
+    Methods
+    -------
+    (see DesiHealpix in py/picca/delta_extraction/data_catalogues/desi_healpix.py)
+    __init__
+    get_filename
+
+    Attributes
+    ----------
+    (see DesiHealpix in py/picca/delta_extraction/data_catalogues/desi_healpix.py)
+
+    in_nside: 16
+    Parameter in_nside to compute the healpix indexes
+
+    logger: logging.Logger
+    Logger object
+    """
+
+    def __init__(self, config):
+        """Initialize class instance
+
+        Arguments
+        ---------
+        config: configparser.SectionProxy
+        Parsed options to initialize class
+        """
+        self.logger = logging.getLogger(__name__)
+
+        # overwrite value for mocks
+        self.in_nside = 16
+        config["in_nside"] = str(self.in_nside)
+
+        super().__init__(config)
+
+    def get_filename(self, survey, healpix, coadd_name):
+        """Get the name of the file to read
+
+        Arguments
+        ---------
+        survey: str
+        Name of the survey (sv, sv1, sv2, sv3, main, special)
+
+        healpix: int
+        Healpix of observations
+
+        Return
+        ------
+        filename: str
+        The name of the file to read
+
+        is_mock: bool
+        True, as we are reading mocks
+        """
+        filename = (
+            f"{self.input_directory}/{healpix//100}/{healpix}/spectra-"
+            f"{self.in_nside}-{healpix}.fits")
+        return filename
+
+    def read_data(self, is_mock=True):
+        """Read the data.
+
+        Method used to read healpix-based survey data.
+
+        Return
+        ------
+        is_mock: bool
+        False for DESI data and True for mocks
+
+        Raise
+        -----
+        DataError if no quasars were found
+        """
+        return super().read_data(is_mock=is_mock)
diff --git a/py/picca/delta_extraction/data_catalogues/sdss_data.py b/py/picca/delta_extraction/data_catalogues/sdss_data.py
index 9a172ab9b..540ed4eac 100644
--- a/py/picca/delta_extraction/data_catalogues/sdss_data.py
+++ b/py/picca/delta_extraction/data_catalogues/sdss_data.py
@@ -186,7 +186,8 @@ def read_from_spec(self, catalogue):
                         "reso_pix": wdisp
                     })
             else:
-                raise DataError(f"analysis_type = {self.analysis_type}")
+                raise DataError("An unknown analysis_type was specified,"
+                                " currently PK 1D and BAO 3D are supported.")
 
             forest.rebin()
             if thingid in forests_by_thingid:
@@ -271,6 +272,9 @@ def read_from_spplate(self, catalogue):
                             "exposures_diff": exposures_diff,
                             "reso": reso
                         })
+                else:
+                    raise DataError("An unknown analysis_type was specified,"
+                                    " currently PK 1D and BAO 3D are supported.")
 
                 # rebin arrays
                 # this needs to happen after all arrays are initialized by
diff --git a/py/picca/delta_extraction/masks/dla_mask.py b/py/picca/delta_extraction/masks/dla_mask.py
index 4ec8766a5..bf132356e 100644
--- a/py/picca/delta_extraction/masks/dla_mask.py
+++ b/py/picca/delta_extraction/masks/dla_mask.py
@@ -5,7 +5,14 @@
 from astropy.table import Table
 import fitsio
 import numpy as np
-from scipy.constants import speed_of_light as SPEED_LIGHT
+from scipy.constants import (
+    speed_of_light as SPEED_LIGHT,
+    e as ELEMENTARY_CHARGE,
+    epsilon_0 as EPSILON_0,
+    m_p as PROTON_MASS,
+    m_e as ELECTRON_MASS,
+    k as BOLTZMAN_CONSTANT_K,
+)
 from scipy.special import voigt_profile
 
 from picca.delta_extraction.astronomical_objects.forest import Forest
@@ -52,11 +59,6 @@ def dla_profile(lambda_, z_abs, nhi):
     return transmission
 
 # constants to compute the optical depth of the DLA absoprtion
-ELEMENTARY_CHARGE = 1.6021e-19  # C
-EPSILON_0 = 8.8541e-12  # C^2.s^2.kg^-1.m^-3
-PROTON_MASS = 1.6726e-27  # kg
-ELECTRON_MASS = 9.109e-31  # kg
-BOLTZMAN_CONSTANT_K = 1.3806e-23  # m^2.kg.s^-2.K-1
 GAS_TEMP = 5 * 1e4  # K
 
 # precomputed factors to save time
@@ -197,6 +199,11 @@ def __init__(self, config):
                 z_colname = hdul_colnames.intersection(accepted_zcolnames)
                 if not z_colname:
                     raise ValueError(f"Z colname has to be one of {', '.join(accepted_zcolnames)}")
+                if len(z_colname)>1 :
+                    raise ValueError(
+                        "Not clear which column should be used for the DLA redshift among "
+                        f"{z_colname}. Please remove or rename one of the columns from the DLA "
+                        "fits file.")
                 z_colname = z_colname.pop()
                 columns_list = [los_id_name, z_colname, "NHI"]
                 cat = {col: hdul["DLACAT"][col][:] for col in columns_list}
diff --git a/py/picca/delta_extraction/survey.py b/py/picca/delta_extraction/survey.py
index e57a1aa03..39d1930b0 100644
--- a/py/picca/delta_extraction/survey.py
+++ b/py/picca/delta_extraction/survey.py
@@ -162,7 +162,8 @@ def compute_expected_flux(self):
         ExpectedFluxType = self.config.expected_flux[0]
         expected_flux_arguments = self.config.expected_flux[1]
         self.expected_flux = ExpectedFluxType(expected_flux_arguments)
-        self.expected_flux.compute_expected_flux(self.data.forests)
+        forests = self.data.select_forests_expected_flux_estimation()
+        self.expected_flux.compute_expected_flux(forests)
         t1 = time.time()
         self.logger.info(f"Time spent computing the mean expected flux: {t1-t0}")
 
diff --git a/py/picca/delta_extraction/utils.py b/py/picca/delta_extraction/utils.py
index e22bf5bef..737dfbbae 100644
--- a/py/picca/delta_extraction/utils.py
+++ b/py/picca/delta_extraction/utils.py
@@ -88,8 +88,6 @@
 
 ACCEPTED_BLINDING_STRATEGIES = [
     "none", "desi_m2", "desi_y1", "desi_y3"]
-# TODO: add tags here when we are allowed to unblind them
-UNBLINDABLE_STRATEGIES = []
 
 def class_from_string(class_name, module_name):
     """Return a class from a string. The class must be saved in a module
diff --git a/py/picca/io.py b/py/picca/io.py
index 2f84825af..8789483d1 100644
--- a/py/picca/io.py
+++ b/py/picca/io.py
@@ -326,7 +326,7 @@ def read_deltas(in_dir,
         max_num_spec: int or None - default: None
             Maximum number of spectra to read
         no_project: bool - default: False
-            If True, project the deltas (see equation 5 of du Mas des Bourboux
+            If False, project the deltas (see equation 5 of du Mas des Bourboux
             et al. 2020)
         nproc: int - default: None
             Number of cpus for parallelization. If None, uses all available.
diff --git a/py/picca/pk1d/compute_pk1d.py b/py/picca/pk1d/compute_pk1d.py
index 905412656..8efdd9158 100644
--- a/py/picca/pk1d/compute_pk1d.py
+++ b/py/picca/pk1d/compute_pk1d.py
@@ -8,6 +8,7 @@
     - compute_pk_raw
     - compute_pk_noise
     - compute_correction_reso
+    - check_linear_binning
 See the respective docstrings for more details
 """
 import numpy as np
@@ -17,16 +18,18 @@
 from picca.utils import userprint
 
 
-def split_forest(num_parts,
-                 pixel_step,
-                 lambda_or_log_lambda,
-                 delta,
-                 exposures_diff,
-                 ivar,
-                 first_pixel_index,
-                 abs_igm="LYA",
-                 reso_matrix=None,
-                 linear_binning=False):
+def split_forest(
+    num_parts,
+    pixel_step,
+    lambda_or_log_lambda,
+    delta,
+    exposures_diff,
+    ivar,
+    first_pixel_index,
+    abs_igm="LYA",
+    reso_matrix=None,
+    linear_binning=False,
+):
     """Split the forest in n parts
 
     Arguments
@@ -94,15 +97,17 @@ def split_forest(num_parts,
 
     for index in range(1, num_parts):
         lambda_or_log_lambda_limit.append(
-            lambda_or_log_lambda[num_bins * index + first_pixel_index])
+            lambda_or_log_lambda[num_bins * index + first_pixel_index]
+        )
 
     lambda_or_log_lambda_limit.append(
-        lambda_or_log_lambda[len(lambda_or_log_lambda) - 1] + 0.1 * pixel_step)
+        lambda_or_log_lambda[len(lambda_or_log_lambda) - 1] + 0.1 * pixel_step
+    )
 
     for index in range(num_parts):
-        selection = (
-            (lambda_or_log_lambda >= lambda_or_log_lambda_limit[index]) &
-            (lambda_or_log_lambda < lambda_or_log_lambda_limit[index + 1]))
+        selection = (lambda_or_log_lambda >= lambda_or_log_lambda_limit[index]) & (
+            lambda_or_log_lambda < lambda_or_log_lambda_limit[index + 1]
+        )
 
         lambda_or_log_lambda_part = lambda_or_log_lambda[selection].copy()
         lambda_abs_igm = constants.ABSORBER_IGM[abs_igm]
@@ -110,8 +115,7 @@ def split_forest(num_parts,
         if linear_binning:
             mean_z = np.mean(lambda_or_log_lambda_part) / lambda_abs_igm - 1.0
         else:
-            mean_z = np.mean(10**
-                             lambda_or_log_lambda_part) / lambda_abs_igm - 1.0
+            mean_z = np.mean(10**lambda_or_log_lambda_part) / lambda_abs_igm - 1.0
 
         if reso_matrix is not None:
             reso_matrix_part = reso_matrix[:, selection].copy()
@@ -122,21 +126,78 @@ def split_forest(num_parts,
         if exposures_diff is not None:
             exposures_diff_array.append(exposures_diff[selection].copy())
         else:  # fill exposures_diff_array with zeros
-            userprint('WARNING: exposure_diff is None, filling with zeros.')
+            userprint("WARNING: exposure_diff is None, filling with zeros.")
             exposures_diff_array.append(np.zeros(delta[selection].shape))
         ivar_array.append(ivar[selection].copy())
         if reso_matrix is not None:
             reso_matrix_array.append(reso_matrix_part)
 
     out = [
-        mean_z_array, lambda_or_log_lambda_array, delta_array,
-        exposures_diff_array, ivar_array
+        mean_z_array,
+        lambda_or_log_lambda_array,
+        delta_array,
+        exposures_diff_array,
+        ivar_array,
     ]
     if reso_matrix is not None:
         out.append(reso_matrix_array)
     return out
 
 
+def split_forest_in_z_parts(z_grid,
+                            log_lambda,
+                            delta,
+                            exposures_diff,
+                            ivar,
+                            min_num_pixels=None,
+                            reso_matrix=None,
+                            linear_binning=False):
+    """ Split forest in N parts divided in observed wavelength,
+        according to a given grid in redshift. See split_forest.
+    """
+    loglam_min_grid = np.log10(constants.ABSORBER_IGM['LYA']*(1+z_grid[:-1]))
+    loglam_max_grid = np.log10(constants.ABSORBER_IGM['LYA']*(1+z_grid[1:]))
+
+    num_parts = 0
+    mean_z_array, log_lambda_array, delta_array, ivar_array = [], [], [], []
+    exposures_diff_array = []
+    if reso_matrix is not None:
+        reso_matrix_array = []
+    if linear_binning:
+        lambda_array = []
+
+    for loglam_min_chunk, loglam_max_chunk in zip(loglam_min_grid, loglam_max_grid):
+        selection = (
+            (log_lambda >= loglam_min_chunk) &
+            (log_lambda < loglam_max_chunk))
+        if min_num_pixels is not None and np.sum(selection) < min_num_pixels:
+            continue
+        if np.sum(log_lambda <= loglam_min_chunk)==0 or np.sum(log_lambda >= loglam_max_chunk)==0:
+            # Strict criterium: the deltas are kept only if they span over the whole redshift part
+            continue
+        num_parts += 1
+        log_lambda_part = log_lambda[selection].copy()
+        log_lambda_array.append(log_lambda_part)
+        delta_array.append(delta[selection].copy())
+        ivar_array.append(ivar[selection].copy())
+        exposures_diff_array.append(exposures_diff[selection].copy())
+        mean_z_array.append(
+            np.mean(10**log_lambda_part) / constants.ABSORBER_IGM['LYA'] - 1.0)
+        if reso_matrix is not None:
+            reso_matrix_array.append(reso_matrix[:, selection].copy())
+        if linear_binning:
+            lambda_array.append(10**log_lambda_part)
+
+    if linear_binning:
+        out = [mean_z_array, lambda_array, delta_array, exposures_diff_array, ivar_array]
+    else:
+        out = [mean_z_array, log_lambda_array, delta_array, exposures_diff_array, ivar_array]
+    if reso_matrix is not None:
+        out.append(reso_matrix_array)
+
+    return out
+
+
 def rebin_diff_noise(pixel_step, lambda_or_log_lambda, exposures_diff):
     """Rebin the semidifference between two customized coadded spectra to
     construct the noise array
@@ -174,8 +235,11 @@ def rebin_diff_noise(pixel_step, lambda_or_log_lambda, exposures_diff):
     rebin_delta_lambda_or_log_lambda = rebin * pixel_step
 
     # rebin not mixing pixels separated by masks
-    bins = np.floor((lambda_or_log_lambda - lambda_or_log_lambda.min()) /
-                    rebin_delta_lambda_or_log_lambda + 0.5).astype(int)
+    bins = np.floor(
+        (lambda_or_log_lambda - lambda_or_log_lambda.min())
+        / rebin_delta_lambda_or_log_lambda
+        + 0.5
+    ).astype(int)
 
     rebin_exposure_diff = np.bincount(bins.astype(int), weights=exposures_diff)
     rebin_counts = np.bincount(bins.astype(int))
@@ -187,19 +251,24 @@ def rebin_diff_noise(pixel_step, lambda_or_log_lambda, exposures_diff):
     # now merge the rebinned array into a noise array
     noise = np.zeros(exposures_diff.size)
     for index in range(len(exposures_diff) // len(rebin_exposure_diff) + 1):
-        length_max = min(len(exposures_diff),
-                         (index + 1) * len(rebin_exposure_diff))
-        noise[index *
-              len(rebin_exposure_diff):length_max] = rebin_exposure_diff[:(
-                  length_max - index * len(rebin_exposure_diff))]
+        length_max = min(len(exposures_diff), (index + 1) * len(rebin_exposure_diff))
+        noise[index * len(rebin_exposure_diff) : length_max] = rebin_exposure_diff[
+            : (length_max - index * len(rebin_exposure_diff))
+        ]
         # shuffle the array before the next iteration
         np.random.shuffle(rebin_exposure_diff)
 
     return noise
 
 
-def fill_masked_pixels(delta_lambda_or_log_lambda, lambda_or_log_lambda, delta,
-                       exposures_diff, ivar, no_apply_filling):
+def fill_masked_pixels(
+    delta_lambda_or_log_lambda,
+    lambda_or_log_lambda,
+    delta,
+    exposures_diff,
+    ivar,
+    no_apply_filling,
+):
     """Fills the masked pixels with zeros
 
     Note that inputs can be either linear or log-lambda spaced units (but
@@ -275,8 +344,13 @@ def fill_masked_pixels(delta_lambda_or_log_lambda, lambda_or_log_lambda, delta,
 
     num_masked_pixels = len(index_all) - len(lambda_or_log_lambda_index)
 
-    return (lambda_or_log_lambda_new, delta_new, exposures_diff_new, ivar_new,
-            num_masked_pixels)
+    return (
+        lambda_or_log_lambda_new,
+        delta_new,
+        exposures_diff_new,
+        ivar_new,
+        num_masked_pixels,
+    )
 
 
 def compute_pk_raw(delta_lambda_or_log_lambda, delta, linear_binning=False):
@@ -299,32 +373,86 @@ def compute_pk_raw(delta_lambda_or_log_lambda, delta, linear_binning=False):
     k: array of float
     The Fourier modes the Power Spectrum is measured on
 
+    fft_delta: array of complex
+    The Fourier transform of delta
+
     pk: array of float
     The Power Spectrum
     """
     if linear_binning:  # spectral length in AA
         length_lambda = delta_lambda_or_log_lambda * len(delta)
     else:  # spectral length in km/s
-        length_lambda = (delta_lambda_or_log_lambda * constants.SPEED_LIGHT *
-                         np.log(10.) * len(delta))
+        length_lambda = (
+            delta_lambda_or_log_lambda
+            * constants.SPEED_LIGHT
+            * np.log(10.0)
+            * len(delta)
+        )
 
     # make 1D FFT
     num_pixels = len(delta)
     fft_delta = rfft(delta)
 
-    # compute power spectrum
-    pk = (fft_delta.real**2 + fft_delta.imag**2) * length_lambda / num_pixels**2
+    # compute wavenumber
     k = 2 * np.pi * rfftfreq(num_pixels, length_lambda / num_pixels)
 
-    return k, pk
+    # normalize Fourier transform of delta
+    fft_delta = fft_delta * np.sqrt(length_lambda) / num_pixels
+
+    # compute power spectrum
+    pk = fft_delta.real**2 + fft_delta.imag**2
+
+    return k, fft_delta, pk
+
 
+def compute_pk_cross_exposure(fft_delta_array,fft_delta_array_2):
+    """
+    Computes the cross-exposure power spectrum estimator.
+
+    Arguments
+    ---------
+    fft_delta_array: numpy.array
+    Contains the the fft of delta for all individual exposures
+
+    fft_delta_array_2: numpy.array
+    Contains the the fft of delta for all individual exposures for the second term
+
+    Return
+    ------
+    pk_cross_exposure: numpy.array
+    The power spectrum of the cross exposure estimator
 
-def compute_pk_noise(delta_lambda_or_log_lambda,
-                     ivar,
-                     exposures_diff,
-                     run_noise,
-                     num_noise_exposures=10,
-                     linear_binning=False):
+    """
+    number_exposures = len(fft_delta_array)
+    if number_exposures < 2:
+        raise ValueError(
+            "The cross exposure estimator can only be used "
+            "with a number of exposures equal or higher than 2"
+        )
+
+    pk_cross_exposure = np.zeros_like(fft_delta_array[0],dtype=np.float_)
+
+    for i, fft_delta_i in enumerate(fft_delta_array):
+        for j, fft_delta_2_j in enumerate(fft_delta_array_2):
+            if j != i :
+                pk_cross_exposure += (
+                    fft_delta_i.real * fft_delta_2_j.real
+                    + fft_delta_i.imag * fft_delta_2_j.imag
+                )
+    number_pairs = number_exposures * (number_exposures - 1)
+    pk_cross_exposure = pk_cross_exposure / number_pairs
+
+    return pk_cross_exposure
+
+
+def compute_pk_noise(
+    delta_lambda_or_log_lambda,
+    ivar,
+    exposures_diff,
+    run_noise,
+    num_noise_exposures=10,
+    linear_binning=False,
+):
     """Compute the noise power spectrum
 
     Two noise power spectrum are computed: one using the pipeline noise and
@@ -357,11 +485,18 @@ def compute_pk_noise(delta_lambda_or_log_lambda,
 
     pk_diff: array of float
     The noise Power Spectrum using the noise derived from exposures_diff
+
+    fft_delta_noise: array of float
+    The Fourier transform of noise pipeline realization
+
+    fft_delta_diff: array of float
+    The Fourier transform of the exposure difference delta
     """
     num_pixels = len(ivar)
     num_bins_fft = num_pixels // 2 + 1
 
     pk_noise = np.zeros(num_bins_fft)
+    fft_delta_noise = np.zeros(num_bins_fft)
     error = np.zeros(num_pixels)
     w = ivar > 0
     error[w] = 1.0 / np.sqrt(ivar[w])
@@ -369,19 +504,19 @@ def compute_pk_noise(delta_lambda_or_log_lambda,
     if run_noise:
         for _ in range(num_noise_exposures):
             delta_exp = np.zeros(num_pixels)
-            delta_exp[w] = np.random.normal(0., error[w])
-            _, pk_exp = compute_pk_raw(delta_lambda_or_log_lambda,
-                                       delta_exp,
-                                       linear_binning=linear_binning)
+            delta_exp[w] = np.random.normal(0.0, error[w])
+            _, fft_delta_noise, pk_exp = compute_pk_raw(
+                delta_lambda_or_log_lambda, delta_exp, linear_binning=linear_binning
+            )
             pk_noise += pk_exp
 
         pk_noise /= float(num_noise_exposures)
 
-    _, pk_diff = compute_pk_raw(delta_lambda_or_log_lambda,
-                                exposures_diff,
-                                linear_binning=linear_binning)
+    _, fft_delta_diff, pk_diff = compute_pk_raw(
+        delta_lambda_or_log_lambda, exposures_diff, linear_binning=linear_binning
+    )
 
-    return pk_noise, pk_diff
+    return pk_noise, pk_diff, fft_delta_noise, fft_delta_diff
 
 
 def compute_correction_reso(delta_pixel, mean_reso, k):
@@ -407,15 +542,16 @@ def compute_correction_reso(delta_pixel, mean_reso, k):
     num_bins_fft = len(k)
     correction = np.ones(num_bins_fft)
 
-    pixelization_factor = np.sinc(k * delta_pixel / (2 * np.pi))**2
+    pixelization_factor = np.sinc(k * delta_pixel / (2 * np.pi)) ** 2
 
-    correction *= np.exp(-(k * mean_reso)**2)
+    correction *= np.exp(-((k * mean_reso) ** 2))
     correction *= pixelization_factor
     return correction
 
 
-def compute_correction_reso_matrix(reso_matrix, k, delta_pixel, num_pixel,
-                                   pixelization_correction = False):
+def compute_correction_reso_matrix(
+    reso_matrix, k, delta_pixel, num_pixel, pixelization_correction=False
+):
     """Computes the resolution correction based on the resolution matrix using linear binning
 
     Arguments
@@ -438,20 +574,21 @@ def compute_correction_reso_matrix(reso_matrix, k, delta_pixel, num_pixel,
     correction: array of float
     The resolution correction
     """
-    #this allows either computing the power for each pixel seperately or for the mean
+    # this allows either computing the power for each pixel seperately or for the mean
     reso_matrix = np.atleast_2d(reso_matrix)
 
     w2_arr = []
-    #first compute the power in the resmat for each pixel, then average
+    # first compute the power in the resmat for each pixel, then average
     for resmat in reso_matrix:
         r = np.append(resmat, np.zeros(num_pixel - resmat.size))
-        k_resmat, w2 = compute_pk_raw(delta_pixel, r, linear_binning=True)
+        k_resmat, _, w2 = compute_pk_raw(delta_pixel, r, linear_binning=True)
         try:
             assert np.all(k_resmat == k)
         except AssertionError as error:
             raise AssertionError(
                 "for some reason the resolution matrix correction has "
-                "different k scaling than the pk") from error
+                "different k scaling than the pk"
+            ) from error
         w2 /= w2[0]
         w2_arr.append(w2)
 
@@ -460,19 +597,61 @@ def compute_correction_reso_matrix(reso_matrix, k, delta_pixel, num_pixel,
     # the following assumes that the resolution matrix is storing the actual
     # resolution convolved with the pixelization kernel along each matrix axis
     correction = w_res2
-    if pixelization_correction :
-        pixelization_factor = np.sinc(k * delta_pixel / (2 * np.pi))**2
+    if pixelization_correction:
+        pixelization_factor = np.sinc(k * delta_pixel / (2 * np.pi)) ** 2
         correction /= pixelization_factor
 
     return correction
 
 
+def check_linear_binning(delta):
+    """checks if the wavelength binning is linear or log,
+      this is stable against masking
+
+    Args:
+        delta (Delta): delta class as read with Delta.from_...
+
+    Raises:
+        ValueError: Raised if binning is neither linear nor log,
+        or if delta.log_lambda was actually wavelength
+
+    Returns:
+        linear_binning (bool): boolean telling the binning_type
+        pixel_step (float): size of a wavelength bin in the right unit
+    """
+
+    diff_lambda = np.diff(10**delta.log_lambda)
+    diff_log_lambda = np.diff(delta.log_lambda)
+    q5_lambda, q25_lambda = np.percentile(diff_lambda, [5, 25])
+    q5_log_lambda, q25_log_lambda = np.percentile(diff_log_lambda, [5, 25])
+    if (q25_lambda - q5_lambda) < 1e-6:
+        # we can assume linear binning for this case
+        linear_binning = True
+        pixel_step = np.min(diff_lambda)
+    elif (q25_log_lambda - q5_log_lambda) < 1e-6 and q5_log_lambda < 0.01:
+        # we can assume log_linear binning for this case
+        linear_binning = False
+        pixel_step = np.min(diff_log_lambda)
+    elif q5_log_lambda >= 0.01:
+        raise ValueError(
+            "Could not figure out if linear or log wavelength binning was used,"
+            "probably submitted lambda as log_lambda"
+        )
+    else:
+        raise ValueError(
+            "Could not figure out if linear or log wavelength binning was used"
+        )
+
+    return linear_binning, pixel_step
+
+
 class Pk1D:
     """Class to represent the 1D Power Spectrum for a given forest
 
     Class Methods
     -------------
     from_fitsio
+    write_fits
 
     Methods
     -------
@@ -489,14 +668,8 @@ class Pk1D:
     z_qso: float
     Redshift of the quasar.
 
-    plate: int
-    Plate number of the observation.
-
-    fiberid: int
-    Fiberid of the observation.
-
-    mjd: int
-    Modified Julian Date of the observation.
+    mean_z: float
+    Mean redshift of the forest
 
     mean_snr: float
     Mean signal-to-noise ratio in the forest
@@ -504,12 +677,18 @@ class Pk1D:
     mean_reso: float
     Mean resolution of the forest
 
-    mean_z: float
-    Mean redshift of the forest
-
     num_masked_pixels: int
     Number of masked pixels
 
+    linear_bining: bool
+    If the spectra used is linearly binned or not
+
+    los_id: int
+    Indentifier of the quasar (TARGETID in DESI)
+
+    chunk_id: int
+    For forest cut in chunks, identifier of the chunk
+
     k: array of float
     Fourier modes
 
@@ -519,32 +698,48 @@ class Pk1D:
     pk_noise: array of float
     Noise power spectrum for the different Fourier modes
 
+    pk_diff: array of float or None - default: None
+    Power spectrum of exposures_diff for the different Fourier modes
+
     correction_reso: array of float
     Resolution correction
 
     pk: array of float
     Power Spectrum
 
-    pk_diff: array of float or None
-    Power spectrum of exposures_diff for the different Fourier modes
+    fft_delta: array of float
+    In the case where the fft of delta is stored, contains it
+
+    fft_delta_noise: array of float
+    fft of noise realization
+
+    fft_delta_diff: array of float
+    fft of exposure difference
     """
-    def __init__(self,
-                 ra,
-                 dec,
-                 z_qso,
-                 mean_z,
-                 plate,
-                 mjd,
-                 fiberid,
-                 mean_snr,
-                 mean_reso,
-                 k,
-                 pk_raw,
-                 pk_noise,
-                 correction_reso,
-                 pk,
-                 num_masked_pixels,
-                 pk_diff=None):
+
+    # pylint: disable=too-many-arguments
+    def __init__(
+        self,
+        ra=None,
+        dec=None,
+        z_qso=None,
+        mean_z=None,
+        mean_snr=None,
+        mean_reso=None,
+        num_masked_pixels=None,
+        linear_bining=None,
+        los_id=None,
+        chunk_id=None,
+        k=None,
+        pk_raw=None,
+        pk_noise=None,
+        pk_diff=None,
+        correction_reso=None,
+        pk=None,
+        fft_delta=None,
+        fft_delta_noise = None,
+        fft_delta_diff = None,
+    ):
         """Initialize instance
 
         Arguments
@@ -561,21 +756,24 @@ def __init__(self,
         mean_z: float
         Mean redshift of the forest
 
-        plate: int
-        Plate number of the observation.
-
-        mjd: int
-        Modified Julian Date of the observation.
-
-        fiberid: int
-        Fiberid of the observation.
-
         mean_snr: float
         Mean signal-to-noise ratio in the forest
 
         mean_reso: float
         Mean resolution of the forest
 
+        num_masked_pixels: int
+        Number of masked pixels
+
+        linear_bining: bool
+        If the spectra used is linearly binned or not
+
+        los_id: int
+        Indentifier of the quasar (TARGETID in DESI)
+
+        chunk_id: int
+        For forest cut in chunks, identifier of the chunk
+
         k: array of float
         Fourier modes
 
@@ -585,17 +783,24 @@ def __init__(self,
         pk_noise: array of float
         Noise power spectrum for the different Fourier modes
 
+        pk_diff: array of float or None - default: None
+        Power spectrum of exposures_diff for the different Fourier modes
+
         correction_reso: array of float
         Resolution correction
 
         pk: array of float
         Power Spectrum
 
-        num_masked_pixels: int
-        Number of masked pixels
+        fft_delta: array of float
+        In the case where the fft of delta is stored, contains it
 
-        pk_diff: array of float or None - default: None
-        Power spectrum of exposures_diff for the different Fourier modes
+        fft_delta_noise: array of float
+        fft of noise realization
+
+        fft_delta_diff: array of float
+        fft of exposure difference
+    
         """
         self.ra = ra
         self.dec = dec
@@ -604,16 +809,19 @@ def __init__(self,
         self.mean_snr = mean_snr
         self.mean_reso = mean_reso
         self.num_masked_pixels = num_masked_pixels
+        self.linear_bining = linear_bining
+        self.los_id = los_id
+        self.chunk_id = chunk_id
 
-        self.plate = plate
-        self.mjd = mjd
-        self.fiberid = fiberid
         self.k = k
         self.pk_raw = pk_raw
         self.pk_noise = pk_noise
+        self.pk_diff = pk_diff
         self.correction_reso = correction_reso
         self.pk = pk
-        self.pk_diff = pk_diff
+        self.fft_delta = fft_delta
+        self.fft_delta_noise = fft_delta_noise
+        self.fft_delta_diff = fft_delta_diff
 
     @classmethod
     def from_fitsio(cls, hdu):
@@ -631,25 +839,173 @@ def from_fitsio(cls, hdu):
 
         header = hdu.read_header()
 
-        ra = header['RA']
-        dec = header['DEC']
-        z_qso = header['Z']
-        mean_z = header['MEANZ']
-        mean_reso = header['MEANRESO']
-        mean_snr = header['MEANSNR']
-        plate = header['PLATE']
-        mjd = header['MJD']
-        fiberid = header['FIBER']
-        num_masked_pixels = header['NBMASKPIX']
+        ra = header["RA"]
+        dec = header["DEC"]
+        z_qso = header["Z"]
+        mean_z = header["MEANZ"]
+        mean_reso = header["MEANRESO"]
+        mean_snr = header["MEANSNR"]
+        num_masked_pixels = header["NBMASKPIX"]
+        linear_bining = header["LIN_BIN"]
+        los_id = header["LOS_ID"]
+        chunk_id = header["CHUNK_ID"]
 
         data = hdu.read()
-        k = data['k'][:]
-        pk = data['Pk'][:]
-        pk_raw = data['Pk_raw'][:]
-        pk_noise = data['Pk_noise'][:]
-        correction_reso = data['cor_reso'][:]
-        pk_diff = data['Pk_diff'][:]
-
-        return cls(ra, dec, z_qso, mean_z, plate, mjd, fiberid, mean_snr,
-                   mean_reso, k, pk_raw, pk_noise, correction_reso, pk,
-                   num_masked_pixels, pk_diff)
+        k = data["K"][:]
+        pk_noise = data["PK_NOISE"][:]
+        correction_reso = data["COR_RESO"][:]
+        pk_diff = data["PK_DIFF"][:]
+        pk = data["PK"][:]
+        pk_raw = data["PK_RAW"][:]
+
+        try:
+            fft_delta = data["DELTA_K"][:]
+            fft_delta_noise = data["DELTA_NOISE_K"][:]
+            fft_delta_diff = data["DELTA_DIFF_K"][:]
+
+        except ValueError:
+            fft_delta = None
+            fft_delta_noise = None
+            fft_delta_diff = None
+
+        return cls(
+            ra=ra,
+            dec=dec,
+            z_qso=z_qso,
+            mean_z=mean_z,
+            mean_snr=mean_snr,
+            mean_reso=mean_reso,
+            num_masked_pixels=num_masked_pixels,
+            linear_bining=linear_bining,
+            los_id=los_id,
+            chunk_id=chunk_id,
+            k=k,
+            pk_raw=pk_raw,
+            pk_noise=pk_noise,
+            pk_diff=pk_diff,
+            correction_reso=correction_reso,
+            pk=pk,
+            fft_delta=fft_delta,
+            fft_delta_noise = fft_delta_noise,
+            fft_delta_diff = fft_delta_diff,
+        )
+
+    def write_fits(self, file):
+        """Write an individual pk1D inside a FITS file.
+
+        Arguments
+        ---------
+        file: fitsio file
+        File to which a pk1D hdu will be added
+
+        Return
+        ------
+        None
+        """
+        header = [
+            {
+                "name": "RA",
+                "value": self.ra,
+                "comment": "QSO's Right Ascension [degrees]",
+            },
+            {
+                "name": "DEC",
+                "value": self.dec,
+                "comment": "QSO's Declination [degrees]",
+            },
+            {"name": "Z", "value": self.z_qso, "comment": "QSO's redshift"},
+            {
+                "name": "MEANZ",
+                "value": self.mean_z,
+                "comment": "Absorbers mean redshift",
+            },
+            {
+                "name": "MEANSNR",
+                "value": self.mean_snr,
+                "comment": "Mean signal to noise ratio",
+            },
+            {
+                "name": "MEANRESO",
+                "value": self.mean_reso,
+                "comment": "Mean resolution [km/s]",
+            },
+            {
+                "name": "NBMASKPIX",
+                "value": self.num_masked_pixels,
+                "comment": "Number of masked pixels in the section",
+            },
+            {
+                "name": "LIN_BIN",
+                "value": self.linear_bining,
+                "comment": "analysis was performed on delta with linear binned lambda",
+            },
+            {
+                "name": "LOS_ID",
+                "value": self.los_id,
+                "comment": "line of sight identifier, e.g. THING_ID or TARGETID",
+            },
+            {
+                "name": "CHUNK_ID",
+                "value": self.chunk_id,
+                "comment": "Chunk (sub-forest) identifier",
+            },
+        ]
+
+        cols = [
+            self.k,
+            self.pk_raw,
+            self.pk_noise,
+            self.pk_diff,
+            self.correction_reso,
+            self.pk,
+        ]
+        names = ["K", "PK_RAW", "PK_NOISE", "PK_DIFF", "COR_RESO", "PK"]
+        comments = [
+            "Wavenumber",
+            "Raw power spectrum",
+            "Noise's power spectrum",
+            "Noise coadd difference power spectrum",
+            "Correction resolution function",
+            "Corrected power spectrum (resolution and noise)",
+        ]
+        if self.linear_bining:
+            baseunit = "AA"
+        else:
+            baseunit = "km/s"
+        units = [
+            f"({baseunit})^-1",
+            f"{baseunit}",
+            f"{baseunit}",
+            f"{baseunit}",
+            f"{baseunit}",
+            f"{baseunit}",
+        ]
+
+        if self.fft_delta is not None:
+            cols += [self.fft_delta,
+                     self.fft_delta_noise,
+                     self.fft_delta_diff,
+                     ]
+            names += [
+                "DELTA_K",
+                "DELTA_NOISE_K",
+                "DELTA_DIFF_K",
+            ]
+            comments += [
+                "Fourier delta",
+                "Fourier delta noise",
+                "Fourier delta diff",
+            ]
+            units += [
+                f"{baseunit}^(1/2)",
+                f"{baseunit}^(1/2)",
+                f"{baseunit}^(1/2)",
+            ]
+
+        file.write(
+            cols,
+            names=names,
+            header=header,
+            comment=comments,
+            units=units,
+        )
diff --git a/py/picca/pk1d/postproc_pk1d.py b/py/picca/pk1d/postproc_pk1d.py
index 604315cd9..cdd6ab6fe 100644
--- a/py/picca/pk1d/postproc_pk1d.py
+++ b/py/picca/pk1d/postproc_pk1d.py
@@ -12,25 +12,23 @@
 See the respective documentation for details
 """
 
-import os
 import glob
+import os
+from functools import partial
 from multiprocessing import Pool
 
-from functools import partial
-import numpy as np
-from scipy.stats import binned_statistic
-from scipy.optimize import curve_fit
 import fitsio
-from astropy.table import Table, vstack
+import numpy as np
 from astropy.stats import bootstrap
-
-from picca.constants import SPEED_LIGHT
-from picca.constants import ABSORBER_IGM
-from picca.utils import userprint
+from astropy.table import Table, vstack
+from scipy.optimize import curve_fit
+from scipy.stats import binned_statistic
+from picca.constants import ABSORBER_IGM, SPEED_LIGHT
 from picca.pk1d.utils import MEANPK_FITRANGE_SNR, fitfunc_variance_pk1d
+from picca.utils import userprint
 
 
-def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
+def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None, skymask_matrices=None):
     """Read Pk1D data from a single file.
 
     Arguments
@@ -49,6 +47,12 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
     Required if len(snrcut)>1. List of redshifts
     associated to the list of snr cuts.
 
+    skymask_matrices: list or None
+    if not None, computes a correction associated to skyline masking, for the Pk_raw
+    and Pk_noise vectors. This correction makes use of a matrix operation.
+    The corrected powers are stored into "Pk_raw_skycorr" and "Pk_noise_skycorr".
+    Note that this correction makes sense only after the Pks have been averaged over sightlines.
+
     Return
     ------
     p1d_table: Table
@@ -60,6 +64,8 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
     """
     p1d_table = []
     z_array = []
+    if skymask_matrices is not None:
+        meanz_skymatrices = np.array([x[0] for x in skymask_matrices])
     with fitsio.FITS(filename, "r") as hdus:
         for i, hdu in enumerate(hdus[1:]):
             data = hdu.read()
@@ -73,6 +79,9 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
                 "Pk_diff",
                 "cor_reso",
                 "Pk_noise_miss",
+                "Delta_k",
+                "Delta_noise_k",
+                "Delta_diff_k",
             ]:
                 try:
                     chunk_table.rename_column(colname.upper(), colname)
@@ -84,13 +93,33 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
                     chunk_table["Pk_raw"] - chunk_table["Pk_noise"]
                 ) / chunk_table["cor_reso"]
 
+            if skymask_matrices is not None:
+                chunk_table["Pk_raw_skycorr"] = chunk_table["Pk_raw"]
+                chunk_table["Pk_noise_skycorr"] = chunk_table["Pk_noise"]
+                w, = np.where(np.isclose(meanz_skymatrices, chunk_header["MEANZ"], atol=1.e-2))
+                if len(w)==1:
+                    correction_matrix = skymask_matrices[w[0]][1]
+                    ll = len(chunk_table["Pk_raw"])
+                    correction_matrix = np.copy(correction_matrix[0:ll, 0:ll])
+                    if len(chunk_table["Pk_raw"])!=correction_matrix.shape[0]:
+                        userprint(f"""file {filename} hdu {i+1}:"""
+                                  """Pk_raw doesnt match shape of skymatrix.""")
+                    else:
+                        chunk_table["Pk_raw_skycorr"] = correction_matrix @ chunk_table["Pk_raw"]
+                        chunk_table["Pk_noise_skycorr"] = (
+                            correction_matrix @ chunk_table["Pk_noise"]
+                        )
+                chunk_table["Pk_skycorr"] = (
+                    chunk_table["Pk_raw_skycorr"] - chunk_table["Pk_noise_skycorr"]
+                ) / chunk_table["cor_reso"]
+
             chunk_table["forest_z"] = float(chunk_header["MEANZ"])
             chunk_table["forest_snr"] = float(chunk_header["MEANSNR"])
             chunk_table["forest_id"] = int(chunk_header["LOS_ID"])
             if "CHUNK_ID" in chunk_header:
-                chunk_table[
-                    "sub_forest_id"
-                ] = f"{chunk_header['LOS_ID']}_{chunk_header['CHUNK_ID']}"
+                chunk_table["sub_forest_id"] = (
+                    f"{chunk_header['LOS_ID']}_{chunk_header['CHUNK_ID']}"
+                )
 
             if snrcut is not None:
                 if len(snrcut) > 1:
@@ -108,7 +137,8 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
             # Empirically remove very noisy chunks
             (wk,) = np.where(chunk_table["k"] < kbin_edges[-1])
             if (
-                chunk_table["Pk_noise"][wk] > 1000000 * chunk_table["Pk_raw"][wk]
+                np.abs(chunk_table["Pk_noise"][wk])
+                > 1000000 * np.abs(chunk_table["Pk_raw"][wk])
             ).any():
                 userprint(
                     f"file {filename} hdu {i+1} has very high noise power: discarded"
@@ -126,6 +156,8 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
     p1d_table["Pk_norescor"] = p1d_table["Pk_raw"] - p1d_table["Pk_noise"]
     p1d_table["Pk_nonoise"] = p1d_table["Pk_raw"] / p1d_table["cor_reso"]
     p1d_table["Pk_noraw"] = p1d_table["Pk_noise"] / p1d_table["cor_reso"]
+    if skymask_matrices is not None:
+        p1d_table["Delta2_skycorr"] = p1d_table["k"] * p1d_table["Pk_skycorr"] / np.pi
     try:
         p1d_table["Pk_noraw_miss"] = p1d_table["Pk_noise_miss"] / p1d_table["cor_reso"]
     except KeyError:
@@ -136,6 +168,42 @@ def read_pk1d(filename, kbin_edges, snrcut=None, zbins_snrcut=None):
     return p1d_table, z_array
 
 
+def mean_p1d_table_regular_slice(izbin, nbins_k):
+    """Return the arguments of a slice of mean P1D table for a given redshift.
+
+    Arguments
+    ---------
+    izbin (int):
+    Current redshift bin being considered.
+
+    nbins_k (int):
+    Number of k bins.
+
+    Return
+    ------
+    Arguments of a slice of mean P1D table.
+    """
+    return izbin * nbins_k, (izbin + 1) * nbins_k
+
+
+def cov_table_regular_slice(izbin, nbins_k):
+    """Return the arguments of a slice of covariance table for a given redshift.
+
+    Arguments
+    ---------
+    izbin (int):
+    Current redshift bin being considered.
+
+    nbins_k (int):
+    Number of k bins.
+
+    Return
+    ------
+    Arguments of a slice of covariance table.
+    """
+    return izbin * nbins_k * nbins_k, (izbin + 1) * nbins_k * nbins_k
+
+
 def compute_mean_pk1d(
     p1d_table,
     z_array,
@@ -221,6 +289,8 @@ def compute_mean_pk1d(
     if "sub_forest_id" in p1d_table_cols:
         p1d_table_cols.remove("sub_forest_id")
 
+    p1d_table_cols = [col for col in p1d_table_cols if "Delta_" not in col]
+
     # Convert data into velocity units
     if velunits:
         conversion_factor = (
@@ -340,111 +410,67 @@ def compute_mean_pk1d(
         nomedians,
     )
 
-    if compute_covariance:
-        userprint("Computing covariance matrix")
-        params_pool = []
-        # Main loop 1) z bins
-        for izbin in range(nbins_z):
-            select_z = (p1d_table["forest_z"] < zbin_edges[izbin + 1]) & (
-                p1d_table["forest_z"] > zbin_edges[izbin]
-            )
-            sub_forest_ids = np.unique(p1d_table["sub_forest_id"][select_z])
-            params_pool.append([izbin, select_z, sub_forest_ids])
+    if compute_covariance or compute_bootstrap:
+        userprint("Computation of p1d groups for covariance matrix calculation")
 
-        func = partial(
-            compute_cov,
-            p1d_table,
-            mean_p1d_table,
-            zbin_centers,
-            n_chunks,
-            kbin_edges,
-            k_index,
-            nbins_k,
-            weight_method,
-            snrfit_table,
-        )
-        if number_worker == 1:
-            output_cov = [func(*p) for p in params_pool]
-        else:
-            with Pool(number_worker) as pool:
-                output_cov = pool.starmap(func, params_pool)
-        # Main loop 1) z bins
+        p1d_groups = []
         for izbin in range(nbins_z):
-            (
-                zbin_array,
-                index_zbin_array,
-                n_array,
-                covariance_array,
-                k1_array,
-                k2_array,
-            ) = (*output_cov[izbin],)
-            i_min = izbin * nbins_k * nbins_k
-            i_max = (izbin + 1) * nbins_k * nbins_k
-            cov_table["zbin"][i_min:i_max] = zbin_array
-            cov_table["index_zbin"][i_min:i_max] = index_zbin_array
-            cov_table["N"][i_min:i_max] = n_array
-            cov_table["covariance"][i_min:i_max] = covariance_array
-            cov_table["k1"][i_min:i_max] = k1_array
-            cov_table["k2"][i_min:i_max] = k2_array
-
-    if compute_bootstrap:
-        userprint("Computing covariance matrix with bootstrap method")
 
-        params_pool = []
-        # Main loop 1) z bins
-        for izbin in range(nbins_z):
-            select_z = (p1d_table["forest_z"] < zbin_edges[izbin + 1]) & (
-                p1d_table["forest_z"] > zbin_edges[izbin]
+            zbin_array = np.full(nbins_k * nbins_k, zbin_centers[izbin])
+            index_zbin_array = np.full(nbins_k * nbins_k, izbin, dtype=int)
+            kbin_centers = (kbin_edges[1:] + kbin_edges[:-1]) / 2
+            k1_array, k2_array = np.meshgrid(kbin_centers, kbin_centers, indexing="ij")
+            k1_array = np.ravel(k1_array)
+            k2_array = np.ravel(k2_array)
+            select = mean_p1d_table["index_zbin"] == izbin
+            n_array = np.ravel(
+                np.outer(mean_p1d_table["N"][select], mean_p1d_table["N"][select])
             )
-
-            sub_forest_ids = np.unique(p1d_table["sub_forest_id"][select_z])
-            if sub_forest_ids.size > 0:
-                bootid = np.array(
-                    bootstrap(np.arange(sub_forest_ids.size), number_bootstrap)
-                ).astype(int)
+            index_cov = cov_table_regular_slice(izbin, nbins_k)
+            cov_table["zbin"][index_cov[0]:index_cov[1]] = zbin_array
+            cov_table["index_zbin"][index_cov[0]:index_cov[1]] = index_zbin_array
+            cov_table["k1"][index_cov[0]:index_cov[1]] = k1_array
+            cov_table["k2"][index_cov[0]:index_cov[1]] = k2_array
+            cov_table["N"][index_cov[0]:index_cov[1]] = n_array
+            index_mean = mean_p1d_table_regular_slice(izbin, nbins_k)
+            mean_pk = mean_p1d_table["meanPk"][index_mean[0] : index_mean[1]]
+            error_pk = mean_p1d_table["errorPk"][index_mean[0] : index_mean[1]]
+
+            if n_chunks[izbin] == 0:
+                p1d_weights_z, covariance_weights_z, p1d_groups_z = [], [], []
             else:
-                bootid = np.full(number_bootstrap, None)
+                p1d_weights_z, covariance_weights_z, p1d_groups_z = compute_p1d_groups(
+                    weight_method,
+                    nbins_k,
+                    zbin_edges,
+                    izbin,
+                    p1d_table,
+                    k_index,
+                    snrfit_table,
+                    number_worker,
+                )
 
-            # Main loop 2) number of bootstrap samples
-            for iboot in range(number_bootstrap):
-                params_pool.append([izbin, select_z, sub_forest_ids[bootid[iboot]]])
+            p1d_groups.append(
+                [
+                    mean_pk,
+                    error_pk,
+                    p1d_weights_z,
+                    covariance_weights_z,
+                    p1d_groups_z,
+                ]
+            )
 
-        func = partial(
-            compute_cov,
-            p1d_table,
-            mean_p1d_table,
-            zbin_centers,
-            n_chunks,
-            kbin_edges,
-            k_index,
-            nbins_k,
+        compute_and_fill_covariance(
+            compute_covariance,
+            compute_bootstrap,
             weight_method,
-            snrfit_table,
+            nbins_k,
+            nbins_z,
+            p1d_groups,
+            number_worker,
+            number_bootstrap,
+            cov_table,
         )
-        if number_worker == 1:
-            output_cov = [func(*p) for p in params_pool]
-        else:
-            with Pool(number_worker) as pool:
-                output_cov = pool.starmap(func, params_pool)
-        # Main loop 1) z bins
-        for izbin in range(nbins_z):
-            boot_cov = []
-            # Main loop 2) number of bootstrap samples
-            for iboot in range(number_bootstrap):
-                (
-                    zbin_array,
-                    index_zbin_array,
-                    n_array,
-                    covariance_array,
-                    k1_array,
-                    k2_array,
-                ) = (*output_cov[izbin * number_bootstrap + iboot],)
-                boot_cov.append(covariance_array)
-
-            i_min = izbin * nbins_k * nbins_k
-            i_max = (izbin + 1) * nbins_k * nbins_k
-            cov_table["boot_covariance"][i_min:i_max] = np.mean(boot_cov, axis=0)
-            cov_table["error_boot_covariance"][i_min:i_max] = np.std(boot_cov, axis=0)
 
     if output_snrfit is not None:
         np.savetxt(
@@ -516,21 +542,20 @@ def fill_average_pk(
             median_array,
             snrfit_array,
         ) = (*output_mean[izbin],)
-        i_min = izbin * nbins_k
-        i_max = (izbin + 1) * nbins_k
+        index_mean = mean_p1d_table_regular_slice(izbin,nbins_k)
 
-        mean_p1d_table["zbin"][i_min:i_max] = zbin_array
-        mean_p1d_table["index_zbin"][i_min:i_max] = index_zbin_array
-        mean_p1d_table["N"][i_min:i_max] = n_array
+        mean_p1d_table["zbin"][index_mean[0]:index_mean[1]] = zbin_array
+        mean_p1d_table["index_zbin"][index_mean[0]:index_mean[1]] = index_zbin_array
+        mean_p1d_table["N"][index_mean[0]:index_mean[1]] = n_array
         for icol, col in enumerate(p1d_table_cols):
-            mean_p1d_table["mean" + col][i_min:i_max] = mean_array[icol]
-            mean_p1d_table["error" + col][i_min:i_max] = error_array[icol]
-            mean_p1d_table["min" + col][i_min:i_max] = min_array[icol]
-            mean_p1d_table["max" + col][i_min:i_max] = max_array[icol]
+            mean_p1d_table["mean" + col][index_mean[0]:index_mean[1]] = mean_array[icol]
+            mean_p1d_table["error" + col][index_mean[0]:index_mean[1]] = error_array[icol]
+            mean_p1d_table["min" + col][index_mean[0]:index_mean[1]] = min_array[icol]
+            mean_p1d_table["max" + col][index_mean[0]:index_mean[1]] = max_array[icol]
             if not nomedians:
-                mean_p1d_table["median" + col][i_min:i_max] = median_array[icol]
+                mean_p1d_table["median" + col][index_mean[0]:index_mean[1]] = median_array[icol]
         if weight_method == "fit_snr":
-            snrfit_table[i_min:i_max, :] = snrfit_array
+            snrfit_table[index_mean[0]:index_mean[1], :] = snrfit_array
 
 
 def compute_average_pk_redshift(
@@ -728,7 +753,8 @@ def compute_average_pk_redshift(
                 data_snr[mask_nan_p1d_values],
                 p1d_values[mask_nan_p1d_values],
             )
-
+            if len(p1d_values) == 0:
+                continue
             standard_dev, _, _ = binned_statistic(
                 data_snr, p1d_values, statistic="std", bins=snr_bin_edges
             )
@@ -737,6 +763,8 @@ def compute_average_pk_redshift(
                 standard_dev[~np.isnan(standard_dev)],
                 snr_bins[~np.isnan(standard_dev)],
             )
+            if len(standard_dev) == 0:
+                continue
             coef, *_ = curve_fit(
                 fitfunc_variance_pk1d,
                 snr_bins,
@@ -821,8 +849,7 @@ def compute_average_pk_redshift(
                     mean = np.average(p1d_table[col][select], weights=redshift_weights)
                     # simple analytic expression:
                     error = np.std(p1d_table[col][select]) * (
-                        np.sqrt(np.sum(redshift_weights**2))
-                        / np.sum(redshift_weights)
+                        np.sqrt(np.sum(redshift_weights**2)) / np.sum(redshift_weights)
                     )
                 else:
                     mean = np.mean(p1d_table[col][select])
@@ -854,12 +881,375 @@ def compute_average_pk_redshift(
     )
 
 
+def compute_and_fill_covariance(
+    compute_covariance,
+    compute_bootstrap,
+    weight_method,
+    nbins_k,
+    nbins_z,
+    p1d_groups,
+    number_worker,
+    number_bootstrap,
+    cov_table,
+):
+    """Compute the covariance and bootstrap covariance and fill the corresponding
+    cov_table variable
+
+    compute_covariance: Bool
+    If True, compute statistical covariance matrix of the mean P1D.
+    Requires  p1d_table to contain 'sub_forest_id', since correlations are computed
+    within individual forests.
+
+    compute_bootstrap: Bool
+    If True, compute statistical covariance using a simple bootstrap method.
+
+    weight_method: str,
+    Method to weight the data.
+
+    nbins_k (int):
+    Number of k bins.
+
+    nbins_z (int):
+    Number of z bins.
+
+    p1d_groups (array-like):
+    Individual p1d pixels grouped in the same wavenumber binning for all subforest
+
+    number_worker: int
+    Calculations of mean P1Ds and covariances are run parallel over redshift bins.
+
+    number_bootstrap: int
+    Number of bootstrap samples used if compute_bootstrap is True.
+
+    cov_table (array-like):
+    Covariance table to fill.
+
+    Return
+    ------
+    None
+    """
+
+    if compute_covariance:
+        userprint("Computation of covariance matrix")
+
+        func = partial(
+            compute_cov,
+            weight_method,
+            nbins_k,
+        )
+        if number_worker == 1:
+            output_cov = [func(*p1d_group) for p1d_group in p1d_groups]
+        else:
+            with Pool(number_worker) as pool:
+                output_cov = pool.starmap(func, p1d_groups)
+
+        for izbin in range(nbins_z):
+            covariance_array = output_cov[izbin]
+            index_cov = cov_table_regular_slice(izbin, nbins_k)
+            cov_table["covariance"][index_cov[0]:index_cov[1]] = covariance_array
+
+    if compute_bootstrap:
+        userprint("Computing covariance matrix with bootstrap method")
+        p1d_groups_bootstrap = []
+        for izbin in range(nbins_z):
+            number_sub_forests = len(p1d_groups[izbin][2])
+            if number_sub_forests > 0:
+                bootid = np.array(
+                    bootstrap(np.arange(number_sub_forests), number_bootstrap)
+                ).astype(int)
+            else:
+                bootid = np.full(number_bootstrap, None)
+
+            for iboot in range(number_bootstrap):
+                if bootid[iboot] is None:
+                    (
+                        mean_pk,
+                        error_pk,
+                        p1d_weights_z,
+                        covariance_weights_z,
+                        p1d_groups_z,
+                    ) = ([], [], [], [], [])
+                else:
+                    mean_pk = p1d_groups[izbin][0]
+                    error_pk = p1d_groups[izbin][1]
+                    p1d_weights_z = p1d_groups[izbin][2][bootid[iboot]]
+                    covariance_weights_z = p1d_groups[izbin][3][bootid[iboot]]
+                    p1d_groups_z = p1d_groups[izbin][4][bootid[iboot]]
+                p1d_groups_bootstrap.append(
+                    [
+                        mean_pk,
+                        error_pk,
+                        p1d_weights_z,
+                        covariance_weights_z,
+                        p1d_groups_z,
+                    ]
+                )
+
+        func = partial(
+            compute_cov,
+            weight_method,
+            nbins_k,
+        )
+        if number_worker == 1:
+            output_cov = [func(*p) for p in p1d_groups_bootstrap]
+        else:
+            with Pool(number_worker) as pool:
+                output_cov = pool.starmap(func, p1d_groups_bootstrap)
+
+        for izbin in range(nbins_z):
+            boot_cov = []
+            for iboot in range(number_bootstrap):
+                covariance_array = (output_cov[izbin * number_bootstrap + iboot],)
+                boot_cov.append(covariance_array)
+
+            index_cov = cov_table_regular_slice(izbin, nbins_k)
+            cov_table["boot_covariance"][index_cov[0]:index_cov[1]] = np.mean(boot_cov, axis=0)
+            cov_table["error_boot_covariance"][index_cov[0]:index_cov[1]] = np.std(boot_cov, axis=0)
+
+
+def compute_p1d_groups(
+    weight_method,
+    nbins_k,
+    zbin_edges,
+    izbin,
+    p1d_table,
+    k_index,
+    snrfit_table,
+    number_worker,
+):
+    """Compute the P1D groups before covariance matrix calculation.
+    Put all the P1D in the same k grid.
+
+    Arguments
+    ---------
+    weight_method: str,
+    Method to weight the data.
+
+    nbins_k (int):
+    Number of k bins.
+
+    zbin_edges (array-like):
+    All redshift bins.
+
+    izbin (int):
+    Current redshift bin being considered.
+
+    p1d_table (array-like):
+    Table of 1D power spectra, with columns 'Pk' and 'sub_forest_id'.
+
+    k_index (array-like):
+    Array of indices for k-values, with -1 indicating values outside of the k bins.
+
+    snrfit_table: numpy ndarray,
+    Table containing SNR fit infos
+
+    number_worker: int
+    Number of workers for the parallelization
+
+    Return
+    ------
+    p1d_weights  (array-like):
+    Weights associated with p1d pixels for all subforest, used in the calculation of covariance.
+
+    covariance_weights (array-like):
+    Weights for all subforest used inside the main covariance sum.
+
+    p1d_groups (array-like):
+    Individual p1d pixels grouped in the same wavenumber binning for all subforest
+    """
+
+    select_z = (p1d_table["forest_z"] < zbin_edges[izbin + 1]) & (
+        p1d_table["forest_z"] > zbin_edges[izbin]
+    )
+    p1d_sub_table = Table(
+        [
+            p1d_table["sub_forest_id"][select_z],
+            k_index[select_z],
+            p1d_table["Pk"][select_z],
+        ],
+        names=("sub_forest_id", "k_index", "pk"),
+    )
+    if weight_method == "fit_snr":
+        index_slice = mean_p1d_table_regular_slice(izbin, nbins_k)
+        snrfit_z = snrfit_table[index_slice[0] : index_slice[1], :]
+        p1d_sub_table["weight"] = 1 / fitfunc_variance_pk1d(
+            p1d_table["forest_snr"][select_z],
+            snrfit_z[k_index[select_z], 2],
+            snrfit_z[k_index[select_z], 3],
+        )
+    else:
+        p1d_sub_table["weight"] = 1
+
+    # Remove bins that are not associated with any wavenumber bin.
+    p1d_sub_table = p1d_sub_table[p1d_sub_table["k_index"] >= 0]
+
+    grouped_table = p1d_sub_table.group_by("sub_forest_id")
+    p1d_los_table = [
+        group[["k_index", "pk", "weight"]] for group in grouped_table.groups
+    ]
+
+    del grouped_table
+
+    func = partial(
+        compute_groups_for_one_forest,
+        nbins_k,
+    )
+    if number_worker == 1:
+        output_cov = [func(*p1d_los) for p1d_los in p1d_los_table]
+    else:
+        with Pool(number_worker) as pool:
+            output_cov = np.array(pool.map(func, p1d_los_table))
+
+    del p1d_los_table
+
+    p1d_weights, covariance_weights, p1d_groups = (
+        output_cov[:, 0, :],
+        output_cov[:, 1, :],
+        output_cov[:, 2, :],
+    )
+    return p1d_weights, covariance_weights, p1d_groups
+
+
+def compute_groups_for_one_forest(nbins_k, p1d_los):
+    """Compute the P1D groups for one subforest.
+
+    Arguments
+    ---------
+    nbins_k (int):
+    Number of k bins.
+
+    p1d_los (array-like):
+    Table containing all p1d pixels unordered for one subforest
+
+    Return
+    ------
+    p1d_weights_id  (array-like):
+    Weights associated with p1d pixels for one subforest, used in the calculation of covariance.
+
+    covariance_weights_id (array-like):
+    Weights for one subforest used inside the main covariance sum.
+
+    p1d_groups_id (array-like):
+    Individual p1d pixels grouped in the same wavenumber binning for one subforest
+    """
+    p1d_weights_id = np.zeros(nbins_k)
+    covariance_weights_id = np.zeros(nbins_k)
+    p1d_groups_id = np.zeros(nbins_k)
+
+    for ikbin in range(nbins_k):
+        mask_ikbin = p1d_los["k_index"] == ikbin
+        number_in_bins = len(mask_ikbin[mask_ikbin])
+        if number_in_bins != 0:
+            weight = p1d_los["weight"][mask_ikbin][0]
+            p1d_weights_id[ikbin] = weight
+            covariance_weights_id[ikbin] = np.sqrt(weight / number_in_bins)
+            p1d_groups_id[ikbin] = np.nansum(
+                p1d_los["pk"][mask_ikbin] * covariance_weights_id[ikbin]
+            )
+    return p1d_weights_id, covariance_weights_id, p1d_groups_id
+
+
 def compute_cov(
+    weight_method,
+    nbins_k,
+    mean_pk,
+    error_pk,
+    p1d_weights,
+    covariance_weights,
+    p1d_groups,
+):
+    """Compute the covariance of a set of 1D power spectra.
+    This is a new version of the covariance calculation.
+    It needs that the input data are expressed on the same
+    wavenumber grid. The calculation is then performed in
+    an entire vectorized way.
+
+    Arguments
+    ---------
+    weight_method: str,
+    Method to weight the data.
+
+    nbins_k (int):
+    Number of k bins.
+
+    mean_pk (array-like):
+    Mean 1D power spectra, for the considered redshift bin.
+
+    error_pk (array-like):
+    Standard deviation of the 1D power spectra, for the considered redshift bin.
+
+    p1d_weights  (array-like):
+    Weights associated with p1d pixels for all subforest, used in the calculation of covariance.
+
+    covariance_weights (array-like):
+    Weights for all subforest used inside the main covariance sum.
+
+    p1d_groups (array-like):
+    Individual p1d pixels grouped in the same wavenumber binning for all subforest
+
+    Return
+    ------
+    covariance_array (array-like):
+    Array of covariance coefficients.
+    """
+
+    if len(p1d_groups) == 0:
+        return np.full(nbins_k * nbins_k, np.nan)
+
+    mean_pk_product = np.outer(mean_pk, mean_pk)
+
+    sum_p1d_weights = np.nansum(p1d_weights, axis=0)
+    weights_sum_of_product = np.outer(sum_p1d_weights, sum_p1d_weights)
+
+    p1d_groups_product_sum = np.zeros((nbins_k, nbins_k))
+    covariance_weights_product_of_sum = np.zeros((nbins_k, nbins_k))
+    weights_product_of_sum = np.zeros((nbins_k, nbins_k))
+
+    for i, p1d_group in enumerate(p1d_groups):
+        # The summation is done with np.nansum instead of simple addition to not
+        # include the NaN that are present in the individual p1d.
+        # The summation is not done at the end, to prevent memory overhead.
+        p1d_groups_product_sum = np.nansum(
+            [p1d_groups_product_sum, np.outer(p1d_group, p1d_group)], axis=0
+        )
+        covariance_weights_product_of_sum = np.nansum(
+            [
+                covariance_weights_product_of_sum,
+                np.outer(covariance_weights[i], covariance_weights[i]),
+            ],
+            axis=0,
+        )
+        weights_product_of_sum = np.nansum(
+            [weights_product_of_sum, np.outer(p1d_weights[i], p1d_weights[i])], axis=0
+        )
+
+    del p1d_groups, covariance_weights, p1d_weights
+
+    covariance_matrix = (weights_product_of_sum / weights_sum_of_product) * (
+        (p1d_groups_product_sum / covariance_weights_product_of_sum) - mean_pk_product
+    )
+
+    # For fit_snr method, due to the SNR fitting scheme used for weighting,
+    # the diagonal of the weigthed sample covariance matrix is not equal
+    # to the error in mean P1D. This is tested on Ohio mocks and data.
+    # We choose to renormalize the whole covariance matrix.
+    if weight_method == "fit_snr":
+        covariance_diag = np.diag(covariance_matrix)
+        covariance_matrix = (
+            covariance_matrix
+            * np.outer(error_pk, error_pk)
+            / np.sqrt(np.outer(covariance_diag, covariance_diag))
+        )
+
+    covariance_array = np.ravel(covariance_matrix)
+
+    return covariance_array
+
+
+def compute_cov_not_vectorized(
     p1d_table,
     mean_p1d_table,
-    zbin_centers,
     n_chunks,
-    kbin_edges,
     k_index,
     nbins_k,
     weight_method,
@@ -869,6 +1259,8 @@ def compute_cov(
     sub_forest_ids,
 ):
     """Compute the covariance of a set of 1D power spectra.
+    This is an old implementation which is summing up each individual mode every time.
+    This is very slow for large data samples.
 
     Arguments
     ---------
@@ -878,9 +1270,6 @@ def compute_cov(
     mean_p1d_table (array-like):
     Table of mean 1D power spectra, with column 'meanPk'.
 
-    zbin_centers (array-like):
-    Array of bin centers for redshift.
-
     n_chunks (array-like):
     Array of the number of chunks in each redshift bin.
 
@@ -907,43 +1296,18 @@ def compute_cov(
 
     Return
     ------
-    zbin_array (array-like):
-    Array of redshift bin centers for each covariance coefficient.
-
-    index_zbin_array (array-like):
-    Array of redshift bin indices for each covariance coefficient.
-
-    n_array (array-like):
-    Array of the number of power spectra used to compute each covariance coefficient.
-
     covariance_array (array-like):
     Array of covariance coefficients.
     """
-    zbin_array = np.zeros(nbins_k * nbins_k)
-    index_zbin_array = np.zeros(nbins_k * nbins_k, dtype=int)
     n_array = np.zeros(nbins_k * nbins_k)
     covariance_array = np.zeros(nbins_k * nbins_k)
-    k1_array = np.zeros(nbins_k * nbins_k)
-    k2_array = np.zeros(nbins_k * nbins_k)
     if weight_method == "fit_snr":
         weight_array = np.zeros(nbins_k * nbins_k)
 
-    kbin_centers = (kbin_edges[1:] + kbin_edges[:-1]) / 2
     if n_chunks[izbin] == 0:  # Fill rows with NaNs
-        zbin_array[:] = zbin_centers[izbin]
-        index_zbin_array[:] = izbin
-        n_array[:] = 0
         covariance_array[:] = np.nan
-        k1_array[:] = np.nan
-        k2_array[:] = np.nan
-        return (
-            zbin_array,
-            index_zbin_array,
-            n_array,
-            covariance_array,
-            k1_array,
-            k2_array,
-        )
+        return covariance_array
+
     # First loop 1) id sub-forest bins
     for sub_forest_id in sub_forest_ids:
         select_id = select_z & (p1d_table["sub_forest_id"] == sub_forest_id)
@@ -957,7 +1321,8 @@ def compute_cov(
             # adapted with weights w_i = np.sqrt(w_ipk * w_ipk2) for covariance
             # to obtain the right definition of variance on the diagonal
             selected_snr = p1d_table["forest_snr"][select_id]
-            snrfit_z = snrfit_table[izbin * nbins_k : (izbin + 1) * nbins_k, :]
+            index_slice = mean_p1d_table_regular_slice(izbin, nbins_k)
+            snrfit_z = snrfit_table[index_slice[0] : index_slice[1], :]
             selected_variance_estimated = fitfunc_variance_pk1d(
                 selected_snr, snrfit_z[selected_ikbin, 2], snrfit_z[selected_ikbin, 3]
             )
@@ -1017,10 +1382,6 @@ def compute_cov(
             # weight_array[index] if weights are normalized to give
             # sum(weight_array[index]) = n_array[index]
             covariance_array[index] = covariance_array[index] / n_array[index]
-            zbin_array[index] = zbin_centers[izbin]
-            index_zbin_array[index] = izbin
-            k1_array[index] = kbin_centers[ikbin]
-            k2_array[index] = kbin_centers[ikbin2]
 
             if ikbin2 != ikbin:
                 # index of the (ikbin,ikbin2) coefficient on the bottom of the matrix
@@ -1028,11 +1389,6 @@ def compute_cov(
                 covariance_array[index_2] = covariance_array[index]
                 n_array[index_2] = n_array[index]
 
-                zbin_array[index_2] = zbin_centers[izbin]
-                index_zbin_array[index_2] = izbin
-                k1_array[index_2] = kbin_centers[ikbin2]
-                k2_array[index_2] = kbin_centers[ikbin]
-
     # For fit_snr method, due to the SNR fitting scheme used for weighting,
     # the diagonal of the weigthed sample covariance matrix is not equal
     # to the error in mean P1D. This is tested on Ohio mocks.
@@ -1068,7 +1424,7 @@ def compute_cov(
                         )
                     )
 
-    return zbin_array, index_zbin_array, n_array, covariance_array, k1_array, k2_array
+    return covariance_array
 
 
 def run_postproc_pk1d(
@@ -1079,6 +1435,7 @@ def run_postproc_pk1d(
     weight_method="no_weights",
     apply_z_weights=False,
     snrcut=None,
+    skymask_matrices=None,
     zbins_snrcut=None,
     output_snrfit=None,
     nomedians=False,
@@ -1117,7 +1474,7 @@ def run_postproc_pk1d(
 
     with Pool(ncpu) as pool:
         output_readpk1d = pool.starmap(
-            read_pk1d, [[f, kbin_edges, snrcut, zbins_snrcut] for f in files]
+            read_pk1d, [[f, kbin_edges, snrcut, zbins_snrcut, skymask_matrices] for f in files]
         )
 
     output_readpk1d = [x for x in output_readpk1d if x is not None]
@@ -1141,13 +1498,297 @@ def run_postproc_pk1d(
         compute_covariance=compute_covariance,
         compute_bootstrap=compute_bootstrap,
         number_bootstrap=number_bootstrap,
+        number_worker=ncpu,
+    )
+
+    metadata_header = {
+        "VELUNITS": velunits,
+        "NQSO": len(np.unique(p1d_table["forest_id"])),
+    }
+
+    write_mean_pk1d(
+        output_file,
+        mean_p1d_table,
+        metadata_table,
+        metadata_header,
+        cov_table,
     )
 
+
+def check_mean_pk1d_compatibility(
+    mean_p1d_tables,
+    metadata_tables,
+    metadata_headers,
+    cov_tables,
+):
+    """
+    Check the compatibility between different average p1d
+
+    Arguments
+    ---------
+    mean_p1d_tables: array-like
+    List of p1d tables to average
+
+    metadata_tables: array-like
+    List of metadata table of each mean p1d
+
+    metadata_headers: array-like
+    List of metadata header of each mean p1d
+
+    mean_p1d_tables: array-like
+    List of covariance tables to average
+    """
+
+    if len(mean_p1d_tables) <= 1:
+        raise ValueError(
+            """Not enough mean p1d data to coadd (0 or 1),"""
+            """ Please check the input data"""
+        )
+    if (len(mean_p1d_tables) != len(metadata_tables)) | (
+        len(mean_p1d_tables) != len(metadata_headers)
+    ):
+        raise ValueError(
+            """The number of mean p1d data to coadd is different,"""
+            """ Please check the input data"""
+        )
+    if (len(mean_p1d_tables) != len(cov_tables)) & (len(cov_tables) != 0):
+        raise ValueError(
+            """The input mean p1d data does not all contains covariance,"""
+            """ Please check the input data"""
+        )
+
+    z_min = metadata_tables[-1]["z_min"]
+    z_max = metadata_tables[-1]["z_max"]
+    k_min = metadata_tables[-1]["k_min"]
+    k_max = metadata_tables[-1]["k_max"]
+    k_bin_size = len(mean_p1d_tables[-1]["meank"])
+    for i, metadata in enumerate(metadata_tables[:-1]):
+        test = np.all(metadata["z_min"] == z_min)
+        test &= np.all(metadata["z_max"] == z_max)
+        test &= np.all(metadata["k_min"] == k_min)
+        test &= np.all(metadata["k_max"] == k_max)
+        test &= len(mean_p1d_tables[i]["meank"]) == k_bin_size
+
+        if not test:
+            raise ValueError(
+                """The input mean p1d data does not have the same k and z binning,"""
+                """ Please check the input data"""
+            )
+
+    velunits = metadata_headers[-1]["VELUNITS"]
+    for _, header in enumerate(metadata_headers[:-1]):
+        if header["VELUNITS"] != velunits:
+            raise ValueError(
+                """The input mean p1d data are not expressed in the same units,"""
+                """ Please check the input data"""
+            )
+
+    mean_p1d_table_colnames = mean_p1d_tables[-1].colnames
+    for i, table in enumerate(mean_p1d_tables[:-1]):
+        if not np.all(table.colnames == mean_p1d_table_colnames):
+            raise ValueError(
+                """The mean p1d tables does not have the same column names,"""
+                """ Please check the input data"""
+            )
+    cov_table_colnames = cov_tables[-1].colnames
+    for i, table in enumerate(cov_tables[:-1]):
+        if not np.all(table.colnames == cov_table_colnames):
+            raise ValueError(
+                """The covariance tables does not have the same column names,"""
+                """ Please check the input data"""
+            )
+
+
+def average_mean_pk1d_files(
+    mean_p1d_names,
+    output_path,
+    weighted_mean=False,
+):
+    """
+    Compute and write the average of mean p1d
+
+    Arguments
+    ---------
+    mean_p1d_names: array-like
+    List of the name of p1d means to average
+
+    output_path: str
+    Output path where to write
+
+    weighted_mean: bool
+    If True, compute the weighted average using the errors as weights.
+    """
+
+    mean_p1d_tables, metadata_tables, metadata_headers, cov_tables = (
+        [],
+        [],
+        [],
+        [],
+    )
+
+    for mean_p1d_name in mean_p1d_names:
+        hdus = fitsio.FITS(mean_p1d_name)
+        metadata_tables.append(Table(hdus[2].read()))
+        metadata_headers.append(hdus[2].read_header())
+        mean_p1d_tables.append(Table(hdus[1].read()))
+        if len(hdus) > 3:
+            cov_tables.append(Table(hdus[3].read()))
+
+    check_mean_pk1d_compatibility(
+        mean_p1d_tables, metadata_tables, metadata_headers, cov_tables
+    )
+
+    combination_metadata_header = {
+        "VELUNITS": metadata_headers[0]["VELUNITS"],
+        "NQSO": np.sum([header["NQSO"] for header in metadata_headers]),
+    }
+
+    combination_metadata_table = metadata_tables[0][
+        ["z_min", "z_max", "k_min", "k_max"]
+    ]
+    combination_metadata_table["N_chunks"] = np.sum(
+        np.array([metadata["N_chunks"] for metadata in metadata_tables]), axis=0
+    )
+
+    combination_mean_p1d_table = mean_p1d_tables[0][["zbin", "index_zbin"]]
+    combination_mean_p1d_table["N"] = np.sum(
+        np.array([mean_p1d_table["N"] for mean_p1d_table in mean_p1d_tables]), axis=0
+    )
+    for colname in mean_p1d_tables[0].colnames:
+        all_mean_p1d_colname = np.array(
+            [mean_p1d[colname] for mean_p1d in mean_p1d_tables]
+        )
+        masked_all_mean_p1d_colname = np.ma.masked_array(
+            all_mean_p1d_colname, np.isnan(all_mean_p1d_colname)
+        )
+        if ("mean" in colname) | ("median" in colname):
+            if weighted_mean:
+                weight_colname = (
+                    colname.replace("mean", "error")
+                    if "mean" in colname
+                    else colname.replace("median", "error")
+                )
+                weights = (
+                    1
+                    / (
+                        np.array(
+                            [mean_p1d[weight_colname] for mean_p1d in mean_p1d_tables]
+                        )
+                    )
+                    ** 2
+                )
+                new_mean_p1d_colname = np.ma.average(
+                    masked_all_mean_p1d_colname, axis=0, weights=weights
+                ).filled(np.nan)
+            else:
+                new_mean_p1d_colname = np.ma.average(
+                    masked_all_mean_p1d_colname, axis=0
+                ).filled(np.nan)
+        elif "error" in colname:
+            new_mean_p1d_colname = np.ma.average(
+                masked_all_mean_p1d_colname, axis=0
+            ).filled(np.nan) / np.sqrt(len(mean_p1d_tables))
+        elif "min" in colname:
+            new_mean_p1d_colname = np.ma.min(
+                masked_all_mean_p1d_colname, axis=0
+            ).filled(np.nan)
+        elif "max" in colname:
+            new_mean_p1d_colname = np.ma.max(
+                masked_all_mean_p1d_colname, axis=0
+            ).filled(np.nan)
+        else:
+            continue
+        combination_mean_p1d_table[colname] = new_mean_p1d_colname
+
+    combination_cov_table = cov_tables[0][["zbin", "index_zbin"]]
+    combination_cov_table["N"] = np.sum(
+        np.array([cov_table["N"] for cov_table in cov_tables]),
+        axis=0,
+    )
+
+    if len(cov_tables) == 0:
+        combination_cov_table = None
+    else:
+        all_covariance = np.array([cov_table["covariance"] for cov_table in cov_tables])
+        masked_all_covariance = np.ma.masked_array(
+            all_covariance, np.isnan(all_covariance)
+        )
+        combination_cov_table["covariance"] = np.ma.average(
+            masked_all_covariance, axis=0
+        ).filled(np.nan)
+        combination_cov_table["k1"] = cov_tables[0]["k1"]
+        combination_cov_table["k2"] = cov_tables[0]["k2"]
+
+        if "boot_covariance" in cov_tables[0].colnames:
+            all_boot_covariance = np.array(
+                [cov_table["boot_covariance"] for cov_table in cov_tables]
+            )
+            all_error_boot_covariance = np.array(
+                [cov_table["error_boot_covariance"] for cov_table in cov_tables]
+            )
+
+            masked_all_boot_covariance = np.ma.masked_array(
+                all_boot_covariance, np.isnan(all_boot_covariance)
+            )
+            masked_all_error_boot_covariance = np.ma.masked_array(
+                all_error_boot_covariance, np.isnan(all_error_boot_covariance)
+            )
+
+            if weighted_mean:
+                weights = 1 / masked_all_error_boot_covariance**2
+                combination_cov_table["boot_covariance"] = np.ma.average(
+                    masked_all_boot_covariance, axis=0, weights=weights
+                ).filled(np.nan)
+            else:
+                combination_cov_table["boot_covariance"] = np.ma.average(
+                    masked_all_boot_covariance, axis=0
+                ).filled(np.nan)
+            combination_cov_table["error_boot_covariance"] = np.ma.average(
+                masked_all_error_boot_covariance, axis=0
+            ).filled(np.nan) / np.sqrt(len(cov_tables))
+
+    output_file = os.path.join(output_path, mean_p1d_names[0].split("/")[-1])
+    write_mean_pk1d(
+        output_file,
+        combination_mean_p1d_table,
+        combination_metadata_table,
+        combination_metadata_header,
+        combination_cov_table,
+    )
+
+
+def write_mean_pk1d(
+    output_file,
+    mean_p1d_table,
+    metadata_table,
+    metadata_header,
+    cov_table,
+):
+    """
+    Write a mean p1d file
+
+    Arguments
+    ---------
+    output_file: string
+    Output file name
+
+    mean_p1d_table: Table
+    Table containing the mean p1d properties
+
+    metadata_table: Table
+    Table containing the metadata
+
+    metadata_header: Table
+    Table containing the metadata header to write
+
+    cov_table: Table
+    Table containing the covariance of the mean p1d
+    """
     result = fitsio.FITS(output_file, "rw", clobber=True)
     result.write(mean_p1d_table.as_array())
     result.write(
         metadata_table.as_array(),
-        header={"VELUNITS": velunits, "NQSO": len(np.unique(p1d_table["forest_id"]))},
+        header=metadata_header,
     )
     if cov_table is not None:
         result.write(cov_table.as_array())
diff --git a/py/picca/pk1d/prep_pk1d.py b/py/picca/pk1d/prep_pk1d.py
index dddbcb45f..05f5fa317 100644
--- a/py/picca/pk1d/prep_pk1d.py
+++ b/py/picca/pk1d/prep_pk1d.py
@@ -147,14 +147,13 @@ def exp_diff_desi(
     mask_even = ivar_tot_even > 0
     flux_tot_even[mask_even] /= ivar_tot_even[mask_even]
 
+    alpha = 1
     if method_alpha == "eboss":
-        alpha = 1
         if num_exp % 2 == 1:
             n_even = num_exp // 2
             alpha = np.sqrt(4.0 * n_even * (n_even + 1)) / num_exp
 
     elif method_alpha == "eboss_corr":
-        alpha = 1
         if num_exp % 2 == 1:
             n_even = num_exp // 2
             alpha = np.sqrt((2 * n_even) / (num_exp))
@@ -181,6 +180,9 @@ def exp_diff_desi(
             (time_odd * time_even) / (time_exp * (time_odd + time_even))
         )
 
+    else:
+        raise ValueError(f"Unknown method_alpha: {method_alpha}")
+
     diff = 0.5 * (flux_tot_even - flux_tot_odd) * alpha
     return diff
 
diff --git a/py/picca/pk1d/utils.py b/py/picca/pk1d/utils.py
index 6641a4da6..bba4d6e5c 100644
--- a/py/picca/pk1d/utils.py
+++ b/py/picca/pk1d/utils.py
@@ -2,7 +2,8 @@
 pk1d package"""
 
 import numpy as np
-from picca.delta_extraction.utils import SPEED_LIGHT  # pylint: disable=unused-import
+from picca import constants
+from picca.utils import userprint
 
 DEFAULT_K_MIN_VEL = 0.000813
 DEFAULT_K_BIN_VEL = 0.000542
@@ -16,6 +17,11 @@
 # used in mean Pk computation when weight_method=='fit_snr':
 MEANPK_FITRANGE_SNR = [1, 10]
 
+# Wavelength grid to estimate skyline masks:
+# default values from eg. desispec/scripts/proc.py
+DEFAULT_MINWAVE_SKYMASK = 3600.0
+DEFAULT_MAXWAVE_SKYMASK = 9824.0
+DEFAULT_DWAVE_SKYMASK = 0.8
 
 
 def fitfunc_variance_pk1d(snr, amp, zero_point):
@@ -37,3 +43,71 @@ def fitfunc_variance_pk1d(snr, amp, zero_point):
     float: The variance of the signal.
     """
     return (amp / (snr - 1)**2) + zero_point
+
+
+def skyline_mask_matrices_desi(z_parts, skyline_mask_file,
+                               minwave=DEFAULT_DWAVE_SKYMASK,
+                               maxwave=DEFAULT_MAXWAVE_SKYMASK,
+                               dwave=DEFAULT_DWAVE_SKYMASK):
+    """ Compute matrices to correct for the masking effect on FFTs,
+    when chunks are defined with the `--parts-in-redshift` option in picca_Pk1D.
+    Only implemented for DESI data, ie delta lambda = 0.8 and no rebinning in the deltas.
+
+    Arguments
+    ---------
+    z_parts: list
+    Edges of redshift chunks
+
+    skyline_mask_file: str
+    Name of file containing the list of skyline masks
+
+    minwave, maxwave, dwave: float
+    Parameter defining the wavelength grid used for the skymask. Should be consistent with
+    arguments lambda min, lambda max, delta lambda used in picca_delta_extraction.py.
+
+    Return
+    ------
+    A list with N items [meanz, matrix] where N = len(z_parts)-1 is the number of redshift chunks.
+    Each matrix is the inverse of, symbolically, M_ij ~ |M_{i-j}|^2 where M_k is the
+    Fourier transform of the mask function associated to skylines. Within the current
+    implementation, the matrix sizes are dictated by the DESI wavelength binning.
+    """
+    skyline_list = np.genfromtxt(skyline_mask_file,
+                                 names=('type', 'wave_min', 'wave_max', 'frame'))
+    ref_wavegrid = np.arange(minwave, maxwave, dwave)
+    num_parts = len(z_parts)-1
+    out = []
+
+    for iz in range(num_parts):
+        lmin = constants.ABSORBER_IGM['LYA'] * (1+z_parts[iz])
+        lmax = constants.ABSORBER_IGM['LYA'] * (1+z_parts[iz+1])
+        # - the following selection is identical to compute_pk1d.split_forest_in_z_parts:
+        wave = ref_wavegrid[ (ref_wavegrid>=lmin) & (ref_wavegrid<lmax) ]
+        npts = len(wave)
+        skymask = np.ones(npts)
+        selection = ( (skyline_list['wave_min']<=lmax) & (skyline_list['wave_min']>=lmin)
+                    ) | ( (skyline_list['wave_max']<=lmax) & (skyline_list['wave_max']>=lmin) )
+        for skyline in skyline_list[selection]:
+            skymask[(wave>=skyline['wave_min']) & (wave<=skyline['wave_max'])] = 0
+        skymask_tilde = np.fft.fft(skymask)/npts
+        mask_matrix = np.zeros((npts, npts))
+        for j in range(npts):
+            for l in range(npts):
+                index_mask = j-l if j>=l else j-l+npts
+                mask_matrix[j, l] = (
+                    skymask_tilde[index_mask].real ** 2
+                    + skymask_tilde[index_mask].imag ** 2
+                )
+        try:
+            inv_matrix = np.linalg.inv(mask_matrix)
+        except np.linalg.LinAlgError:
+            userprint(
+                """Warning: cannot invert sky mask matrix """
+                f"""for z bin {z_parts[iz]} - {z_parts[iz+1]}"""
+            )
+            userprint("No correction will be applied for this bin")
+            inv_matrix = np.eye(npts)
+        meanz = (z_parts[iz]+z_parts[iz+1])/2
+        out.append([meanz, inv_matrix])
+
+    return out
diff --git a/py/picca/raw_io.py b/py/picca/raw_io.py
index 9f34d8f0f..e3a7866dd 100644
--- a/py/picca/raw_io.py
+++ b/py/picca/raw_io.py
@@ -217,6 +217,7 @@ def write_delta_from_transmission(deltas, mean_flux, flux_variance, healpix, out
         header['FIBERID'] = delta.fiberid
         header['ORDER'] = delta.order
         header['WAVE_SOLUTION'] = 'lin' if lin_spaced else 'log'
+        header['DELTA_LAMBDA'] = delta_x
 
         cols = [
             delta.log_lambda, delta.delta, delta.weights,
@@ -481,8 +482,8 @@ def convert_transmission_to_deltas(obj_path, out_dir, in_dir=None, in_filenames=
     header['L_MAX'] = lambda_max
     header['LR_MIN'] = lambda_min_rest_frame
     header['LR_MAX'] = lambda_max_rest_frame
-    header['DEL_LL'] = delta_log_lambda
-    header['DEL_L'] = delta_lambda
+    header['DEL_LL'] = delta_x if not lin_spaced else None
+    header['DEL_L'] = delta_x if lin_spaced else None
     header['LINEAR'] = lin_spaced
     results.write(cols, names=names, header=header, extname='STATS')
     results.close()
diff --git a/py/picca/xcf.py b/py/picca/xcf.py
index 8973a2bda..cba6a0362 100644
--- a/py/picca/xcf.py
+++ b/py/picca/xcf.py
@@ -52,6 +52,9 @@
 cosmo = None
 ang_correlation = False
 
+# variables for distortion matrix
+redshift_evolution_in_distortion_matrix = True
+
 # variables used in the wick covariance matrix computation
 get_variance_1d = {}
 xi_1d = {}
@@ -386,6 +389,15 @@ def compute_dmat_forest_pairs_fast(log_lambda1, r_comov1, dist_m1, z1, weights1,
             weights12 = weights1[i] * weights2[j]
             z = (z1[i] + z2[j]) / 2
 
+            # this scale factor applies to all of the eta terms
+            # it depends on i and j so it cannot be factored out
+            # HERE WE ARE ASSUMING THAT ALPHA_OBJ IS FOR Z2 AND NOT Z1
+            # (see call in xcf.compute_dmat)
+            if redshift_evolution_in_distortion_matrix :
+                zfac = (((1+z1[i])/(1+z_ref))**(alpha-1)) * (((1+z2[j])/(1+z_ref))**(alpha_obj-1))
+            else :
+                zfac = 1
+
             bins_r_par = np.floor(
                 (r_par - r_par_min) / (r_par_max - r_par_min) * num_bins_r_par)
             bins_r_trans = np.floor(r_trans / r_trans_max * num_bins_r_trans)
@@ -419,14 +431,14 @@ def compute_dmat_forest_pairs_fast(log_lambda1, r_comov1, dist_m1, z1, weights1,
 
             # first eta, second term: weight/sum(weights)
             # second eta, first term: kronecker delta
-            eta2[j + num_pixels2 * model_bins] += weights1[i] / sum_weights1
+            eta2[j + num_pixels2 * model_bins] +=  zfac *weights1[i] / sum_weights1
 
             if order1 == 1:
                 # first eta, third term: (non-zero only for order=1)
                 #   weight*(Lambda-bar(Lambda))*(Lambda-bar(Lambda))/
                 #   sum(weight*(Lambda-bar(Lambda)**2))
                 # second eta, first term: kronecker delta
-                eta4[j + num_pixels2 * model_bins] += (
+                eta4[j + num_pixels2 * model_bins] +=  zfac *(
                     weights1[i] * log_lambda_minus_mean1[i] /
                     sum_weights_square_log_lambda_minus_mean1)
 
@@ -441,7 +453,17 @@ def compute_dmat_forest_pairs_fast(log_lambda1, r_comov1, dist_m1, z1, weights1,
         # first eta, first term: kronecker delta
         # second eta, first term: kronecker delta
         dmat_bin = model_bins + num_model_bins_r_par * num_model_bins_r_trans * bins
-        dmat[dmat_bin] += weights12
+
+
+        # this scale factor was applied to all of the eta terms above
+        # but not yet to the first term
+        # HERE WE ARE ASSUMING THAT ALPHA_OBJ IS FOR Z2 AND NOT Z1
+        # (see call in xcf.compute_dmat)
+        if redshift_evolution_in_distortion_matrix :
+            zfac = (((1+z1[i])/(1+z_ref))**(alpha-1)) * (((1+z2[j])/(1+z_ref))**(alpha_obj-1))
+        else :
+            zfac = 1
+        dmat[dmat_bin] +=  zfac *weights12
         # rest of the terms
         for k in unique_bins_model:
             dmat_bin = k + num_model_bins_r_par * num_model_bins_r_trans * bins
diff --git a/pylintrc b/pylintrc
index 7e550d1b6..a40654f48 100644
--- a/pylintrc
+++ b/pylintrc
@@ -502,6 +502,7 @@ valid-metaclass-classmethod-first-arg=cls
 
 # Maximum number of arguments for function / method.
 max-args=20
+max-positional-arguments=20
 
 # Maximum number of attributes for a class (see R0902).
 max-attributes=25
diff --git a/requirements.txt b/requirements.txt
index d14e69d05..d7503b970 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
-numpy>=1.20.3
-scipy>=1.6.3
-iminuit>=2.6.1
-healpy>=1.14.0
-fitsio>=1.1.4
-llvmlite>=0.36.0
-numba>=0.55.1
-h5py>=3.2.1
-future>=0.18.2
-setuptools>=56.2.0
-gitpython>=3.1.18
+numpy>=2.0.0
+scipy>=1.14.0
+iminuit>=2.26.0
+healpy>=1.17.1
+fitsio>=1.2.4
+llvmlite>=0.43.0
+numba>=0.60.0
+h5py>=3.11.0
+future>=1.0.0
+setuptools>=69.5.1
+gitpython>=3.1.43
diff --git a/setup.py b/setup.py
index ed8adbdbc..86a2f52a2 100644
--- a/setup.py
+++ b/setup.py
@@ -2,19 +2,25 @@
 
 import glob
 import git
+from git import InvalidGitRepositoryError
 
 from setuptools import find_namespace_packages, setup
 from pathlib import Path
 
 scripts = sorted(glob.glob('bin/picca*'))
 
-description = (f"Package for Igm Cosmological-Correlations Analyses\n"
-               f"commit hash: {git.Repo('.').head.object.hexsha}")
+exec(open('py/picca/_version.py').read())
+version = __version__
+
+try:
+    description = (f"Package for Igm Cosmological-Correlations Analyses\n"
+                   f"commit hash: {git.Repo('.').head.object.hexsha}")
+except InvalidGitRepositoryError:
+    description = (f"Package for Igm Cosmological-Correlations Analyses\n"
+                   f"version: {version}")
 this_directory = Path(__file__).parent
 long_description = (this_directory / "README.md").read_text()
 
-exec(open('py/picca/_version.py').read())
-version = __version__
 
 setup(name="picca",
     version = version,


Test patch:
diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml
index fe2f16bc7..1984d920d 100644
--- a/.github/workflows/publish-to-test-pypi.yml
+++ b/.github/workflows/publish-to-test-pypi.yml
@@ -9,7 +9,10 @@ jobs:
   build-n-publish:
     name: Build and publish Picca 📦 to PyPI and TestPyPI
     runs-on: ubuntu-latest
-
+    environment: Release
+    permissions:
+      id-token: write  # IMPORTANT: this permission is mandatory for trusted publishing
+      
     steps:
     - uses: actions/checkout@v3
     - name: Set up Python
@@ -34,11 +37,8 @@ jobs:
     - name: Publish distribution 📦 to Test PyPI
       uses: pypa/gh-action-pypi-publish@release/v1
       with:
-        password: ${{ secrets.TEST_PYPI_API_TOKEN }}
         repository-url: https://test.pypi.org/legacy/
         
     - name: Publish distribution 📦 to PyPI
       if: startsWith(github.ref, 'refs/tags')
       uses: pypa/gh-action-pypi-publish@release/v1
-      with:
-        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/py/picca/tests/data/test_Pk1D/Pk1D.fits.gz b/py/picca/tests/data/test_Pk1D/Pk1D.fits.gz
index bb8bb90b2..265e3fc8c 100644
Binary files a/py/picca/tests/data/test_Pk1D/Pk1D.fits.gz and b/py/picca/tests/data/test_Pk1D/Pk1D.fits.gz differ
diff --git a/py/picca/tests/data/test_Pk1D/Pk1D_raw.fits.gz b/py/picca/tests/data/test_Pk1D/Pk1D_raw.fits.gz
index 7dce515a9..49337848a 100644
Binary files a/py/picca/tests/data/test_Pk1D/Pk1D_raw.fits.gz and b/py/picca/tests/data/test_Pk1D/Pk1D_raw.fits.gz differ
diff --git a/py/picca/tests/data/test_Pk1D/meanPk1D_covariance.fits.gz b/py/picca/tests/data/test_Pk1D/meanPk1D_covariance.fits.gz
new file mode 100644
index 000000000..eb0595e03
Binary files /dev/null and b/py/picca/tests/data/test_Pk1D/meanPk1D_covariance.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/fast_metal_dmat.fits.gz b/py/picca/tests/data/test_cor/fast_metal_dmat.fits.gz
new file mode 100644
index 000000000..469b97a85
Binary files /dev/null and b/py/picca/tests/data/test_cor/fast_metal_dmat.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/fast_metal_dmat_cross.fits.gz b/py/picca/tests/data/test_cor/fast_metal_dmat_cross.fits.gz
new file mode 100644
index 000000000..c3c90a17e
Binary files /dev/null and b/py/picca/tests/data/test_cor/fast_metal_dmat_cross.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/fast_metal_dmat_cross_zcuts.fits.gz b/py/picca/tests/data/test_cor/fast_metal_dmat_cross_zcuts.fits.gz
new file mode 100644
index 000000000..ce54c5943
Binary files /dev/null and b/py/picca/tests/data/test_cor/fast_metal_dmat_cross_zcuts.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/fast_metal_dmat_zcuts.fits.gz b/py/picca/tests/data/test_cor/fast_metal_dmat_zcuts.fits.gz
new file mode 100644
index 000000000..d6abafc54
Binary files /dev/null and b/py/picca/tests/data/test_cor/fast_metal_dmat_zcuts.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/fast_metal_xdmat.fits.gz b/py/picca/tests/data/test_cor/fast_metal_xdmat.fits.gz
new file mode 100644
index 000000000..2bc3db8a5
Binary files /dev/null and b/py/picca/tests/data/test_cor/fast_metal_xdmat.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-28.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-28.fits.gz
new file mode 100644
index 000000000..60a30da0c
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-45.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-45.fits.gz
new file mode 100644
index 000000000..7a15d39d5
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz
new file mode 100644
index 000000000..b4b0caa3c
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration1.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration1.fits.gz
new file mode 100644
index 000000000..3a46fe90c
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration1.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration2.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration2.fits.gz
new file mode 100644
index 000000000..a9e5003e5
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration2.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration3.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration3.fits.gz
new file mode 100644
index 000000000..d6b761058
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration3.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration4.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration4.fits.gz
new file mode 100644
index 000000000..9daf39787
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration4.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration5.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration5.fits.gz
new file mode 100644
index 000000000..c7a7d2571
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration5.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration6.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration6.fits.gz
new file mode 100644
index 000000000..b064dafd9
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration6.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration7.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration7.fits.gz
new file mode 100644
index 000000000..e071bb4cc
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration7.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration8.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration8.fits.gz
new file mode 100644
index 000000000..a35dbfe8c
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration8.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration9.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration9.fits.gz
new file mode 100644
index 000000000..2b1e1fc54
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes_iteration9.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/rejection_log.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/rejection_log.fits.gz
new file mode 100644
index 000000000..8837316ee
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/Log/rejection_log.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib1_delta_attributes.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib1_delta_attributes.fits.gz
new file mode 100644
index 000000000..5ea6a87f4
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib1_delta_attributes.fits.gz differ
diff --git a/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib2_delta_attributes.fits.gz b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib2_delta_attributes.fits.gz
new file mode 100644
index 000000000..876113428
Binary files /dev/null and b/py/picca/tests/data/test_cor/input_from_delta_extraction_lya_nodla/calib2_delta_attributes.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/astronomical_object_tests.py b/py/picca/tests/delta_extraction/astronomical_object_tests.py
index b02228b1f..1f3858eba 100644
--- a/py/picca/tests/delta_extraction/astronomical_object_tests.py
+++ b/py/picca/tests/delta_extraction/astronomical_object_tests.py
@@ -638,12 +638,13 @@ def assert_get_data(self, test_obj, blinding=False):
             comments[3] == ("Quasar continuum. "
                             "Check input spectra for units"))
 
+        self.assertTrue(names[4] == "IVAR")
+        self.assertTrue(np.allclose(cols[4], test_obj.ivar))
+        self.assertTrue(units[4] == "(Flux units)^-1")
+        self.assertTrue(
+            comments[4] == "Inverse variance. Check input spectra for units")
+        
         if isinstance(test_obj, Pk1dForest):
-            self.assertTrue(names[4] == "IVAR")
-            self.assertTrue(np.allclose(cols[4], test_obj.ivar))
-            self.assertTrue(units[4] == "(Flux units)^-1")
-            self.assertTrue(
-                comments[4] == "Inverse variance. Check input spectra for units")
 
             self.assertTrue(names[5] == "DIFF")
             self.assertTrue(np.allclose(cols[5], test_obj.exposures_diff))
diff --git a/py/picca/tests/delta_extraction/data/.config.ini b/py/picca/tests/delta_extraction/data/.config.ini
index 51531b00c..5dc273ffb 100644
--- a/py/picca/tests/delta_extraction/data/.config.ini
+++ b/py/picca/tests/delta_extraction/data/.config.ini
@@ -32,6 +32,7 @@ keep bal = False
 minimal snr pk1d = 1
 minimal snr bao3d = 0
 num processors = 1
+delta extraction single exposure = None
 
 [corrections]
 num corrections = 4
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-28.fits.gz
index d52c5fcc7..8d9d108ef 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-45.fits.gz
index 2b30767e3..9a1ca4767 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-28.fits.gz
index 63a8266f5..067b75e6c 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-45.fits.gz
index 1f52bf096..940691ef5 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib2/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-28.fits.gz
index ced52475e..6f53f2f07 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-45.fits.gz
index df4b38168..f91989000 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib2_nomask/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-28.fits.gz
index 1846dbcd0..fd93d45dd 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-45.fits.gz
index 06332d5e6..603a39f4e 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_calib_nomask/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-1.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-1.fits.gz
index 6bc83dd84..b55a962ea 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-1.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-1.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-50.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-50.fits.gz
index 6d06c3cec..7137e0b4d 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-50.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lin/Delta/delta-50.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Delta/delta-74.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Delta/delta-74.fits.gz
index 6d23ff885..b58a515b0 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Delta/delta-74.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Delta/delta-74.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes.fits.gz
index 88207c5ca..3a02ad531 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration1.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration1.fits.gz
index 6601636fe..cec41cbcc 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration1.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration1.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration2.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration2.fits.gz
index 54d619033..0f86468c0 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration2.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration2.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration3.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration3.fits.gz
index 74d05b689..d12941734 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration3.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration3.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration4.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration4.fits.gz
index da74f33c9..1d2668e03 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration4.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/delta_attributes_iteration4.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/rejection_log.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/rejection_log.fits.gz
index 98b3c91df..81163d3b1 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/rejection_log.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_desi_mocks/Log/rejection_log.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-28.fits.gz
index 60a30da0c..5ac42b28e 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-45.fits.gz
index 7a15d39d5..c89397dc9 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nodla/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-28.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-28.fits.gz
index 9429a9d28..6d276cf85 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-28.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-28.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-45.fits.gz b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-45.fits.gz
index d07802b99..2c2db0d46 100644
Binary files a/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-45.fits.gz and b/py/picca/tests/delta_extraction/data/delta_extraction_lya_nomask_nodla/Delta/delta-45.fits.gz differ
diff --git a/py/picca/tests/delta_extraction/data/delta_lin_fast.ini b/py/picca/tests/delta_extraction/data/delta_lin_fast.ini
new file mode 100644
index 000000000..04de3dae4
--- /dev/null
+++ b/py/picca/tests/delta_extraction/data/delta_lin_fast.ini
@@ -0,0 +1,13 @@
+[general]
+out dir = $THIS_DIR/results/delta_extraction_lin_fast/
+overwrite = True
+
+[data]
+type = DesiHealpixFast
+input directory = $THIS_DIR/data/
+save format = BinTableHDU
+catalogue = $THIS_DIR/data/QSO_cat_fuji_dark_healpix.fits.gz
+wave solution = lin
+
+[expected flux]
+type = Dr16ExpectedFlux
diff --git a/py/picca/tests/delta_extraction/data/delta_lin_image_fast.ini b/py/picca/tests/delta_extraction/data/delta_lin_image_fast.ini
new file mode 100644
index 000000000..ac4a98fe1
--- /dev/null
+++ b/py/picca/tests/delta_extraction/data/delta_lin_image_fast.ini
@@ -0,0 +1,13 @@
+[general]
+out dir = $THIS_DIR/results/delta_extraction_lin_image_fast/
+overwrite = True
+
+[data]
+type = DesiHealpixFast
+save format = ImageHDU
+input directory = $THIS_DIR/data/
+catalogue = $THIS_DIR/data/QSO_cat_fuji_dark_healpix.fits.gz
+wave solution = lin
+
+[expected flux]
+type = Dr16ExpectedFlux
diff --git a/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks.ini b/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks.ini
index 2f77370f9..efcc379bd 100644
--- a/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks.ini
+++ b/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks.ini
@@ -8,6 +8,8 @@ type = DesisimMocks
 save format = BinTableHDU
 catalogue = $THIS_DIR/data/desi_mock_test_catalogue.fits
 input directory = $THIS_DIR/data
+lambda min = 3599.9
+lambda max = 5499.9
 
 [corrections]
 num corrections = 0
diff --git a/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks_fast.ini b/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks_fast.ini
new file mode 100644
index 000000000..d185998c7
--- /dev/null
+++ b/py/picca/tests/delta_extraction/data/delta_lya_desi_mocks_fast.ini
@@ -0,0 +1,19 @@
+[general]
+overwrite = True
+out dir = $THIS_DIR/results/delta_extraction_lya_desi_mocks_fast/
+
+[data]
+wave solution = lin
+type = DesisimMocksFast
+save format = BinTableHDU
+catalogue = $THIS_DIR/data/desi_mock_test_catalogue.fits
+input directory = $THIS_DIR/data
+lambda min = 3599.9
+lambda max = 5499.9
+
+
+[corrections]
+num corrections = 0
+
+[expected flux]
+type = Dr16ExpectedFlux
diff --git a/py/picca/tests/delta_extraction/data/dla_mask_forest1_set_ivar.txt b/py/picca/tests/delta_extraction/data/dla_mask_forest1_set_ivar.txt
index 399595710..a0b671b4e 100644
--- a/py/picca/tests/delta_extraction/data/dla_mask_forest1_set_ivar.txt
+++ b/py/picca/tests/delta_extraction/data/dla_mask_forest1_set_ivar.txt
@@ -1,196 +1,196 @@
 # log_lambda flux ivar transmission_correction
-3.5617025007672907 1.0 4.0 0.9983934273144015
-3.562002500767291 1.0 4.0 0.9983605638947897
-3.562302500767291 1.0 4.0 0.9983266246179062
-3.5626025007672912 1.0 4.0 0.9982915627159972
-3.5629025007672914 1.0 4.0 0.9982553288159512
-3.5632025007672916 1.0 4.0 0.9982178707642664
-3.563502500767292 1.0 4.0 0.99817913343808
-3.563802500767292 1.0 4.0 0.9981390585409848
-3.564102500767292 1.0 4.0 0.998097584382216
-3.5644025007672924 1.0 4.0 0.9980546456376457
-3.5647025007672926 1.0 4.0 0.9980101730908558
-3.5650025007672927 1.0 4.0 0.9979640933523689
-3.565302500767293 1.0 4.0 0.9979163285549095
-3.565602500767293 1.0 4.0 0.9978667960223301
-3.5659025007672933 1.0 4.0 0.9978154079095705
-3.5662025007672935 1.0 4.0 0.9977620708107203
-3.5665025007672937 1.0 4.0 0.9977066853319153
-3.566802500767294 1.0 4.0 0.9976491456254168
-3.567102500767294 1.0 4.0 0.9975893388807927
-3.5674025007672943 1.0 4.0 0.9975271447686264
-3.5677025007672944 1.0 4.0 0.9974624348316273
-3.5680025007672946 1.0 4.0 0.9973950718173844
-3.568302500767295 1.0 4.0 0.9973249089462807
-3.568602500767295 1.0 4.0 0.997251789107275
-3.568902500767295 1.0 4.0 0.9971755439733104
-3.5692025007672954 1.0 4.0 0.9970959930270425
-3.5695025007672956 1.0 4.0 0.997012942486347
-3.5698025007672958 1.0 4.0 0.9969261841176511
-3.570102500767296 1.0 4.0 0.9968354939235109
-3.570402500767296 1.0 4.0 0.9967406306889699
-3.5707025007672963 1.0 4.0 0.9966413343690727
-3.5710025007672965 1.0 4.0 0.9965373242973861
-3.5713025007672967 1.0 4.0 0.9964282971924652
-3.571602500767297 1.0 4.0 0.9963139249358017
-3.571902500767297 1.0 4.0 0.9961938520908332
-3.5722025007672973 1.0 4.0 0.9960676931279583
-3.5725025007672975 1.0 4.0 0.9959350293150758
-3.5728025007672977 1.0 4.0 0.9957954052267899
-3.573102500767298 1.0 4.0 0.9956483248179061
-3.573402500767298 1.0 4.0 0.995493246997974
-3.5737025007672982 1.0 4.0 0.9953295806331155
-3.5740025007672984 1.0 4.0 0.9951566788888895
-3.5743025007672986 1.0 4.0 0.9949738328130615
-3.574602500767299 1.0 4.0 0.9947802640393597
-3.574902500767299 1.0 4.0 0.9945751164719614
-3.575202500767299 1.0 4.0 0.9943574467847958
-3.5755025007672994 1.0 4.0 0.9941262135387514
-3.5758025007672996 1.0 4.0 0.9938802646823368
-3.5761025007672997 1.0 4.0 0.993618323155666
-3.5764025007673 1.0 4.0 0.993338970261888
-3.5767025007673 1.0 4.0 0.993040626401817
-3.5770025007673003 1.0 4.0 0.9927215286833734
-3.5773025007673005 1.0 4.0 0.9923797048133908
-3.5776025007673007 1.0 4.0 0.9920129425501025
-3.577902500767301 1.0 4.0 0.9916187538333201
-3.578202500767301 1.0 4.0 0.9911943325069946
-3.5785025007673013 1.0 4.0 0.9907365042937747
-3.5788025007673014 1.0 4.0 0.9902416673578129
-3.5791025007673016 1.0 4.0 0.9897057213798168
-3.579402500767302 1.0 4.0 0.9891239825396025
-3.579702500767302 1.0 4.0 0.9884910811190275
-3.580002500767302 1.0 4.0 0.987800837551674
-3.5803025007673024 1.0 4.0 0.9870461115859807
-3.5806025007673026 1.0 4.0 0.9862186177004252
-3.5809025007673028 1.0 4.0 0.9853086978802165
-3.581202500767303 1.0 4.0 0.9843050401485963
-3.581502500767303 1.0 4.0 0.9831943275781823
-3.5818025007673033 1.0 4.0 0.9819607975105098
-3.5821025007673035 1.0 4.0 0.9805856838372642
-3.5824025007673037 1.0 4.0 0.9790465056422732
-3.582702500767304 1.0 4.0 0.9773161520797242
-3.583002500767304 1.0 4.0 0.9753616942854462
-3.5833025007673043 1.0 4.0 0.9731428276662394
-3.5836025007673045 1.0 4.0 0.970609807890364
-3.5839025007673047 1.0 4.0 0.9677006847303209
-3.584202500767305 1.0 4.0 0.9643375491020666
-3.584502500767305 1.0 4.0 0.9604213732059352
-3.5848025007673052 1.0 4.0 0.9558248135456833
-3.5851025007673054 1.0 4.0 0.9503820145728338
-3.5854025007673056 1.0 4.0 0.9438739157427193
-3.585702500767306 1.0 4.0 0.9360066849351789
-3.586002500767306 1.0 4.0 0.9263794227770391
-3.586302500767306 1.0 4.0 0.9144347433432132
-3.5866025007673064 1.0 4.0 0.8993813831228601
-3.5869025007673065 1.0 4.0 0.880070036927525
-3.5872025007673067 1.0 4.0 0.8547892835800044
-3.587502500767307 1.0 4.0 0.8209229628842137
-3.587802500767307 1.0 0.0 0.7743684712094552
-3.5881025007673073 1.0 0.0 0.7085681808087853
-3.5884025007673075 1.0 0.0 0.6130884741179058
-3.5887025007673077 1.0 0.0 0.47277559268032004
-3.589002500767308 1.0 0.0 0.27551842242117786
-3.589302500767308 1.0 0.0 0.06548982095723374
-3.5896025007673082 1.0 0.0 9.972368450158353e-05
+3.5617025007672907 1.0 4.0 0.9983933572831674
+3.562002500767291 1.0 4.0 0.9983604924321979
+3.562302500767291 1.0 4.0 0.9983265516771478
+3.5626025007672912 1.0 4.0 0.9982914882482307
+3.5629025007672914 1.0 4.0 0.9982552527701904
+3.5632025007672916 1.0 4.0 0.9982177930872593
+3.563502500767292 1.0 4.0 0.9981790540741805
+3.563802500767292 1.0 4.0 0.9981389774320146
+3.564102500767292 1.0 4.0 0.9980975014673176
+3.5644025007672924 1.0 4.0 0.998054560853125
+3.5647025007672926 1.0 4.0 0.9980100863700134
+3.5650025007672927 1.0 4.0 0.9979640046253192
+3.565302500767293 1.0 4.0 0.9979162377483866
+3.565602500767293 1.0 4.0 0.9978667030594794
+3.5659025007672933 1.0 4.0 0.9978153127097252
+3.5662025007672935 1.0 4.0 0.9977619732891603
+3.5665025007672937 1.0 4.0 0.9977065853996077
+3.566802500767294 1.0 4.0 0.997649043188737
+3.567102500767294 1.0 4.0 0.9975892338412227
+3.5674025007672943 1.0 4.0 0.9975270370224294
+3.5677025007672944 1.0 4.0 0.9974623242694964
+3.5680025007672946 1.0 4.0 0.9973949583240616
+3.568302500767295 1.0 4.0 0.9973247924001454
+3.568602500767295 1.0 4.0 0.9972516693798971
+3.568902500767295 1.0 4.0 0.9971754209289654
+3.5692025007672954 1.0 4.0 0.9970958665221851
+3.5695025007672956 1.0 4.0 0.9970128123690373
+3.5698025007672958 1.0 4.0 0.9969260502269314
+3.570102500767296 1.0 4.0 0.9968353560887242
+3.570402500767296 1.0 4.0 0.9967404887290173
+3.5707025007672963 1.0 4.0 0.9966411880916007
+3.5710025007672965 1.0 4.0 0.9965371734978973
+3.5713025007672967 1.0 4.0 0.9964281416533426
+3.571602500767297 1.0 4.0 0.9963137644252387
+3.571902500767297 1.0 4.0 0.9961936863616561
+3.5722025007672973 1.0 4.0 0.9960675219163304
+3.5725025007672975 1.0 4.0 0.9959348523390683
+3.5728025007672977 1.0 4.0 0.9957952221848024
+3.573102500767298 1.0 4.0 0.99564813538692
+3.573402500767298 1.0 4.0 0.9954930508316154
+3.5737025007672982 1.0 4.0 0.9953293773595056
+3.5740025007672984 1.0 4.0 0.9951564681082521
+3.5743025007672986 1.0 4.0 0.9949736140950576
+3.574602500767299 1.0 4.0 0.9947800369201096
+3.574902500767299 1.0 4.0 0.9945748804507136
+3.575202500767299 1.0 4.0 0.9943572013201902
+3.5755025007672994 1.0 4.0 0.9941259580446202
+3.5758025007672996 1.0 4.0 0.9938799985229735
+3.5761025007672997 1.0 4.0 0.9936180456404841
+3.5764025007673 1.0 4.0 0.9933386806393736
+3.5767025007673 1.0 4.0 0.9930403238526659
+3.5770025007673003 1.0 4.0 0.9927212123126813
+3.5773025007673005 1.0 4.0 0.9923793736417402
+3.5776025007673007 1.0 4.0 0.992012595503362
+3.577902500767301 1.0 4.0 0.9916183897309329
+3.578202500767301 1.0 4.0 0.9911939500484906
+3.5785025007673013 1.0 4.0 0.990736102043184
+3.5788025007673014 1.0 4.0 0.990241243725591
+3.5791025007673016 1.0 4.0 0.9897052746018072
+3.579402500767302 1.0 4.0 0.9891235106524577
+3.579702500767302 1.0 4.0 0.9884905819313736
+3.580002500767302 1.0 4.0 0.9878003086101398
+3.5803025007673024 1.0 4.0 0.9870455501349953
+3.5806025007673026 1.0 4.0 0.986218020634418
+3.5809025007673028 1.0 4.0 0.9853080616864974
+3.581202500767303 1.0 4.0 0.9843043608387426
+3.581502500767303 1.0 4.0 0.9831936006051574
+3.5818025007673033 1.0 4.0 0.981960017667957
+3.5821025007673035 1.0 4.0 0.9805848451363766
+3.5824025007673037 1.0 4.0 0.9790456011602463
+3.582702500767304 1.0 4.0 0.9773151737719198
+3.583002500767304 1.0 4.0 0.9753606327508301
+3.5833025007673043 1.0 4.0 0.9731416718521367
+3.5836025007673045 1.0 4.0 0.9706085447176791
+3.5839025007673047 1.0 4.0 0.9676992986135065
+3.584202500767305 1.0 4.0 0.9643360213283897
+3.584502500767305 1.0 4.0 0.9604196811246498
+3.5848025007673052 1.0 4.0 0.9558229294975433
+3.5851025007673054 1.0 4.0 0.9503799044609492
+3.5854025007673056 1.0 4.0 0.9438715371025004
+3.585702500767306 1.0 4.0 0.9360039842974132
+3.586002500767306 1.0 4.0 0.9263763320248989
+3.586302500767306 1.0 4.0 0.9144311746307181
+3.5866025007673064 1.0 4.0 0.8993772217440931
+3.5869025007673065 1.0 4.0 0.880065131330995
+3.5872025007673067 1.0 4.0 0.8547834316712566
+3.587502500767307 1.0 4.0 0.8209158944900299
+3.587802500767307 1.0 0.0 0.7743598304289859
+3.5881025007673073 1.0 0.0 0.7085575274499893
+3.5884025007673075 1.0 0.0 0.6130753815751792
+3.5887025007673077 1.0 0.0 0.4727601290049792
+3.589002500767308 1.0 0.0 0.27550290590110615
+3.589302500767308 1.0 0.0 0.06548200960102427
+3.5896025007673082 1.0 0.0 9.968318573651882e-05
 3.5899025007673084 1.0 0.0 0.0
-3.5902025007673086 1.0 0.0 2.1260805951434496e-10
-3.590502500767309 1.0 0.0 0.015581903607593423
-3.590802500767309 1.0 0.0 0.18227287451606158
-3.591102500767309 1.0 0.0 0.3984548855042793
-3.5914025007673094 1.0 0.0 0.5625935213491955
-3.5917025007673096 1.0 0.0 0.6748763082554043
-3.5920025007673098 1.0 0.0 0.7515193416260136
-3.59230250076731 1.0 4.0 0.8050588364756195
-3.59260250076731 1.0 4.0 0.8435198731350725
-3.5929025007673103 1.0 4.0 0.8719027087606961
-3.5932025007673105 1.0 4.0 0.893362932214748
-3.5935025007673107 1.0 4.0 0.9099404958915952
-3.593802500767311 1.0 4.0 0.9229894386348806
-3.594102500767311 1.0 4.0 0.933431867394047
-3.5944025007673113 1.0 4.0 0.9419108833998501
-3.5947025007673115 1.0 4.0 0.9488848584798114
-3.5950025007673116 1.0 4.0 0.9546869950396571
-3.595302500767312 1.0 4.0 0.9595638570645924
-3.595602500767312 1.0 4.0 0.9637008597147503
-3.595902500767312 1.0 4.0 0.9672394847425276
-3.5962025007673124 1.0 4.0 0.9702891320909308
-3.5965025007673126 1.0 4.0 0.9729354237385944
-3.596802500767313 1.0 4.0 0.975246116768427
-3.597102500767313 1.0 4.0 0.977275377230887
-3.597402500767313 1.0 4.0 0.9790669119722805
-3.5977025007673133 1.0 4.0 0.98065629292254
-3.5980025007673135 1.0 4.0 0.9820727024612204
-3.5983025007673137 1.0 4.0 0.983340258424714
-3.598602500767314 1.0 4.0 0.9844790302417742
-3.598902500767314 1.0 4.0 0.9855058255902966
-3.5992025007673143 1.0 4.0 0.9864348047903441
-3.5995025007673145 1.0 4.0 0.9872779646275796
-3.5998025007673147 1.0 4.0 0.9880455223096436
-3.600102500767315 1.0 4.0 0.9887462223866627
-3.600402500767315 1.0 4.0 0.9893875837708347
-3.6007025007673152 1.0 4.0 0.9899760998269836
-3.6010025007673154 1.0 4.0 0.9905174014350083
-3.6013025007673156 1.0 4.0 0.9910163906397801
-3.601602500767316 1.0 4.0 0.9914773507891375
-3.601902500767316 1.0 4.0 0.9919040377636116
-3.602202500767316 1.0 4.0 0.9922997559132362
-3.6025025007673164 1.0 4.0 0.9926674215583979
-3.6028025007673166 1.0 4.0 0.9930096163257877
-3.6031025007673168 1.0 4.0 0.9933286321349564
-3.603402500767317 1.0 4.0 0.9936265092946271
-3.603702500767317 1.0 4.0 0.9939050688875161
-3.6040025007673173 1.0 4.0 0.9941659404005675
-3.6043025007673175 1.0 4.0 0.9944105853810559
-3.6046025007673177 1.0 4.0 0.9946403177579336
-3.604902500767318 1.0 4.0 0.9948563213544857
-3.605202500767318 1.0 4.0 0.9950596650268897
-3.6055025007673183 1.0 4.0 0.9952513157891373
-3.6058025007673185 1.0 4.0 0.9954321502244098
-3.6061025007673186 1.0 4.0 0.9956029644336523
-3.606402500767319 1.0 4.0 0.9957644827315932
-3.606702500767319 1.0 4.0 0.9959173652670877
-3.607002500767319 1.0 4.0 0.9960622147170748
-3.6073025007673194 1.0 4.0 0.9961995821805454
-3.6076025007673196 1.0 4.0 0.996329972379858
-3.6079025007673198 1.0 4.0 0.9964538482608163
-3.60820250076732 1.0 4.0 0.9965716350695805
-3.60850250076732 1.0 4.0 0.9966837239732704
-3.6088025007673203 1.0 4.0 0.9967904752816643
-3.6091025007673205 1.0 4.0 0.9968922213194037
-3.6094025007673207 1.0 4.0 0.9969892689913411
-3.609702500767321 1.0 4.0 0.9970819020779044
-3.610002500767321 1.0 4.0 0.9971703832924473
-3.6103025007673213 1.0 4.0 0.9972549561283606
-3.6106025007673215 1.0 4.0 0.997335846520123
-3.6109025007673217 1.0 4.0 0.9974132643393969
-3.611202500767322 1.0 4.0 0.9974874047446125
-3.611502500767322 1.0 4.0 0.9975584494002001
-3.6118025007673222 1.0 4.0 0.9976265675796557
-3.6121025007673224 1.0 4.0 0.9976919171649098
-3.6124025007673226 1.0 4.0 0.9977546455529844
-3.612702500767323 1.0 4.0 0.9978148904796352
-3.613002500767323 1.0 4.0 0.9978727807685449
-3.613302500767323 1.0 4.0 0.997928437013658
-3.6136025007673234 1.0 4.0 0.9979819722013841
-3.6139025007673236 1.0 4.0 0.9980334922786477
-3.6142025007673237 1.0 4.0 0.9980830966721006
-3.614502500767324 1.0 4.0 0.9981308787632351
-3.614802500767324 1.0 4.0 0.9981769263236245
-3.6151025007673243 1.0 4.0 0.9982213219140661
-3.6154025007673245 1.0 4.0 0.9982641432510087
-3.6157025007673247 1.0 4.0 0.9983054635432892
-3.616002500767325 1.0 4.0 0.9983453518018961
-3.616302500767325 1.0 4.0 0.9983838731251996
-3.6166025007673253 1.0 4.0 0.9984210889618419
-3.6169025007673254 1.0 4.0 0.9984570573532652
-3.6172025007673256 1.0 4.0 0.9984918331576581
-3.617502500767326 1.0 4.0 0.9985254682569294
-3.617802500767326 1.0 4.0 0.9985580117481627
-3.618102500767326 1.0 4.0 0.9985895101208664
-3.6184025007673264 1.0 4.0 0.9986200074212104
-3.6187025007673266 1.0 4.0 0.9986495454043304
-3.6190025007673268 1.0 4.0 0.9986781636756779
-3.619302500767327 1.0 4.0 0.9987058998223078
-3.619602500767327 1.0 4.0 0.9987327895349143
-3.6199025007673273 1.0 4.0 0.99875886672135
+3.5902025007673086 1.0 0.0 2.123960551568673e-10
+3.590502500767309 1.0 0.0 0.015579061561108385
+3.590802500767309 1.0 0.0 0.18225931323083497
+3.591102500767309 1.0 0.0 0.3984388743434077
+3.5914025007673094 1.0 0.0 0.5625793951060628
+3.5917025007673096 1.0 0.0 0.6748647259802757
+3.5920025007673098 1.0 0.0 0.7515099732721983
+3.59230250076731 1.0 4.0 0.8050512190153075
+3.59260250076731 1.0 4.0 0.8435136098234958
+3.5929025007673103 1.0 4.0 0.8718974939415391
+3.5932025007673105 1.0 4.0 0.8933585369428868
+3.5935025007673107 1.0 4.0 0.9099367490980849
+3.593802500767311 1.0 4.0 0.9229862115489604
+3.594102500767311 1.0 4.0 0.9334290619957748
+3.5944025007673113 1.0 4.0 0.9419084241465315
+3.5947025007673115 1.0 4.0 0.9488826864219844
+3.5950025007673116 1.0 4.0 0.9546850636210197
+3.595302500767312 1.0 4.0 0.9595621290979105
+3.595602500767312 1.0 4.0 0.9636993051808351
+3.595902500767312 1.0 4.0 0.9672380791696005
+3.5962025007673124 1.0 4.0 0.9702878553473393
+3.5965025007673126 1.0 4.0 0.9729342591231542
+3.596802500767313 1.0 4.0 0.9752450503174948
+3.597102500767313 1.0 4.0 0.9772743971850191
+3.597402500767313 1.0 4.0 0.9790660083616811
+3.5977025007673133 1.0 4.0 0.98065545724185
+3.5980025007673135 1.0 4.0 0.9820719274121663
+3.5983025007673137 1.0 4.0 0.9833395377107886
+3.598602500767314 1.0 4.0 0.9844783584031653
+3.598902500767314 1.0 4.0 0.9855051978702125
+3.5992025007673143 1.0 4.0 0.9864342170260264
+3.5995025007673145 1.0 4.0 0.9872774131608733
+3.5998025007673147 1.0 4.0 0.98804500391317
+3.600102500767315 1.0 4.0 0.9887457342025247
+3.600402500767315 1.0 4.0 0.989387123259453
+3.6007025007673152 1.0 4.0 0.9899756647241689
+3.6010025007673154 1.0 4.0 0.9905169897157721
+3.6013025007673156 1.0 4.0 0.991016000487684
+3.601602500767316 1.0 4.0 0.9914769805702212
+3.601902500767316 1.0 4.0 0.9919036860041237
+3.602202500767316 1.0 4.0 0.9922994212805374
+3.6025025007673164 1.0 4.0 0.9926671028445246
+3.6028025007673166 1.0 4.0 0.9930093124332529
+3.6031025007673168 1.0 4.0 0.993328342064443
+3.603402500767317 1.0 4.0 0.9936262321342858
+3.603702500767317 1.0 4.0 0.9939048038036293
+3.6040025007673173 1.0 4.0 0.9941656866293818
+3.6043025007673175 1.0 4.0 0.9944103422216146
+3.6046025007673177 1.0 4.0 0.9946400845657717
+3.604902500767318 1.0 4.0 0.9948560975360664
+3.605202500767318 1.0 4.0 0.9950594500346855
+3.6055025007673183 1.0 4.0 0.9952511091172687
+3.6058025007673185 1.0 4.0 0.995431951404769
+3.6061025007673186 1.0 4.0 0.9956027730324526
+3.606402500767319 1.0 4.0 0.9957642983462878
+3.606702500767319 1.0 4.0 0.9959171875236142
+3.607002500767319 1.0 4.0 0.9960620432673863
+3.6073025007673194 1.0 4.0 0.9961994167003927
+3.6076025007673196 1.0 4.0 0.996329812566795
+3.6079025007673198 1.0 4.0 0.9964536938324022
+3.60820250076732 1.0 4.0 0.9965714857617567
+3.60850250076732 1.0 4.0 0.9966835795388925
+3.6088025007673203 1.0 4.0 0.9967903354891723
+3.6091025007673205 1.0 4.0 0.9968920859516158
+3.6094025007673207 1.0 4.0 0.9969891378443573
+3.609702500767321 1.0 4.0 0.9970817749601086
+3.610002500767321 1.0 4.0 0.9971702600235989
+3.6103025007673213 1.0 4.0 0.9972548365387637
+3.6106025007673215 1.0 4.0 0.9973357304498689
+3.6109025007673217 1.0 4.0 0.9974131516376702
+3.611202500767322 1.0 4.0 0.9974872952690547
+3.611502500767322 1.0 4.0 0.9975583430163266
+3.6118025007673222 1.0 4.0 0.9976264641603207
+3.6121025007673224 1.0 4.0 0.9976918165898129
+3.6124025007673226 1.0 4.0 0.9977545477082174
+3.612702500767323 1.0 4.0 0.9978147952572639
+3.613002500767323 1.0 4.0 0.9978726880662238
+3.613302500767323 1.0 4.0 0.9979283467342736
+3.6136025007673234 1.0 4.0 0.9979818842527258
+3.6139025007673236 1.0 4.0 0.998033406573103
+3.6142025007673237 1.0 4.0 0.9980830131263728
+3.614502500767324 1.0 4.0 0.9981307972980812
+3.614802500767324 1.0 4.0 0.9981768468636119
+3.6151025007673243 1.0 4.0 0.9982212443873473
+3.6154025007673245 1.0 4.0 0.9982640675891112
+3.6157025007673247 1.0 4.0 0.99830538968092
+3.616002500767325 1.0 4.0 0.9983452796767597
+3.616302500767325 1.0 4.0 0.9983838026778284
+3.6166025007673253 1.0 4.0 0.998421020135438
+3.6169025007673254 1.0 4.0 0.9984569900935525
+3.6172025007673256 1.0 4.0 0.998491767412744
+3.617502500767326 1.0 4.0 0.9985254039771764
+3.617802500767326 1.0 4.0 0.9985579488860665
+3.618102500767326 1.0 4.0 0.9985894486309436
+3.6184025007673264 1.0 4.0 0.9986199472598922
+3.6187025007673266 1.0 4.0 0.9986494865298633
+3.6190025007673268 1.0 4.0 0.9986781060480299
+3.619302500767327 1.0 4.0 0.9987058434030812
+3.619602500767327 1.0 4.0 0.9987327342872634
+3.6199025007673273 1.0 4.0 0.9987588126099032
diff --git a/py/picca/tests/delta_extraction/data/dla_mask_forest2_set_ivar.txt b/py/picca/tests/delta_extraction/data/dla_mask_forest2_set_ivar.txt
index 99320cf63..054894d5f 100644
--- a/py/picca/tests/delta_extraction/data/dla_mask_forest2_set_ivar.txt
+++ b/py/picca/tests/delta_extraction/data/dla_mask_forest2_set_ivar.txt
@@ -1,196 +1,196 @@
 # log_lambda flux ivar transmission_correction
-3.5617025007672907 1.0 0.0 4.02566259993509e-81
+3.5617025007672907 1.0 0.0 3.9923973967033878e-81
 3.562002500767291 1.0 0.0 0.0
-3.562302500767291 1.0 0.0 9.129954568406628e-33
-3.5626025007672912 1.0 0.0 2.9877384582300445e-10
-3.5629025007672914 1.0 0.0 3.101288606754988e-05
-3.5632025007672916 1.0 0.0 0.002401039786713952
-3.563502500767292 1.0 0.0 0.019507180756110496
-3.563802500767292 1.0 0.0 0.06265980670118523
-3.564102500767292 1.0 0.0 0.12820561733459504
-3.5644025007672924 1.0 0.0 0.20524322890911398
-3.5647025007672926 1.0 0.0 0.2842685801561934
-3.5650025007672927 1.0 0.0 0.3594820464752672
-3.565302500767293 1.0 0.0 0.428117486848669
-3.565602500767293 1.0 0.0 0.489288217363083
-3.5659025007672933 1.0 0.0 0.543110303543023
-3.5662025007672935 1.0 0.0 0.5901653697187408
-3.5665025007672937 1.0 0.0 0.6312045090236377
-3.566802500767294 1.0 0.0 0.666997444328225
-3.567102500767294 1.0 0.0 0.6982625584608744
-3.5674025007672943 1.0 0.0 0.7256396683523935
-3.5677025007672944 1.0 0.0 0.74968428366643
-3.5680025007672946 1.0 0.0 0.770871921653348
-3.568302500767295 1.0 0.0 0.7896065207381271
-3.568602500767295 1.0 4.0 0.8062299597204623
-3.568902500767295 1.0 4.0 0.821031264127668
-3.5692025007672954 1.0 4.0 0.8342549003196323
-3.5695025007672956 1.0 4.0 0.8461079714985894
-3.5698025007672958 1.0 4.0 0.8567663283638709
-3.570102500767296 1.0 4.0 0.8663796935987046
-3.570402500767296 1.0 4.0 0.8750759287943979
-3.5707025007672963 1.0 4.0 0.8829645740936641
-3.5710025007672965 1.0 4.0 0.8901397801269391
-3.5713025007672967 1.0 4.0 0.8966827365953981
-3.571602500767297 1.0 4.0 0.9026636859990985
-3.571902500767297 1.0 4.0 0.908143596286968
-3.5722025007672973 1.0 4.0 0.9131755532968999
-3.5725025007672975 1.0 4.0 0.9178059228959831
-3.5728025007672977 1.0 4.0 0.9220753236070846
-3.573102500767298 1.0 4.0 0.9260194430022921
-3.573402500767298 1.0 4.0 0.9296697250139653
-3.5737025007672982 1.0 4.0 0.9330539503296603
-3.5740025007672984 1.0 4.0 0.9361967279928949
-3.5743025007672986 1.0 4.0 0.9391199130529938
-3.574602500767299 1.0 4.0 0.9418429624482842
-3.574902500767299 1.0 4.0 0.9443832391487149
-3.575202500767299 1.0 4.0 0.9467562728293755
-3.5755025007672994 1.0 4.0 0.9489759839173216
-3.5758025007672996 1.0 4.0 0.9510548766876251
-3.5761025007672997 1.0 4.0 0.9530042061300714
-3.5764025007673 1.0 4.0 0.9548341225250784
-3.5767025007673 1.0 4.0 0.9565537970234754
-3.5770025007673003 1.0 4.0 0.9581715309938693
-3.5773025007673005 1.0 4.0 0.9596948514622811
-3.5776025007673007 1.0 4.0 0.961130594604742
-3.577902500767301 1.0 4.0 0.9624849789509452
-3.578202500767301 1.0 4.0 0.9637636697047566
-3.5785025007673013 1.0 4.0 0.964971835376559
-3.5788025007673014 1.0 4.0 0.9661141977456802
-3.5791025007673016 1.0 4.0 0.9671950760226556
-3.579402500767302 1.0 4.0 0.9682184259559838
-3.579702500767302 1.0 4.0 0.9691878745223745
-3.580002500767302 1.0 4.0 0.9701067507499883
-3.5803025007673024 1.0 4.0 0.9709781131482385
-3.5806025007673026 1.0 4.0 0.9718047741530536
-3.5809025007673028 1.0 4.0 0.9725893219413638
-3.581202500767303 1.0 4.0 0.9733341399213961
-3.581502500767303 1.0 4.0 0.9740414241649048
-3.5818025007673033 1.0 4.0 0.974713199012712
-3.5821025007673035 1.0 4.0 0.9753513310549371
-3.5824025007673037 1.0 4.0 0.9759575416614044
-3.582702500767304 1.0 4.0 0.9765334182152621
-3.583002500767304 1.0 4.0 0.9770804241833093
-3.5833025007673043 1.0 4.0 0.9775999081395054
-3.5836025007673045 1.0 4.0 0.9780931118432049
-3.5839025007673047 1.0 4.0 0.9785611774605472
-3.584202500767305 1.0 4.0 0.9790051540058166
-3.584502500767305 1.0 4.0 0.9794260030692908
-3.5848025007673052 1.0 4.0 0.9798246038888498
-3.5851025007673054 1.0 4.0 0.980201757814304
-3.5854025007673056 1.0 4.0 0.9805581922058031
-3.585702500767306 1.0 4.0 0.9808945638007176
-3.586002500767306 1.0 4.0 0.981211461576861
-3.586302500767306 1.0 4.0 0.9815094091337694
-3.5866025007673064 1.0 4.0 0.9817888666078125
-3.5869025007673065 1.0 4.0 0.982050232131115
-3.5872025007673067 1.0 4.0 0.982293842838451
-3.587502500767307 1.0 4.0 0.982519975420371
-3.587802500767307 1.0 4.0 0.9827288462146698
-3.5881025007673073 1.0 4.0 0.9829206108217855
-3.5884025007673075 1.0 4.0 0.9830953632226793
-3.5887025007673077 1.0 4.0 0.9832531343700243
-3.589002500767308 1.0 4.0 0.9833938902148908
-3.589302500767308 1.0 4.0 0.9835175291213905
-3.5896025007673082 1.0 4.0 0.9836238786105683
-3.5899025007673084 1.0 4.0 0.9837126913619676
-3.5902025007673086 1.0 4.0 0.9837836403862468
-3.590502500767309 1.0 4.0 0.9838363132645849
-3.590802500767309 1.0 4.0 0.9838702053297071
-3.591102500767309 1.0 4.0 0.9838847116384793
-3.5914025007673094 1.0 4.0 0.983879117556227
-3.5917025007673096 1.0 4.0 0.9838525877370609
-3.5920025007673098 1.0 4.0 0.9838041532410783
-3.59230250076731 1.0 4.0 0.9837326964765508
-3.59260250076731 1.0 4.0 0.9836369335907263
-3.5929025007673103 1.0 4.0 0.9835153938537532
-3.5932025007673105 1.0 4.0 0.983366395482635
-3.5935025007673107 1.0 4.0 0.9831880172311896
-3.593802500767311 1.0 4.0 0.9829780649213571
-3.594102500767311 1.0 4.0 0.9827340319026604
-3.5944025007673113 1.0 4.0 0.9824530521893604
-3.5947025007673115 1.0 4.0 0.9821318447246418
-3.5950025007673116 1.0 4.0 0.981766646839159
-3.595302500767312 1.0 4.0 0.9813531344822757
-3.595602500767312 1.0 4.0 0.9808863261744593
-3.595902500767312 1.0 4.0 0.9803604668126945
-3.5962025007673124 1.0 4.0 0.9797688863947996
-3.5965025007673126 1.0 4.0 0.9791038273269786
-3.596802500767313 1.0 4.0 0.9783562321221823
-3.597102500767313 1.0 4.0 0.9775154808172787
-3.597402500767313 1.0 4.0 0.9765690640976898
-3.5977025007673133 1.0 4.0 0.9755021735805958
-3.5980025007673135 1.0 4.0 0.974297184483511
-3.5983025007673137 1.0 4.0 0.9729329972809116
-3.598602500767314 1.0 4.0 0.9713841928743507
-3.598902500767314 1.0 4.0 0.9696199386956528
-3.5992025007673143 1.0 4.0 0.9676025586402146
-3.5995025007673145 1.0 4.0 0.9652856441165611
-3.5998025007673147 1.0 4.0 0.9626115310690385
-3.600102500767315 1.0 4.0 0.9595078894963833
-3.600402500767315 1.0 4.0 0.9558830530955026
-3.6007025007673152 1.0 4.0 0.9516195331467872
-3.6010025007673154 1.0 4.0 0.9465648723702937
-3.6013025007673156 1.0 4.0 0.9405185325351875
-3.601602500767316 1.0 4.0 0.9332127544995432
-3.601902500767316 1.0 4.0 0.9242840685645567
-3.602202500767316 1.0 4.0 0.9132299814932425
-3.6025025007673164 1.0 4.0 0.8993416152311487
-3.6028025007673166 1.0 4.0 0.881596404599704
-3.6031025007673168 1.0 4.0 0.8584829574709137
-3.603402500767317 1.0 4.0 0.8277086605191243
-3.603702500767317 1.0 0.0 0.7857039614922768
-3.6040025007673173 1.0 0.0 0.726787508534109
-3.6043025007673175 1.0 0.0 0.6418662242939958
-3.6046025007673177 1.0 0.0 0.517130357152487
-3.604902500767318 1.0 0.0 0.337370334021082
-3.605202500767318 1.0 0.0 0.11927465366938121
-3.6055025007673183 1.0 0.0 0.0026591501818660763
-3.6058025007673185 1.0 0.0 3.4759062960561738e-31
-3.6061025007673186 1.0 0.0 9.87271803127954e-35
-3.606402500767319 1.0 0.0 0.0025141736391582513
-3.606702500767319 1.0 0.0 0.1184539907335809
-3.607002500767319 1.0 0.0 0.3375053180924655
-3.6073025007673194 1.0 0.0 0.5182521658776412
-3.6076025007673196 1.0 0.0 0.6435891663053709
-3.6079025007673198 1.0 0.0 0.7288510023206626
-3.60820250076732 1.0 0.0 0.7879688020118972
-3.60850250076732 1.0 4.0 0.8301050719942159
-3.6088025007673203 1.0 4.0 0.8609774683913138
-3.6091025007673205 1.0 4.0 0.8841742731155318
-3.6094025007673207 1.0 4.0 0.9019977910083097
-3.609702500767321 1.0 4.0 0.9159644311472136
-3.610002500767321 1.0 4.0 0.9270993232941368
-3.6103025007673213 1.0 4.0 0.9361126070767088
-3.6106025007673215 1.0 4.0 0.9435073408255641
-3.6109025007673217 1.0 4.0 0.9496471890996413
-3.611202500767322 1.0 4.0 0.954799913890923
-3.611502500767322 1.0 4.0 0.9591659626270265
-3.6118025007673222 1.0 4.0 0.9628976625796325
-3.6121025007673224 1.0 4.0 0.9661123631941517
-3.6124025007673226 1.0 4.0 0.9689015983431168
-3.612702500767323 1.0 4.0 0.9713375807039032
-3.613002500767323 1.0 4.0 0.9734778759323008
-3.613302500767323 1.0 4.0 0.975368814470389
-3.6136025007673234 1.0 4.0 0.9770480144911803
-3.6139025007673236 1.0 4.0 0.9785462701144112
-3.6142025007673237 1.0 4.0 0.9798889804168628
-3.614502500767324 1.0 4.0 0.9810972421686425
-3.614802500767324 1.0 4.0 0.9821886935199708
-3.6151025007673243 1.0 4.0 0.9831781712837324
-3.6154025007673245 1.0 4.0 0.9840782273199763
-3.6157025007673247 1.0 4.0 0.9848995374320318
-3.616002500767325 1.0 4.0 0.9856512275489425
-3.616302500767325 1.0 4.0 0.9863411357388173
-3.6166025007673253 1.0 4.0 0.9869760240573329
-3.6169025007673254 1.0 4.0 0.9875617508952343
-3.6172025007673256 1.0 4.0 0.9881034120090838
-3.617502500767326 1.0 4.0 0.9886054565632787
-3.617802500767326 1.0 4.0 0.9890717831106172
-3.618102500767326 1.0 4.0 0.9895058193736713
-3.6184025007673264 1.0 4.0 0.9899105888735864
-3.6187025007673266 1.0 4.0 0.9902887668240218
-3.6190025007673268 1.0 4.0 0.9906427272198688
-3.619302500767327 1.0 4.0 0.990974582669223
-3.619602500767327 1.0 4.0 0.9912862182177022
-3.6199025007673273 1.0 4.0 0.9915793201776922
+3.562302500767291 1.0 0.0 9.100311703855126e-33
+3.5626025007672912 1.0 0.0 2.984872562346406e-10
+3.5629025007672914 1.0 0.0 3.099882395702504e-05
+3.5632025007672916 1.0 0.0 0.002400407528576525
+3.563502500767292 1.0 0.0 0.019503828823393857
+3.563802500767292 1.0 0.0 0.06265223220459366
+3.564102500767292 1.0 0.0 0.1281941258492091
+3.5644025007672924 1.0 0.0 0.20522904738821288
+3.5647025007672926 1.0 0.0 0.28425297902982044
+3.5650025007672927 1.0 0.0 0.359465999883732
+3.565302500767293 1.0 0.0 0.4281016406743208
+3.565602500767293 1.0 0.0 0.4892729583224805
+3.5659025007672933 1.0 0.0 0.5430958390389279
+3.5662025007672935 1.0 0.0 0.5901517915551449
+3.5665025007672937 1.0 0.0 0.6311918380732826
+3.566802500767294 1.0 0.0 0.6669856599639641
+3.567102500767294 1.0 0.0 0.6982516172684856
+3.5674025007672943 1.0 0.0 0.7256295157311657
+3.5677025007672944 1.0 0.0 0.7496748608526737
+3.5680025007672946 1.0 0.0 0.7708631698510242
+3.568302500767295 1.0 0.0 0.7895983834442988
+3.568602500767295 1.0 4.0 0.8062223839375932
+3.568902500767295 1.0 4.0 0.8210242008993158
+3.5692025007672954 1.0 4.0 0.8342483048633423
+3.5695025007672956 1.0 4.0 0.8461018031067913
+3.5698025007672958 1.0 4.0 0.8567605501786097
+3.570102500767296 1.0 4.0 0.8663742723251008
+3.570402500767296 1.0 4.0 0.8750708343919892
+3.5707025007672963 1.0 4.0 0.8829597794686365
+3.5710025007672965 1.0 4.0 0.8901352608381654
+3.5713025007672967 1.0 4.0 0.8966784705809621
+3.571602500767297 1.0 4.0 0.9026596533261623
+3.571902500767297 1.0 4.0 0.9081397789255641
+3.5722025007672973 1.0 4.0 0.9131719349169067
+3.5725025007672975 1.0 4.0 0.9178024886858399
+3.5728025007672977 1.0 4.0 0.9220720601124608
+3.573102500767298 1.0 4.0 0.9260163379828164
+3.573402500767298 1.0 4.0 0.9296667673161447
+3.5737025007672982 1.0 4.0 0.9330511297742179
+3.5740025007672984 1.0 4.0 0.9361940352748884
+3.5743025007672986 1.0 4.0 0.9391173396532325
+3.574602500767299 1.0 4.0 0.9418405005547114
+3.574902500767299 1.0 4.0 0.9443808815865725
+3.575202500767299 1.0 4.0 0.9467540129991028
+3.5755025007672994 1.0 4.0 0.948973815739264
+3.5758025007672996 1.0 4.0 0.9510527945527436
+3.5761025007672997 1.0 4.0 0.9530022048559404
+3.5764025007673 1.0 4.0 0.9548321973165486
+3.5767025007673 1.0 4.0 0.9565519434374536
+3.5770025007673003 1.0 4.0 0.958169744907741
+3.5773025007673005 1.0 4.0 0.9596931290455515
+3.5776025007673007 1.0 4.0 0.9611289322935329
+3.577902500767301 1.0 4.0 0.9624833734250208
+3.578202500767301 1.0 4.0 0.963762117866799
+3.5785025007673013 1.0 4.0 0.9649703343334426
+3.5788025007673014 1.0 4.0 0.9661127447915243
+3.5791025007673016 1.0 4.0 0.967193668623466
+3.579402500767302 1.0 4.0 0.9682170617357101
+3.579702500767302 1.0 4.0 0.9691865512502328
+3.580002500767302 1.0 4.0 0.9701054663289178
+3.5803025007673024 1.0 4.0 0.9709768656043706
+3.5806025007673026 1.0 4.0 0.9718035616260929
+3.5809025007673028 1.0 4.0 0.972588142675784
+3.581202500767303 1.0 4.0 0.9733329922583686
+3.581502500767303 1.0 4.0 0.974040306534888
+3.5818025007673033 1.0 4.0 0.9747121099286314
+3.5821025007673035 1.0 4.0 0.9753502691059001
+3.5824025007673037 1.0 4.0 0.9759565055068934
+3.582702500767304 1.0 4.0 0.9765324065797601
+3.583002500767304 1.0 4.0 0.977079435851314
+3.5833025007673043 1.0 4.0 0.9775989419508901
+3.5836025007673045 1.0 4.0 0.9780921666888959
+3.5839025007673047 1.0 4.0 0.9785602522784781
+3.584202500767305 1.0 4.0 0.9790042477771356
+3.584502500767305 1.0 4.0 0.97942511481479
+3.5848025007673052 1.0 4.0 0.9798237326655932
+3.5851025007673054 1.0 4.0 0.9802009027124277
+3.5854025007673056 1.0 4.0 0.9805573523454699
+3.585702500767306 1.0 4.0 0.9808937383291999
+3.586002500767306 1.0 4.0 0.9812106496657363
+3.586302500767306 1.0 4.0 0.9815086099762066
+3.5866025007673064 1.0 4.0 0.9817880794159309
+3.5869025007673065 1.0 4.0 0.9820494561333986
+3.5872025007673067 1.0 4.0 0.982293077277197
+3.587502500767307 1.0 4.0 0.982519219549158
+3.587802500767307 1.0 4.0 0.9827280992958226
+3.5881025007673073 1.0 4.0 0.9829198721238186
+3.5884025007673075 1.0 4.0 0.983094632017701
+3.5887025007673077 1.0 4.0 0.9832524099310734
+3.589002500767308 1.0 4.0 0.9833931718131886
+3.589302500767308 1.0 4.0 0.9835168160234788
+3.5896025007673082 1.0 4.0 0.9836231700753065
+3.5899025007673084 1.0 4.0 0.9837119866373578
+3.5902025007673086 1.0 4.0 0.9837829387060508
+3.590502500767309 1.0 4.0 0.983835613844697
+3.590802500767309 1.0 4.0 0.9838695073642337
+3.591102500767309 1.0 4.0 0.9838840142954792
+3.5914025007673094 1.0 4.0 0.9838784199730508
+3.5917025007673096 1.0 4.0 0.9838518890152237
+3.5920025007673098 1.0 4.0 0.983803452440587
+3.59230250076731 1.0 4.0 0.9837319926095959
+3.59260250076731 1.0 4.0 0.9836362256146302
+3.5929025007673103 1.0 4.0 0.9835146806630504
+3.5932025007673105 1.0 4.0 0.9833656759001363
+3.5935025007673107 1.0 4.0 0.9831872899978525
+3.593802500767311 1.0 4.0 0.9829773286847534
+3.594102500767311 1.0 4.0 0.9827332852037911
+3.5944025007673113 1.0 4.0 0.9824522934475115
+3.5947025007673115 1.0 4.0 0.9821310722199187
+3.5950025007673116 1.0 4.0 0.9817658586922542
+3.595302500767312 1.0 4.0 0.9813523286309255
+3.595602500767312 1.0 4.0 0.9808855003459295
+3.595902500767312 1.0 4.0 0.9803596184914686
+3.5962025007673124 1.0 4.0 0.9797680127844582
+3.5965025007673126 1.0 4.0 0.9791029253049971
+3.596802500767313 1.0 4.0 0.9783552981860586
+3.597102500767313 1.0 4.0 0.9775145110199784
+3.597402500767313 1.0 4.0 0.9765680539698539
+3.5977025007673133 1.0 4.0 0.9755011180362392
+3.5980025007673135 1.0 4.0 0.9742960777049936
+3.5983025007673137 1.0 4.0 0.9729318325776931
+3.598602500767314 1.0 4.0 0.971382962508345
+3.598902500767314 1.0 4.0 0.9696186336636752
+3.5992025007673143 1.0 4.0 0.967601168400854
+3.5995025007673145 1.0 4.0 0.9652841562444014
+3.5998025007673147 1.0 4.0 0.9626099308130235
+3.600102500767315 1.0 4.0 0.9595061592104241
+3.600402500767315 1.0 4.0 0.9558811714970323
+3.6007025007673152 1.0 4.0 0.9516174743410791
+3.6010025007673154 1.0 4.0 0.946562604552327
+3.6013025007673156 1.0 4.0 0.9405160162426642
+3.601602500767316 1.0 4.0 0.9332099402344671
+3.601902500767316 1.0 4.0 0.9242808935183076
+3.602202500767316 1.0 4.0 0.9132263649911893
+3.6025025007673164 1.0 4.0 0.8993374523498128
+3.6028025007673166 1.0 4.0 0.8815915572159255
+3.6031025007673168 1.0 4.0 0.8584772418813851
+3.603402500767317 1.0 4.0 0.827701831150862
+3.603702500767317 1.0 0.0 0.7856956927073806
+3.6040025007673173 1.0 0.0 0.7267773869049329
+3.6043025007673175 1.0 0.0 0.6418538031113995
+3.6046025007673177 1.0 0.0 0.5171154690628553
+3.604902500767318 1.0 0.0 0.3373543229198344
+3.605202500767318 1.0 0.0 0.11926356363008697
+3.6055025007673183 1.0 0.0 0.0026584577373791533
+3.6058025007673185 1.0 0.0 3.4540780316877065e-31
+3.6061025007673186 1.0 0.0 9.792455614738244e-35
+3.606402500767319 1.0 0.0 0.002513512714700441
+3.606702500767319 1.0 0.0 0.11844294114415026
+3.607002500767319 1.0 0.0 0.33748930647278386
+3.6073025007673194 1.0 0.0 0.5182372945349747
+3.6076025007673196 1.0 0.0 0.6435767870986614
+3.6079025007673198 1.0 0.0 0.728840942145915
+3.60820250076732 1.0 0.0 0.7879606083721414
+3.60850250076732 1.0 4.0 0.8300983275747204
+3.6088025007673203 1.0 4.0 0.8609718451958079
+3.6091025007673205 1.0 4.0 0.8841695241979595
+3.6094025007673207 1.0 4.0 0.9019937318920854
+3.609702500767321 1.0 4.0 0.9159609232962207
+3.610002500767321 1.0 4.0 0.9270962615886508
+3.6103025007673213 1.0 4.0 0.9361099107768787
+3.6106025007673215 1.0 4.0 0.943504947137175
+3.6109025007673217 1.0 4.0 0.9496450485876347
+3.611202500767322 1.0 4.0 0.9547979871855187
+3.611502500767322 1.0 4.0 0.9591642180336926
+3.6118025007673222 1.0 4.0 0.9628960743249919
+3.6121025007673224 1.0 4.0 0.966110910123526
+3.6124025007673226 1.0 4.0 0.9689002629422644
+3.612702500767323 1.0 4.0 0.9713363483560952
+3.613002500767323 1.0 4.0 0.9734767343481465
+3.613302500767323 1.0 4.0 0.9753677532458591
+3.6136025007673234 1.0 4.0 0.9770470247617732
+3.6139025007673236 1.0 4.0 0.9785453442821402
+3.6142025007673237 1.0 4.0 0.9798881119329358
+3.614502500767324 1.0 4.0 0.9810964253591135
+3.614802500767324 1.0 4.0 0.9821879234448352
+3.6151025007673243 1.0 4.0 0.9831774436222591
+3.6154025007673245 1.0 4.0 0.9840775382767839
+3.6157025007673247 1.0 4.0 0.9848988836596786
+3.616002500767325 1.0 4.0 0.985650606083758
+3.616302500767325 1.0 4.0 0.9863405439474181
+3.6166025007673253 1.0 4.0 0.9869754595918103
+3.6169025007673254 1.0 4.0 0.9875612116554243
+3.6172025007673256 1.0 4.0 0.9881028961106523
+3.617502500767326 1.0 4.0 0.988604962310604
+3.617802500767326 1.0 4.0 0.9890713089736531
+3.618102500767326 1.0 4.0 0.9895053639681283
+3.6184025007673264 1.0 4.0 0.9899101509438866
+3.6187025007673266 1.0 4.0 0.9902883452285919
+3.6190025007673268 1.0 4.0 0.9906423209184003
+3.619302500767327 1.0 4.0 0.9909741907116044
+3.619602500767327 1.0 4.0 0.9912858397343701
+3.6199025007673273 1.0 4.0 0.9915789543711965
diff --git a/py/picca/tests/delta_extraction/data_tests.py b/py/picca/tests/delta_extraction/data_tests.py
index 62791f410..bd543bc89 100644
--- a/py/picca/tests/delta_extraction/data_tests.py
+++ b/py/picca/tests/delta_extraction/data_tests.py
@@ -79,6 +79,8 @@ def check_read_file_error(self, data, catalogue, filename, expected_message,
         with self.assertRaises(DataError) as context_manager:
             reader = DesiHealpixFileHandler(data.analysis_type,
                                             data.use_non_coadded_spectra,
+                                            data.uniquify_night_targetid,
+                                            data.keep_single_exposures,
                                             data.logger)
 
             reader((filename, catalogue))
@@ -747,19 +749,9 @@ def test_desi_data_parse_config(self):
 
         data = DesiHealpix(config["data"])
 
-        # run __parse_config with missing 'blinding'
-        config = ConfigParser()
-        config.read_dict({"data": {
-                        }})
-        expected_message = "Missing argument 'unblind' required by DesiData"
-        with self.assertRaises(DataError) as context_manager:
-            data._DesiData__parse_config(config["data"])
-        self.compare_error_message(context_manager, expected_message)
-
         # run __parse_config with missing 'use_non_coadded_spectra'
         config = ConfigParser()
-        config.read_dict({"data": {
-            "unblind": "True",
+        config.read_dict({"data": {"keep single exposures": False,
                         }})
         expected_message = (
             "Missing argument 'use non-coadded spectra' required by DesiData"
@@ -954,34 +946,6 @@ def test_desi_healpix_parse_config(self):
             DesiHealpix(config["data"])
         self.compare_error_message(context_manager, expected_message)
 
-        # create a DesiHealpix with missing DesiData options
-        config = ConfigParser()
-        config.read_dict({"data": {
-            "catalogue": f"{THIS_DIR}/data/QSO_cat_fuji_dark_healpix.fits.gz",
-            "keep surveys": "all",
-            "input directory": f"{THIS_DIR}/data/",
-            "out dir": f"{THIS_DIR}/results/",
-            "use non-coadded spectra": False,
-            "num processors": 1,
-            "wave solution": "lin",
-            "delta lambda": 0.8,
-            "lambda max": 5500.0,
-            "lambda max rest frame": 1200.0,
-            "lambda min": 3600.0,
-            "lambda min rest frame": 1040.0,
-            "analysis type": "BAO 3D",
-            "minimum number pixels in forest": 50,
-            "rejection log file": "rejection.fits",
-            "minimal snr bao3d": 0.0,
-            "save format": "BinTableHDU",
-        }})
-        expected_message = (
-            "Missing argument 'unblind' required by DesiData"
-        )
-        with self.assertRaises(DataError) as context_manager:
-            DesiHealpix(config["data"])
-        self.compare_error_message(context_manager, expected_message)
-
     def test_desi_healpix_read_data(self):
         """Test method read_data from DesiHealpix"""
         # run with one processor; case: only sv data
@@ -1353,6 +1317,90 @@ def test_desi_tile_read_data(self):
 
         self.assertTrue(len(data.forests) == 10)
 
+
+        # run with one processor; case: using individual spectra in P1d mode, not coadding
+        config = ConfigParser()
+        config.read_dict({"data": {
+            "catalogue": f"{THIS_DIR}/data/QSO_cat_fuji_dark_tile.fits.gz",
+            "input directory": f"{THIS_DIR}/data/tile/cumulative",
+            "out dir": f"{THIS_DIR}/results/",
+            "use non-coadded spectra": True,
+            "uniquify night targetid": True,
+            "keep single exposures": True,
+            "num processors": 1,
+            "analysis type": "PK 1D"
+        }})
+        for key, value in defaults_desi_tile.items():
+            if key not in config["data"]:
+                config["data"][key] = str(value)
+
+        data = DesiTile(config["data"])
+
+        self.assertTrue(len(data.forests) == 110)
+
+
+        # run with two processors; case: using individual spectra in P1d mode, not coadding
+        config = ConfigParser()
+        config.read_dict({"data": {
+            "catalogue": f"{THIS_DIR}/data/QSO_cat_fuji_dark_tile.fits.gz",
+            "input directory": f"{THIS_DIR}/data/tile/cumulative",
+            "out dir": f"{THIS_DIR}/results/",
+            "use non-coadded spectra": True,
+            "uniquify night targetid": True,
+            "keep single exposures": True,
+            "num processors": 2,
+            "analysis type": "PK 1D"
+        }})
+        for key, value in defaults_desi_tile.items():
+            if key not in config["data"]:
+                config["data"][key] = str(value)
+
+        data = DesiTile(config["data"])
+
+        self.assertTrue(len(data.forests) == 110)
+
+        # run with one processor; case: using individual spectra in P1d mode, coadding within picca
+        config = ConfigParser()
+        config.read_dict({"data": {
+            "catalogue": f"{THIS_DIR}/data/QSO_cat_fuji_dark_tile.fits.gz",
+            "input directory": f"{THIS_DIR}/data/tile/cumulative",
+            "out dir": f"{THIS_DIR}/results/",
+            "use non-coadded spectra": True,
+            "uniquify night targetid": True,
+            "keep single exposures": False,
+            "num processors": 1,
+            "analysis type": "PK 1D"
+        }})
+        for key, value in defaults_desi_tile.items():
+            if key not in config["data"]:
+                config["data"][key] = str(value)
+
+        data = DesiTile(config["data"])
+
+        self.assertTrue(len(data.forests) == 10)
+
+
+        # run with two processors; case: using individual spectra in P1d mode, coadding within picca
+        config = ConfigParser()
+        config.read_dict({"data": {
+            "catalogue": f"{THIS_DIR}/data/QSO_cat_fuji_dark_tile.fits.gz",
+            "input directory": f"{THIS_DIR}/data/tile/cumulative",
+            "out dir": f"{THIS_DIR}/results/",
+            "use non-coadded spectra": True,
+            "uniquify night targetid": True,
+            "keep single exposures": False,
+            "num processors": 2,
+            "analysis type": "PK 1D"
+            ""
+        }})
+        for key, value in defaults_desi_tile.items():
+            if key not in config["data"]:
+                config["data"][key] = str(value)
+
+        data = DesiTile(config["data"])
+
+        self.assertTrue(len(data.forests) == 10)
+
     def test_desisim_mocks(self):
         """Test DesisimMocks"""
         # case: BAO 3D
diff --git a/py/picca/tests/delta_extraction/profiler.py b/py/picca/tests/delta_extraction/profiler.py
new file mode 100644
index 000000000..bd38952f2
--- /dev/null
+++ b/py/picca/tests/delta_extraction/profiler.py
@@ -0,0 +1,34 @@
+import os
+import sys
+import cProfile
+import argparse
+import pstats
+
+
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+os.environ["THIS_DIR"] = THIS_DIR
+
+PICCA_BIN = THIS_DIR.split("py/picca")[0]+"bin/"
+sys.path.append(PICCA_BIN)
+
+from picca_delta_extraction import main
+
+parser = argparse.ArgumentParser(
+    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+    description=('Compute the delta field '
+                 'from a list of spectra'))
+
+parser.add_argument(
+    'config_file',
+    type=str,
+    default=None,
+    help=
+    ('Configuration file. To learn about all the available options '
+     'check the configuration tutorial in '
+     '$PICCA/tutorials/delta_extraction/picca_delta_extraction_tutorial.ipynb'
+    ))
+
+args = parser.parse_args()
+cProfile.run('main(args)', 'results/profile_output')
+p = pstats.Stats('results/profile_output')
+p.sort_stats('cumulative').print_stats()
diff --git a/py/picca/tests/delta_extraction/scripts_tests.py b/py/picca/tests/delta_extraction/scripts_tests.py
index 8bd10c86a..07f90c675 100644
--- a/py/picca/tests/delta_extraction/scripts_tests.py
+++ b/py/picca/tests/delta_extraction/scripts_tests.py
@@ -99,6 +99,14 @@ def test_delta_lin(self):
 
         self.run_delta_extraction(config_file, out_dir, test_dir)
 
+    def test_delta_lin_fast(self):
+        """End-to-end test using 'LYA' setup with  a linear wavelenth solution"""
+        config_file = "{}/data/delta_lin_fast.ini".format(THIS_DIR)
+        out_dir = "{}/results/delta_extraction_lin".format(THIS_DIR)
+        test_dir = "{}/data/delta_extraction_lin".format(THIS_DIR)
+
+        self.run_delta_extraction(config_file, out_dir, test_dir)
+
     def test_delta_lin_image(self):
         """End-to-end test using 'LYA' linear setup storing data as image"""
         config_file = "{}/data/delta_lin_image.ini".format(THIS_DIR)
@@ -107,6 +115,14 @@ def test_delta_lin_image(self):
 
         self.run_delta_extraction(config_file, out_dir, test_dir)
 
+    def test_delta_lin_image_fast(self):
+        """End-to-end test using 'LYA' linear setup storing data as image"""
+        config_file = "{}/data/delta_lin_image_fast.ini".format(THIS_DIR)
+        out_dir = "{}/results/delta_extraction_lin_image".format(THIS_DIR)
+        test_dir = "{}/data/delta_extraction_lin_image".format(THIS_DIR)
+
+        self.run_delta_extraction(config_file, out_dir, test_dir)
+
     def test_delta_lin_pk1d(self):
         """End-to-end test using 'LYA' setup with a linear wavelenth solution and Pk1D Forests.
         """
@@ -141,6 +157,15 @@ def test_delta_lya_desi_mocks(self):
 
         self.run_delta_extraction(config_file, out_dir, test_dir)
 
+    def test_delta_lya_desi_mocks_fast(self):
+        """End-to-end test using 'LYA' setup without corrections or masking,
+        for desi mocks"""
+        config_file = "{}/data/delta_lya_desi_mocks_fast.ini".format(THIS_DIR)
+        out_dir = "{}/results/delta_extraction_lya_desi_mocks".format(THIS_DIR)
+        test_dir = "{}/data/delta_extraction_lya_desi_mocks".format(THIS_DIR)
+
+        self.run_delta_extraction(config_file, out_dir, test_dir)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/py/picca/tests/test_2_pk1d.py b/py/picca/tests/test_2_pk1d.py
index f19e07d48..f4f509311 100644
--- a/py/picca/tests/test_2_pk1d.py
+++ b/py/picca/tests/test_2_pk1d.py
@@ -69,7 +69,7 @@ def test_Pk1D_raw(self):
         ### Test
         if self._test:
             path1 = self._masterFiles + "/test_Pk1D/Pk1D_raw.fits.gz"
-            path2 = self._branchFiles + "/Products/Pk1D_raw/Pk1D-0.fits.gz"
+            path2 = self._branchFiles + "/Products/Pk1D_raw/Pk1D-272.fits.gz"
             self.compare_fits(path1, path2, "picca_Pk1D.py")
 
         return
@@ -103,5 +103,36 @@ def test_meanPk1D(self):
             self.compare_fits(path1, path2, "picca_Pk1D_postprocess.py")
 
 
+    def test_meanPk1D_covariance(self):
+        """
+            Runs a covariance test of Pk1d postprocessing
+        """
+        import picca.bin.picca_Pk1D_postprocess as picca_Pk1D_postprocess
+
+        self._test = True
+        userprint("\n")
+        ### Send
+        #- picca_Pk1D_postprocess.py takes all Pk1D*.fits.gz files in in-dir
+        #  => copy a single file
+        shutil.copy(self._masterFiles + "/test_Pk1D/Pk1D.fits.gz",
+                    self._branchFiles + "/Products/meanPk1D")
+        print(os.listdir(self._branchFiles + "/Products/meanPk1D"))
+        cmd = "picca_Pk1D_postprocess.py "
+        cmd += " --in-dir " + self._branchFiles + "/Products/meanPk1D"
+        cmd += " --output-file " + self._branchFiles + "/Products/meanPk1D/meanPk1D_covariance.fits.gz"
+        #- small sample => k,z-bins changed wrt default ones
+        cmd += " --zedge-min 2.1 --zedge-max 3.1 --zedge-bin 0.2"
+        cmd += " --kedge-min 0.015 --kedge-max 0.035 --kedge-bin 0.005"
+        cmd += " --covariance"
+        picca_Pk1D_postprocess.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_Pk1D/meanPk1D_covariance.fits.gz"
+            path2 = self._branchFiles + "/Products/meanPk1D/meanPk1D_covariance.fits.gz"
+            self.compare_fits(path1, path2, "picca_Pk1D_postprocess.py")
+
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/py/picca/tests/test_3_cor.py b/py/picca/tests/test_3_cor.py
index 941551bfa..e62d54e9c 100644
--- a/py/picca/tests/test_3_cor.py
+++ b/py/picca/tests/test_3_cor.py
@@ -25,8 +25,11 @@
 import picca.bin.picca_xcf_angl
 import picca.bin.picca_dmat
 import picca.bin.picca_xdmat
+
 import picca.bin.picca_metal_dmat
 import picca.bin.picca_metal_xdmat
+import picca.bin.picca_fast_metal_dmat
+import picca.bin.picca_fast_metal_xdmat
 import picca.bin.picca_export
 import picca.bin.picca_export_cross_covariance
 import picca.bin.picca_export_co
@@ -325,6 +328,8 @@ def test_dmat(self):
         cmd += " --rej 0.99"
         cmd += " --nproc 1"
         cmd += ' --remove-same-half-plate-close-pairs'
+        cmd += ' --no-redshift-evolution'
+
         print(repr(cmd))
         picca.bin.picca_dmat.main(cmd.split()[1:])
 
@@ -367,6 +372,35 @@ def test_metal_dmat(self):
 
         return
 
+
+    def test_fast_metal_dmat(self):
+        """
+            Test metal distortion matrix
+        """
+        importlib.reload(picca.cf)
+
+        userprint("\n")
+        ### Send
+        cmd = "picca_fast_metal_dmat.py"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --out " + self._branchFiles + "/Products/Correlations/fast_metal_dmat.fits.gz"
+        cmd += r" --abs-igm SiIII(1207)"
+        cmd += " --rp-min +0.0"
+        cmd += " --rp-max +60.0"
+        cmd += " --rt-max +60.0"
+        cmd += " --np 15"
+        cmd += " --nt 15"
+        print(repr(cmd))
+        picca.bin.picca_fast_metal_dmat.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_cor/fast_metal_dmat.fits.gz"
+            path2 = self._branchFiles + "/Products/Correlations/fast_metal_dmat.fits.gz"
+            self.compare_fits(path1, path2, "picca_fast_metal_dmat.py")
+
+        return
+
     def test_wick(self):
         """
             Test wick covariances
@@ -465,6 +499,8 @@ def test_dmat_cross(self):
         cmd += " --nproc 1"
         cmd += ' --remove-same-half-plate-close-pairs'
         cmd += " --unfold-cf"
+        cmd += ' --no-redshift-evolution'
+
         print(repr(cmd))
         picca.bin.picca_dmat.main(cmd.split()[1:])
 
@@ -511,6 +547,38 @@ def test_metal_dmat_cross(self):
 
         return
 
+    def test_fast_metal_dmat_cross(self):
+        """
+            Test metal cross distortion matrix
+        """
+        importlib.reload(picca.cf)
+
+        userprint("\n")
+        ### Send
+        cmd = "picca_fast_metal_dmat.py"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --out " + self._branchFiles + \
+            "/Products/Correlations/fast_metal_dmat_cross.fits.gz"
+        cmd += r" --abs-igm SiIII(1207)"
+        cmd += r" --abs-igm2 SiIII(1207)"
+        cmd += " --rp-min -60.0"
+        cmd += " --rp-max +60.0"
+        cmd += " --rt-max +60.0"
+        cmd += " --np 30"
+        cmd += " --nt 15"
+        cmd += " --unfold-cf"
+        print(repr(cmd))
+        picca.bin.picca_fast_metal_dmat.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_cor/fast_metal_dmat_cross.fits.gz"
+            path2 = self._branchFiles + "/Products/Correlations/fast_metal_dmat_cross.fits.gz"
+            self.compare_fits(path1, path2, "picca_fast_metal_dmat.py")
+
+        return
+
     def test_export_cf_cross(self):
         """
             Test export of cross correlation function
@@ -602,6 +670,7 @@ def test_xdmat(self):
         cmd += " --rej 0.99"
         cmd += " --nproc 1"
         cmd += " --z-evol-obj 1."
+        cmd += ' --no-redshift-evolution'
         print(repr(cmd))
         picca.bin.picca_xdmat.main(cmd.split()[1:])
 
@@ -643,6 +712,34 @@ def test_metal_xdmat(self):
 
         return
 
+    def test_fast_metal_xdmat(self):
+        """
+            Test metal cross distortion matrix
+        """
+        userprint("\n")
+        ### Send
+        cmd = "picca_fast_metal_xdmat.py"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --drq " + self._masterFiles + "/test_delta/cat.fits"
+        cmd += " --out " + self._branchFiles + "/Products/Correlations/fast_metal_xdmat.fits.gz"
+        cmd += r" --abs-igm SiIII(1207)"
+        cmd += " --rp-min -60.0"
+        cmd += " --rp-max +60.0"
+        cmd += " --rt-max +60.0"
+        cmd += " --np 30"
+        cmd += " --nt 15"
+        cmd += " --z-evol-obj 1."
+        print(repr(cmd))
+        picca.bin.picca_fast_metal_xdmat.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_cor/fast_metal_xdmat.fits.gz"
+            path2 = self._branchFiles + "/Products/Correlations/fast_metal_xdmat.fits.gz"
+            self.compare_fits(path1, path2, "picca_fast_metal_xdmat.py")
+
+        return
+
     def test_xwick(self):
         """
             Test wick covariances for cross
@@ -788,6 +885,8 @@ def test_dmat_zcuts(self):
         cmd += " --z-cut-max 2.3"
         cmd += " --z-min-sources 2.3"
         cmd += " --z-max-sources 2.5"
+        cmd += ' --no-redshift-evolution'
+
         print(repr(cmd))
         picca.bin.picca_dmat.main(cmd.split()[1:])
 
@@ -834,6 +933,38 @@ def test_metal_dmat_zcuts(self):
 
         return
 
+    def test_fast_metal_dmat_zcuts(self):
+        """
+            Test metal distortion matrix
+        """
+        importlib.reload(picca.cf)
+
+        userprint("\n")
+        ### Send
+        cmd = "picca_metal_dmat.py"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --out " + self._branchFiles + "/Products/Correlations/fast_metal_dmat_zcuts.fits.gz"
+        cmd += r" --abs-igm SiIII(1207)"
+        cmd += " --rp-min +0.0"
+        cmd += " --rp-max +60.0"
+        cmd += " --rt-max +60.0"
+        cmd += " --np 15"
+        cmd += " --nt 15"
+        cmd += " --z-cut-min 2.25"
+        cmd += " --z-cut-max 2.3"
+        cmd += " --z-min-sources 2.3"
+        cmd += " --z-max-sources 2.5"
+        print(repr(cmd))
+        picca.bin.picca_fast_metal_dmat.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_cor/fast_metal_dmat_zcuts.fits.gz"
+            path2 = self._branchFiles + "/Products/Correlations/fast_metal_dmat_zcuts.fits.gz"
+            self.compare_fits(path1, path2, "picca_fast_metal_dmat.py")
+
+        return
+
     def test_wick_zcuts(self):
         """
             Test wick covariances
@@ -929,6 +1060,8 @@ def test_dmat_cross_zcuts(self):
         cmd += " --z-cut-max 2.3"
         cmd += " --z-min-sources 2.3"
         cmd += " --z-max-sources 2.5"
+        cmd += ' --no-redshift-evolution'
+
         print(repr(cmd))
         picca.bin.picca_dmat.main(cmd.split()[1:])
 
@@ -979,6 +1112,42 @@ def test_metal_dmat_cross_zcuts(self):
 
         return
 
+    def test_fast_metal_dmat_cross_zcuts(self):
+        """
+            Test metal cross distortion matrix
+        """
+        importlib.reload(picca.cf)
+
+        userprint("\n")
+        ### Send
+        cmd = "picca_fast_metal_dmat.py"
+        cmd += " --in-attributes " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --in-attributes2 " + self._masterFiles + "/test_cor/input_from_delta_extraction_lya_nodla/Log/delta_attributes.fits.gz"
+        cmd += " --out " + self._branchFiles + \
+            "/Products/Correlations/fast_metal_dmat_cross_zcuts.fits.gz"
+        cmd += r" --abs-igm SiIII(1207)"
+        cmd += r" --abs-igm2 SiIII(1207)"
+        cmd += " --rp-min -60.0"
+        cmd += " --rp-max +60.0"
+        cmd += " --rt-max +60.0"
+        cmd += " --np 30"
+        cmd += " --nt 15"
+        cmd += " --unfold-cf"
+        cmd += " --z-cut-min 2.25"
+        cmd += " --z-cut-max 2.3"
+        cmd += " --z-min-sources 2.3"
+        cmd += " --z-max-sources 2.5"
+        print(repr(cmd))
+        picca.bin.picca_fast_metal_dmat.main(cmd.split()[1:])
+
+        ### Test
+        if self._test:
+            path1 = self._masterFiles + "/test_cor/fast_metal_dmat_cross_zcuts.fits.gz"
+            path2 = self._branchFiles + "/Products/Correlations/fast_metal_dmat_cross_zcuts.fits.gz"
+            self.compare_fits(path1, path2, "picca_fast_metal_dmat.py")
+
+        return
+
     def test_xcf_angl_zcuts(self):
         """
             Test angular cross correlation function
@@ -1065,6 +1234,7 @@ def test_xdmat_zcuts(self):
         cmd += " --z-cut-max 2.3"
         cmd += " --z-min-sources 2.3"
         cmd += " --z-max-sources 2.5"
+        cmd += ' --no-redshift-evolution'
         print(repr(cmd))
         picca.bin.picca_xdmat.main(cmd.split()[1:])