Repository: I2PC/scipion-em-xmipp (language: python)

Issue:
DeepCenter: New protocols
4 new protocols that performs centering and global assignment of  a set of particles:
- deep_center: uses a labeled set of particles to train CNN-model(s) to center particles
- deep_center_predict: uses input trained model(s) to predict centers of an input set of particles
- deep_global_assignment: uses a labeled set of particles to train CNN-model(s) to perform global assignment of Euler angles
- deep_global_assignment_predict: uses input trained model(s) to assign angles of an input set of particles


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/buildPlugin.yml b/.github/workflows/buildPlugin.yml
new file mode 100644
index 000000000..9f49edd05
--- /dev/null
+++ b/.github/workflows/buildPlugin.yml
@@ -0,0 +1,88 @@
+# Name of the GitHub Action
+name: Build scipion-em-xmipp on Pull Request
+
+# Specify when the Action should be triggered: when a pull request is opened against the 'devel' or 'master' branch
+on:
+  pull_request:
+    branches: [devel, master]
+
+# Define the job that should be run
+jobs:
+  build:
+    # Specify the machine to run the job on
+    runs-on: ubuntu-22.04
+
+    # Define the steps to be taken in the job
+    steps:    
+    # Installing dependencies
+    - name: Install dependencies 
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libfftw3-dev libopenmpi-dev openmpi-bin libhdf5-dev python3-numpy python3-dev libtiff5-dev unzip libsqlite3-dev default-jdk git cmake libopencv-dev libopenmpi-dev make cmake
+    
+    # Installing CUDA
+    - name: Install CUDA
+      uses: Jimver/cuda-toolkit@v0.2.10
+      id: cuda-toolkit
+      with:
+        cuda: '11.7.0'
+        linux-local-args: '[ "--toolkit" ]'
+
+    # Installing Miniconda
+    - name: Install Miniconda
+      working-directory: ${{ github.workspace }}/../
+      run: |
+        wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
+        bash Miniconda3-latest-Linux-x86_64.sh -b -p ${{ github.workspace }}/../miniconda/
+        source ./miniconda/etc/profile.d/conda.sh
+        conda update -n base -c defaults conda -y
+        
+    # Installing Scipion
+    - name: Install Scipion with dependencies
+      working-directory: ${{ github.workspace }}/../
+      run: |
+        eval "$(${{ github.workspace }}/../miniconda/bin/conda shell.bash hook)"
+        pip3 install --user scipion-installer
+        python3 -m scipioninstaller -conda -noXmipp -noAsk scipion
+    
+    # Installing Xmipp's Python dependencies in scipion conda enviroment
+    - name: Install scons in scipion enviroment
+      run: |
+        eval "$(${{ github.workspace }}/../miniconda/bin/conda shell.bash hook)"
+        conda activate scipion3
+        pip install scons
+        
+    # Cloning Xmipp 
+    - name: Cloning Xmipp
+      working-directory: ${{ github.workspace }}/../
+      run: git clone https://github.com/I2PC/xmipp.git
+    
+    # Checkout Xmipp to Pull Request branch if exists, by default stays in devel
+    - name: Conditionally checkout Xmipp to ${{ github.head_ref }}
+      working-directory: ${{ github.workspace }}/../xmipp
+      env:
+        BRANCH_NAME: ${{ github.head_ref }}
+      run: |
+        if [ $(git ls-remote --heads https://github.com/I2PC/xmipp.git $BRANCH_NAME | wc -l) -eq 1 ]; then
+          git checkout $BRANCH_NAME
+        fi
+      
+    # Installing Xmipp
+    - name: Compile Xmipp and show log
+      working-directory: ${{ github.workspace }}/../xmipp
+      env:
+        BUILD_TESTS: True
+      run: |
+        ../scipion/scipion3 run ./xmipp
+        cat compileLOG.txt
+    
+    # Check out the repository in the pull request
+    - name: Checkout repository
+      uses: actions/checkout@v3
+      with:
+        ref: ${{ github.head_ref }}
+            
+    # Install plugin from the pull request using the Scipion3 installp command
+    - name: Install plugin from pull request
+      working-directory: ${{ github.workspace }}
+      run: ../scipion/scipion3 installp -p . --devel --noBin
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 000000000..d46d88e90
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,186 @@
+
+## Release 3.23.07 - 
+   - New protocols
+      - Movie Dose analysis
+      - deep_center
+      - deep_global_assignment
+      - deep_center_predict
+      - deep_global_assignment_predict
+   - Protocols updated
+      - consensus_classes (Efficient p-value calculation, updated intersection merging process, generalized protocol for other set of classes)
+      - Movie Gain: changed _stepsCheckSecs and fixed inputMovies calling, np.asscalar discontinued in numpy 1.16
+      - convert_pdb: dont allow set size if template volume, to convert a set of pdbs to volumes, generates an mrc file
+      - CTF_consensus: add 4 threads by default
+      - process: Better instantiation of Scipion subclasses
+      - create_mask3d: Addding a validate in 3dmask, add :mrc to input filename
+      - consensus_local_ctf: save defocus in proper fields,  compute consensus for local defocus U and V separately, add consensus angle
+      - align_volume: Included the label in the volumes
+      - crop_resize: Add mask as input. Mask resize is now possible
+      - subtract_projection: change pad validation error for warning, parallelized
+   - Protocols fixed
+      - Tilt analysis: Close correctly the output sets once finished
+      - Deep micrograph cleaner: fix two bugs that occured during streaming implementation bug 
+      - volume_adjust_sub: fix with :mrc
+      - Picking consensus: define correctly the possibleOutputs bug 
+      - Center particles: streaming bug when definining the outputs bug
+      - volume_subtraction: bug fixed in filename
+      - compare_reprojections: fix update subtract projection output
+      - deep_micrograph_screen: Bug fix that prevents using small GPUs
+      - consensus_classes:Fixed manual output generation
+   - Protocols deprecated (For more details visit [this](https://github.com/I2PC/xmipp/wiki/Deprecating-programs-and-protocols))
+      - apply_deformation_zernike3d
+      - classify_kmeans2d
+      - kmeans_clustering
+      - particle_boxSize
+      - rotational_spectra
+      - split_volume_hierarchical_cluster
+   - Viewers
+      - viewer_resolution_fs: fixing 0.1 threshold not found
+      - viewer_projmatch, viewer_metaprotocol_golden_highres: Fixing viewers, change removed ChimeraClientView to ChimeraView
+      - monores_viewer: fix histogram
+      - viewer_structure_map: Change the label for each volume
+
+
+## Release 3.23.03 - Kratos
+  - New protocol status: beta, new, production and updated. Will appear in the left pannel of Scipion 
+  - Protocol subtract_projection: user experience improvements, no final mask by default, apply ciruclar mask in adjustment image to avoid edge artifacts, validate same sampling rate with tolerance in third decimal
+  - Protocol convert_pdb: Allowed to save centered PDB used for conversion. 
+  - Protocol align_volume_and_particles: add alingment validation
+  - Protocol FlexAlign: updating protocol to reflect changes in the executable, fixed test, removing unused protocol (Movie average)
+  - Protocol align_volume_and_particles:Align volume and particles adapted to tomography and works in the absence of tomo plugin.
+  - Protocol volume_consensus: validate same sampling rate with tolerance in third decimal
+  - Protocols deprecated (for more details visit the [wiki](https://github.com/I2PC/xmipp/wiki/Deprecating-programs)): protocol_deep _align, reconstruct_heterogeneous, protocol_metaprotocol_create_output, protocol_metaprotocol_discrete_heterogeneity_scheduler
+
+  
+## Hot fix 3.22.11.2
+- Align volume and particles works in the absence of tomo plugin.
+
+## Hot fix 3.22.11.1
+- Align volume and particles adapted to tomography. Defines possible outputs. Optimized. Test more exhaustive for matrices
+
+## Release 3.22.11 - Iris
+  - Protocol_cl2d_align: The input can now be a set of averages or a set of 2D classes 
+  - Protocol_local_ctf: Default value are now changed for maxDefocusChange
+  - Protocol_apply_zernike3d: Now accepts either a Volume or SetOfVolumes and applies the coefficients in a loop in the deform step
+  - Protocol_postProcessing_deepPostProcessing: Managed GPU memory to avoid errors
+  - Protocol_resolution_deepres: Mandatory mask
+  - Protocol center particles and Gl2d (all options): Fix streaming
+  - Protocol_create_3d_mask: Allows volume Null=True
+  - Protocol_reconstruct_fourier: Set pixel size
+  - GL2D static: Bug fixing
+  - Protocol_trigger_data: Bug fixing
+  - Protocol_crop_resize: Set sampling rate of mrc files when cropping resizing volumes or particles
+  - subtract_projection: New protocol for boosting particles. Add protocol to wizard XmippParticleMaskRadiusWizard as now the protocol uses it
+
+  - **New tests:** deep_hand, pick_noise, screen_deep_learning, resolution_B_factor
+  - Fixed TestHighres test
+
+## Release 3.22.07 - Helios 
+- rotate_volume: New protocol
+- subtract_projection: New implementation based on adjustment by regression instead of POCS and improved performance
+- local_ctf: Add new sameDefocus option + formatting
+- compare_reprojections & protocol_align_volume: Fast Fourier by default
+- crop_resize: Allows input pointers
+- resolution_deepres: Resize output to original size
+- denoise_particles: Added setOfAverages as input option
+- process: Change output from stk (spider) to mrcs (mrc)
+- trigger_data: Bug fixed
+- screen_deeplearning:  Added descriptive help
+- center_particles: Added summary info
+- align_volume_and_particles: Summary error fixed
+- cl2d: Summary errors solved 
+- New tests: test_protocol_reconstruct_fourier, test_protocols_local_defocus, test_protocols_local_defocus, TestXmippAlignVolumeAndParticles,  TestXmippRotateVolume
+- Improved tests: test_protocols_deepVolPostprocessing, test_protocols_xmipp_3d, Test ProjSubtracion
+- Excluded tests: test_protocols_zernike3d, test_protocols_metaprotocol_heterogeneity
+
+## Release 3.22.04 - Gaia
+- protocol_core_analysis: New protocol
+- protocol_compare_angles: Bug fix in compare angles under some conditions
+- protocol_center_particles: protocol simplified (removed setofCoordinates as output)
+- protocol_CTF_consensus: concurrency error fixed
+- protocol_convert_pdb: remove size if deactivated
+- protocol_resolution_deepres: binary masked not stored in Extra folder and avoiding memory problems on GPUs
+- protocol_add_noise: fixes
+- protocol_compare_reprojections: improve computation of residuals + tests + fix + formatting
+- protocol_screen_deepConsensus: multiple fixes in batch processing, trainging and streaming mode
+- protocol_shift_particles: apply transform is now optional
+
+
+## Release 3.22.01 - Eris
+- Visit changeLog.md of xmipp
+
+## Release 3.21.06 - Caerus
+
+- CUDA-11 support
+- New protocol: Deep align
+- ChimeraX support
+- Improvements of streaming process
+- Several performance optimizations
+- Build time optimization
+- Multiple bug fixes
+- Improved documentation
+
+
+## Release 3.20.07 - Boreas
+
+- New Protocol: MicrographCleaner is a new algorithm that removes coordinates picked from carbon edges, aggregations, ice crystals and other contaminations
+- New functionality: The protocol compare reprojections can now compute the residuals after alignment
+- New protocol: Split frames divide input movies into odd and even movies so that they can be processed independently
+- New protocol: Continuous heterogeneity analysis using spherical harmonics (not ready to be used)
+- Bug fixing when some micrograph has no coordinates in the consensus-picking.
+- New functionalities: Different architectures and training modes
+- Normal Mode Analysis protocols have been moved to the plugin ContinuousFlex
+- Fixing MPI version of the Fourier Reconstruction
+- New protocol: local CTF integration and consensus protocol for local ctf (also the viewers)
+- Local CTF analysis tools: Not yet ready for general public
+- New functionallity: Introducing the posibility of automatic estimation of the gain orientation.
+- Bugs fixings regarding stability on streaming processing
+- Support of heterogeneous movie sets
+- New protocol: Clustering of subtomogram coordinates into connected components that can be processed independently
+- New Protocol: Removing duplicated coordinates
+- New protocol: Subtomograms can be projected in several ways to 2D images so that 2D clustering tools can be used
+- New protocol: Regions of Interest can be defined in tomograms (e.g., membranes)
+- Bug fixing in mask3d protocol
+- Bug fix: in helical search symmetry protocol
+- Enhanced precision of the FlexAlign program
+- Now, deepLearningToolkit is under its own conda environment
+- Multiple protocols accelerated using GPU
+- New functionality: Xmipp CTF estimation can now take a previous defocus and do not change it
+- New functionallity: CTF-consensus is able to take the primary main values or an average of the two.
+- New functionallity: CTF-consensus is able to append metadata from the secondary input
+- New functionality: Xmipp Highres can now work with non-phase flipped images
+- New functionality: Xmipp Preprocess particles can now phase flip the images
+- New protocol: Tool to evaluate the quality of a map-model fitting
+- Allowing multi-GPU processing using FlexAlign
+- Improvement in monores and localdeblur
+- Randomize phases also available for images
+- Change the plugin to the new Scipion structure
+- Migrating the code to python3
+
+## Release 3.19.04 -
+
+- Highres can now take a global alignment performed by any other method
+- New protocol: 3D bionotes
+- New protocol: Align volume and particles
+- New protocol: Center particles
+- New protocols: GL2D, GL2D streaming and GL2D static
+- New protocol: 2D kmeans clustering
+- New protocol: compare angles
+- New protocol: consensus 3D classes
+- New protocol: CTF consensus
+- New protocol: deep denoising
+- New protocols: Eliminate empty particles and eliminate empty classes
+- New protocol: Extract unit cell
+- New protocol: Generate reprojections
+- New protocol: metaprotocol heterogenety output, metaprotocol heterogeneity subset and metaprotocol heterogeneity
+- New protocol: Movie Max Shift
+- New protocol: particle boxsize
+- New protocol: pick noise
+- New protocol: significant heterogeneity
+- New protocol: swarm consensus intial volumes
+- New protocol: directional ResDir
+- New protocol: local monoTomo
+- New protocol: deep consensus picking
+- New protocol: screen deep learning
+- New protocol: split volume hierarchical
+- New protocol: trigger data
diff --git a/CHANGES.txt b/CHANGES.txt
deleted file mode 100644
index 7486c2923..000000000
--- a/CHANGES.txt
+++ /dev/null
@@ -1,81 +0,0 @@
-## Hot fix 3.22.11.2
-- Align volume and particles works in the absence of tomo plugin.
-
-## Hot fix 3.22.11.1
-- Align volume and particles adapted to tomography. Defines possible outputs. Optimized. Test more exhaustive for matrices
-
-## Release 3.21.06 -
-
-- CUDA-11 support
-- New protocol: Deep align
-- ChimeraX support
-- Improvements of streaming process
-- Several performance optimizations
-- Build time optimization
-- Multiple bug fixes
-- Improved documentation
-
-
-## Release 3.20.07 -
-
-- New Protocol: MicrographCleaner is a new algorithm that removes coordinates picked from carbon edges, aggregations, ice crystals and other contaminations
-- New functionality: The protocol compare reprojections can now compute the residuals after alignment
-- New protocol: Split frames divide input movies into odd and even movies so that they can be processed independently
-- New protocol: Continuous heterogeneity analysis using spherical harmonics (not ready to be used)
-- Bug fixing when some micrograph has no coordinates in the consensus-picking.
-- New functionalities: Different architectures and training modes
-- Normal Mode Analysis protocols have been moved to the plugin ContinuousFlex
-- Fixing MPI version of the Fourier Reconstruction
-- New protocol: local CTF integration and consensus protocol for local ctf (also the viewers)
-- Local CTF analysis tools: Not yet ready for general public
-- New functionallity: Introducing the posibility of automatic estimation of the gain orientation.
-- Bugs fixings regarding stability on streaming processing
-- Support of heterogeneous movie sets
-- New protocol: Clustering of subtomogram coordinates into connected components that can be processed independently
-- New Protocol: Removing duplicated coordinates
-- New protocol: Subtomograms can be projected in several ways to 2D images so that 2D clustering tools can be used
-- New protocol: Regions of Interest can be defined in tomograms (e.g., membranes)
-- Bug fixing in mask3d protocol
-- Bug fix: in helical search symmetry protocol
-- Enhanced precision of the FlexAlign program
-- Now, deepLearningToolkit is under its own conda environment
-- Multiple protocols accelerated using GPU
-- New functionality: Xmipp CTF estimation can now take a previous defocus and do not change it
-- New functionallity: CTF-consensus is able to take the primary main values or an average of the two.
-- New functionallity: CTF-consensus is able to append metadata from the secondary input
-- New functionality: Xmipp Highres can now work with non-phase flipped images
-- New functionality: Xmipp Preprocess particles can now phase flip the images
-- New protocol: Tool to evaluate the quality of a map-model fitting
-- Allowing multi-GPU processing using FlexAlign
-- Improvement in monores and localdeblur
-- Randomize phases also available for images
-- Change the plugin to the new Scipion structure
-- Migrating the code to python3
-
-## Release 3.19.04 -
-
-- Highres can now take a global alignment performed by any other method
-- New protocol: 3D bionotes
-- New protocol: Align volume and particles
-- New protocol: Center particles
-- New protocols: GL2D, GL2D streaming and GL2D static
-- New protocol: 2D kmeans clustering
-- New protocol: compare angles
-- New protocol: consensus 3D classes
-- New protocol: CTF consensus
-- New protocol: deep denoising
-- New protocols: Eliminate empty particles and eliminate empty classes
-- New protocol: Extract unit cell
-- New protocol: Generate reprojections
-- New protocol: metaprotocol heterogenety output, metaprotocol heterogeneity subset and metaprotocol heterogeneity
-- New protocol: Movie Max Shift
-- New protocol: particle boxsize
-- New protocol: pick noise
-- New protocol: significant heterogeneity
-- New protocol: swarm consensus intial volumes
-- New protocol: directional ResDir
-- New protocol: local monoTomo
-- New protocol: deep consensus picking
-- New protocol: screen deep learning
-- New protocol: split volume hierarchical
-- New protocol: trigger data
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index b0da77511..a75229a22 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 scipion-pyworkflow>=3.0.31
+scikit-learn==0.22
 scipion-em
-scikit-learn==0.22.1
-scipy<=1.8.1
+scipy<=1.10.0
 joblib
 peppercorn
 plotter
diff --git a/xmipp3/__init__.py b/xmipp3/__init__.py
index 54140f003..a9e4f57ff 100644
--- a/xmipp3/__init__.py
+++ b/xmipp3/__init__.py
@@ -38,10 +38,10 @@
 from .constants import XMIPP_HOME, XMIPP_URL, XMIPP_DLTK_NAME
 
 _logo = "xmipp_logo.png"
-_references = ['delaRosaTrevin2013', 'Sorzano2013']
-_currentBinVersion = '3.23.03.0'
-# This will allow scipion to detect the plugin version and print it in the log.
-__version__ = _currentBinVersion[2:] + ".3"  # Set this to ".0" on each xmipp binary release, otherwise increase it --> ".1", ".2", ...
+_references = ['delaRosaTrevin2013', 'Sorzano2013', 'Strelak2021']
+_currentBinVersion = '3.23.07.0'
+__version__ = _currentBinVersion[2:] + ".0"  # Set this to ".0" on each xmipp binary release, otherwise increase it --> ".1", ".2", ...
+
 
 
 class Plugin(pwem.Plugin):
@@ -209,35 +209,34 @@ def installDeepLearningToolkit(plugin, env):
                                                env=plugin.getEnviron(),
                                                stdout=subprocess.PIPE
                                                ).stdout.read().decode('utf-8').split(".")[0]
-            try:
-                int(nvidiaDriverVer)
-            except ValueError:
-                nvidiaDriverVer = None
             if int(nvidiaDriverVer) < 390:
                 preMsgs.append("Incompatible driver %s" % nvidiaDriverVer)
                 cudaMsgs.append("Your NVIDIA drivers are too old (<390). "
                                 "Tensorflow was installed without GPU support. "
-                                "Just CPU computations enabled (slow computations).")
+                                "Just CPU computations enabled (slow computations)."
+                                "To enable CUDA (drivers>390 needed), "
+                                "set CUDA=True in 'scipion.conf' file")
                 nvidiaDriverVer = None
-        except Exception as e:
-            preMsgs.append(str(e))
+        except (ValueError, TypeError):
+            nvidiaDriverVer = None
+            preMsgs.append("Not nvidia driver found. Type: "
+                           " nvidia-smi --query-gpu=driver_version --format=csv,noheader")
+            preMsgs.append(
+                "CUDA will NOT be USED. (not found or incompatible)")
+            msg = ("Tensorflow installed without GPU. Just CPU computations "
+                   "enabled (slow computations).")
+            cudaMsgs.append(msg)
+            useGpu = False
 
     if nvidiaDriverVer is not None:
-        preMsgs.append("CUDA support find. Driver version: %s" % nvidiaDriverVer)
-        msg = "Tensorflow installed with CUDA SUPPORT."
+        preMsgs.append("CUDA support found. Driver version: %s" % nvidiaDriverVer)
+        msg = "Tensorflow will be installed with CUDA SUPPORT."
         cudaMsgs.append(msg)
         useGpu = True
-    else:
-        preMsgs.append("CUDA will NOT be USED. (not found or incompatible)")
-        msg = ("Tensorflow installed without GPU. Just CPU computations "
-               "enabled (slow computations). To enable CUDA (drivers>390 needed), "
-               "set CUDA=True in 'scipion.conf' file")
-        cudaMsgs.append(msg)
-        useGpu = False
+
 
     # commands  = [(command, target), (cmd, tgt), ...]
-    cmdsInstall = [(cmd, envName + ".yml") for cmd, envName in
-                   CondaEnvManager.yieldInstallAllCmds(useGpu=useGpu)]
+    cmdsInstall = list(CondaEnvManager.yieldInstallAllCmds(useGpu=useGpu))
 
     now = datetime.now()
     installDLvars = {'modelsUrl': "http://scipion.cnb.csic.es/downloads/scipion/software/em",
@@ -267,6 +266,6 @@ def installDeepLearningToolkit(plugin, env):
                          " fi" % installDLvars,           # End of command
                          installDLvars['xmippLibToken'])  # Target
 
-    env.addPackage(XMIPP_DLTK_NAME, version='0.2', urlSuffix='external',
+    env.addPackage(XMIPP_DLTK_NAME, version='1.0', urlSuffix='external',
                    commands=[xmippInstallCheck]+cmdsInstall+[modelsDownloadCmd],
                    deps=[], tar=XMIPP_DLTK_NAME+'.tgz')
diff --git a/xmipp3/bibtex.py b/xmipp3/bibtex.py
index 0e3e465b6..c1bca0e78 100644
--- a/xmipp3/bibtex.py
+++ b/xmipp3/bibtex.py
@@ -604,5 +604,21 @@
 author = {E. Fernández-Giménez and M. Martínez and R. Sánchez-García and R. Marabini and E. Ramírez-Aportela and
 P. Conesa and J.M. Carazo and C.O.S. Sorzano},
 }
+
+@Article{Strelak2021,
+AUTHOR = {Strelak, David and Jiménez-Moreno, Amaya and Vilas, José L. and Ramírez-Aportela, Erney and Sánchez-García, Ruben and Maluenda, David and Vargas, Javier and Herreros, David and Fernández-Giménez, Estrella and de Isidro-Gómez, Federico P. and Horacek, Jan and Myska, David and Horacek, Martin and Conesa, Pablo and Fonseca-Reyna, Yunior C. and Jiménez, Jorge and Martínez, Marta and Harastani, Mohamad and Jonić, Slavica and Filipovic, Jiri and Marabini, Roberto and Carazo, José M. and Sorzano, Carlos O. S.},
+TITLE = {Advances in Xmipp for Cryo–Electron Microscopy: From Xmipp to Scipion},
+JOURNAL = {Molecules},
+VOLUME = {26},
+YEAR = {2021},
+NUMBER = {20},
+ARTICLE-NUMBER = {6224},
+URL = {https://www.mdpi.com/1420-3049/26/20/6224},
+PubMedID = {34684805},
+ISSN = {1420-3049},
+DOI = {https://doi.org/10.3390/molecules26206224}
+}
+
+
 """
 
diff --git a/xmipp3/protocols/protocol_apply_deformation_zernike3d.py b/xmipp3/legacy/protocols/protocol_apply_deformation_zernike3d.py
similarity index 100%
rename from xmipp3/protocols/protocol_apply_deformation_zernike3d.py
rename to xmipp3/legacy/protocols/protocol_apply_deformation_zernike3d.py
diff --git a/xmipp3/protocols/protocol_classify_kmeans2d.py b/xmipp3/legacy/protocols/protocol_classify_kmeans2d.py
similarity index 100%
rename from xmipp3/protocols/protocol_classify_kmeans2d.py
rename to xmipp3/legacy/protocols/protocol_classify_kmeans2d.py
diff --git a/xmipp3/protocols/protocol_kmeans_clustering.py b/xmipp3/legacy/protocols/protocol_kmeans_clustering.py
similarity index 100%
rename from xmipp3/protocols/protocol_kmeans_clustering.py
rename to xmipp3/legacy/protocols/protocol_kmeans_clustering.py
diff --git a/xmipp3/protocols/protocol_particle_boxsize.py b/xmipp3/legacy/protocols/protocol_particle_boxsize.py
similarity index 100%
rename from xmipp3/protocols/protocol_particle_boxsize.py
rename to xmipp3/legacy/protocols/protocol_particle_boxsize.py
diff --git a/xmipp3/protocols/protocol_rotational_spectra.py b/xmipp3/legacy/protocols/protocol_rotational_spectra.py
similarity index 99%
rename from xmipp3/protocols/protocol_rotational_spectra.py
rename to xmipp3/legacy/protocols/protocol_rotational_spectra.py
index 6131cb066..1d6713fe2 100644
--- a/xmipp3/protocols/protocol_rotational_spectra.py
+++ b/xmipp3/legacy/protocols/protocol_rotational_spectra.py
@@ -1,4 +1,3 @@
-# **************************************************************************
 # *
 # * Authors:     Josue Gomez Blanco (josue.gomez-blanco@mcgill.ca)
 # *
diff --git a/xmipp3/protocols/protocol_split_volume_hierarchical_cluster.py b/xmipp3/legacy/protocols/protocol_split_volume_hierarchical_cluster.py
similarity index 100%
rename from xmipp3/protocols/protocol_split_volume_hierarchical_cluster.py
rename to xmipp3/legacy/protocols/protocol_split_volume_hierarchical_cluster.py
diff --git a/xmipp3/legacy/viewers/viewer_mltomo.py b/xmipp3/legacy/viewers/viewer_mltomo.py
index 88ab6dc6a..5c4c973a0 100644
--- a/xmipp3/legacy/viewers/viewer_mltomo.py
+++ b/xmipp3/legacy/viewers/viewer_mltomo.py
@@ -32,7 +32,7 @@
 
 from pwem.objects import FSC
 from pwem.viewers import (DataView, Classes3DView, FscViewer, ObjectView,
-                          ChimeraView, ChimeraClientView)
+                          ChimeraView)
 import pwem.viewers.showj as showj
 import pwem.emlib.metadata as md
 from pyworkflow.protocol.params import EnumParam, NumericRangeParam, LabelParam
@@ -186,7 +186,7 @@ def _showVolumesChimera(self):
             f.close()
             view = ChimeraView(cmdFile)
         else:
-            view = ChimeraClientView(volumes[0])
+            view = ChimeraView(volumes[0])
 
         return [view]
 
diff --git a/xmipp3/protocols.conf b/xmipp3/protocols.conf
index be3d5889c..519d3db6f 100644
--- a/xmipp3/protocols.conf
+++ b/xmipp3/protocols.conf
@@ -5,7 +5,8 @@ Protocols SPA = [
 		{"tag": "protocol", "value": "XmippProtOFAlignment", "text": "default"},
 		{"tag": "protocol", "value": "XmippProtMovieMaxShift", "text": "default"},
 		{"tag": "protocol", "value": "XmippProtSplitFrames", "text": "default"}	,
-		{"tag": "protocol", "value": "XmippProtMovieGain", "text": "default"}
+		{"tag": "protocol", "value": "XmippProtMovieGain", "text": "default"},
+		{"tag": "protocol", "value": "XmippProtMovieDoseAnalysis", "text": "default"}
 	]},
 	{"tag": "section", "text": "Micrographs", "openItem": "False", "children": [
 		{"tag": "protocol_group", "text": "Preprocess", "openItem": "False", "children": [
@@ -19,7 +20,6 @@ Protocols SPA = [
 	]},
 	{"tag": "section", "text": "Particles", "children": [
 		{"tag": "protocol_group", "text": "Picking", "openItem": "False", "children": [
-		{"tag": "protocol", "value": "XmippProtParticleBoxsize",   "text": "default"},
 		{"tag": "protocol", "value": "XmippProtParticlePicking",   "text": "default"},
 		{"tag": "protocol", "value": "XmippParticlePickingAutomatic",   "text": "default"},
 		{"tag": "protocol", "value": "XmippProtScreenDeepConsensus",   "text": "default"},
@@ -69,8 +69,7 @@ Protocols SPA = [
 			{"tag": "protocol", "value": "XmippProtStrGpuCrrSimple",   "text": "default"},
 			{"tag": "section", "text": "more", "openItem": "False", "children": [
 				{"tag": "protocol", "value": "XmippProtML2D", "text": "default"},
-				{"tag": "protocol", "value": "XmippProtKerdensom", "text": "default"},
-				{"tag": "protocol", "value": "XmippProtRotSpectra", "text": "default"}				
+				{"tag": "protocol", "value": "XmippProtKerdensom", "text": "default"}
 			]}
 		]}
 	]},
@@ -132,6 +131,7 @@ Protocols SPA = [
 		{"tag": "protocol", "value": "XmippProtMonoRes",   "text": "default"},
 		{"tag": "protocol", "value": "XmippProtDeepRes",   "text": "default"},		
 		{"tag": "protocol", "value": "XmippProtResolution3D",  "text": "default"},
+		{"tag": "protocol", "value": "XmippProtFSO",  "text": "default"},
 		{"tag": "protocol", "value": "XmippProtMultipleFSCs",  "text": "default"},
 		{"tag": "protocol", "value": "XmippProtMonoDir",  "text": "default"},
         {"tag": "protocol", "value": "XmippProtMonoTomo", "text": "default"}
diff --git a/xmipp3/protocols/__init__.py b/xmipp3/protocols/__init__.py
index 9d62179a4..38d254ec8 100644
--- a/xmipp3/protocols/__init__.py
+++ b/xmipp3/protocols/__init__.py
@@ -31,6 +31,7 @@
 from .protocol_assignment_tilt_pair import XmippProtAssignmentTiltPair
 from .protocol_align_volume import XmippProtAlignVolume, XmippProtAlignVolumeForWeb
 from .protocol_angular_graph_consistency import XmippProtAngularGraphConsistency
+from .protocol_angular_resolution_alignment import XmippProtResolutionAlignment
 from .protocol_preprocess.protocol_add_noise import (XmippProtAddNoiseVolumes,
                                                      XmippProtAddNoiseParticles)
 from .protocol_apply_alignment import XmippProtApplyAlignment
@@ -38,7 +39,7 @@
 from .protocol_break_symmetry import XmippProtAngBreakSymmetry
 from .protocol_cl2d_align import XmippProtCL2DAlign
 from .protocol_cl2d import XmippProtCL2D
-from .protocol_classify_kmeans2d import XmippProtKmeansClassif2D
+#from .protocol_classify_kmeans2d import XmippProtKmeansClassif2D
 from .protocol_classification_gpuCorr import XmippProtGpuCrrCL2D
 from .protocol_classification_gpuCorr_semi import XmippProtStrGpuCrrSimple
 from .protocol_classification_gpuCorr_full import XmippProtStrGpuCrrCL2D
@@ -51,7 +52,7 @@
 from .protocol_ctf_consensus import XmippProtCTFConsensus
 from .protocol_ctf_micrographs import XmippProtCTFMicrographs
 from .protocol_ctf_correct_wiener2d import XmippProtCTFCorrectWiener2D
-from .protocol_consensus_classes3D import XmippProtConsensusClasses3D
+from .protocol_consensus_classes import XmippProtConsensusClasses
 from .protocol_denoise_particles import XmippProtDenoiseParticles
 from .protocol_deep_micrograph_screen import  XmippProtDeepMicrographScreen
 from .protocol_eliminate_empty_images import (XmippProtEliminateEmptyParticles,
@@ -68,6 +69,7 @@
 from .protocol_flexalign import XmippProtFlexAlign
 from .protocol_movie_opticalflow import XmippProtOFAlignment, ProtMovieAlignment
 from .protocol_movie_max_shift import XmippProtMovieMaxShift
+from .protocol_movie_dose_analysis import XmippProtMovieDoseAnalysis
 from .protocol_movie_split_frames import XmippProtSplitFrames
 from .protocol_multiple_fscs import XmippProtMultipleFSCs
 from .protocol_multireference_alignability import XmippProtMultiRefAlignability
@@ -75,7 +77,7 @@
 from .protocol_particle_pick_automatic import XmippParticlePickingAutomatic
 from .protocol_particle_pick_consensus import XmippProtConsensusPicking
 from .protocol_pick_noise import XmippProtPickNoise
-from .protocol_particle_boxsize import XmippProtParticleBoxsize
+#from .protocol_particle_boxsize import XmippProtParticleBoxsize
 from .protocol_particle_pick import XmippProtParticlePicking
 from .protocol_particle_pick_pairs import XmippProtParticlePickingPairs
 from .protocol_phantom_create import XmippProtPhantom
@@ -96,7 +98,7 @@
 from .protocol_resolution_deepres import XmippProtDeepRes
 from .protocol_postProcessing_deepPostProcessing import XmippProtDeepVolPostProc
 from .protocol_rotate_volume import XmippProtRotateVolume
-from .protocol_rotational_spectra import XmippProtRotSpectra
+#from .protocol_rotational_spectra import XmippProtRotSpectra
 from .protocol_rotational_symmetry import XmippProtRotationalSymmetry
 from .protocol_screen_particles import XmippProtScreenParticles
 from .protocol_screen_deepConsensus import XmippProtScreenDeepConsensus, XmippProtDeepConsSubSet
@@ -119,7 +121,6 @@
 from .protocol_write_testP import XmippProtWriteTestP
 from .protocol_generate_reprojections import XmippProtGenerateReprojections
 from .protocol_deep_denoising import XmippProtDeepDenoising  # deepDenoising has to be after XmippProtGenerateReprojections and XmippProtCompareReprojections
-from .protocol_split_volume_hierarchical_cluster import XmippProtSplitVolumeHierarchical
 from .protocol_metaprotocol_create_subset import XmippMetaProtCreateSubset
 from .protocol_volume_deform_zernike3d import XmippProtVolumeDeformZernike3D
 from .protocol_structure_map_zernike3d import XmippProtStructureMapZernike3D
@@ -137,3 +138,7 @@
 from .protocol_volume_consensus import XmippProtVolConsensus
 from .protocol_classes_2d_mapping import XmippProtCL2DMap
 from .protocol_deep_hand import XmippProtDeepHand
+from .protocol_deep_center import XmippProtDeepCenter
+from .protocol_deep_center_predict import XmippProtDeepCenterPredict
+from .protocol_deep_global_assignment import XmippProtDeepGlobalAssignment
+from .protocol_deep_global_assignment_predict import XmippProtDeepGlobalAssignmentPredict
diff --git a/xmipp3/protocols/protocol_align_volume.py b/xmipp3/protocols/protocol_align_volume.py
index 24a62400c..6c03ebbb5 100644
--- a/xmipp3/protocols/protocol_align_volume.py
+++ b/xmipp3/protocols/protocol_align_volume.py
@@ -32,6 +32,7 @@
 from pwem.objects import Volume, Transform, SetOfVolumes
 
 from xmipp3.convert import getImageLocation
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 
 ALIGN_MASK_CIRCULAR = 0
@@ -51,6 +52,8 @@ class XmippProtAlignVolume(ProtAlignVolume):
      """
     _label = 'align volume'
     nVols = 0
+    _devStatus = UPDATED
+
     
     def __init__(self, **args):
         ProtAlignVolume.__init__(self, **args)
@@ -200,7 +203,9 @@ def createOutputStep(self):
             fnOutVol = self._getExtraPath("vol%02d.mrc"%idx)
             outVol.setLocation(fnOutVol)
             outVol.setObjComment(vol.getObjComment())
-            #set transformation matrix             
+            outVol.setObjLabel(vol.getObjLabel())
+
+            #set transformation matrix
             fhInputTranMat = self._getExtraPath('transformation-matrix_vol%06d.txt'%idx)
             transMatFromFile = np.loadtxt(fhInputTranMat)
             transformationMat = np.reshape(transMatFromFile,(4,4))
@@ -329,7 +334,7 @@ def _getMaskArgs(self):
         return maskArgs
     
     def _getAlignArgs(self):
-        alignArgs = ''
+        alignArgs = ' --dontWrap'
         
         if self.alignmentAlgorithm == ALIGN_ALGORITHM_FAST_FOURIER:
             alignArgs += " --frm"
diff --git a/xmipp3/protocols/protocol_align_volume_and_particles.py b/xmipp3/protocols/protocol_align_volume_and_particles.py
index be88ff13d..49dd67078 100644
--- a/xmipp3/protocols/protocol_align_volume_and_particles.py
+++ b/xmipp3/protocols/protocol_align_volume_and_particles.py
@@ -256,7 +256,7 @@ def _getMaskArgs(self):
             if self.maskType == ALIGN_MASK_CIRCULAR:
                 maskArgs+=" --mask circular -%d" % self.maskRadius
             else:
-                maskArgs+=" --mask binary_file %s" % self.volMask
+                maskArgs+=" --mask binary_file %s" % self.maskFile.get().getFileName()
         return maskArgs
 
           
diff --git a/xmipp3/protocols/protocol_angular_resolution_alignment.py b/xmipp3/protocols/protocol_angular_resolution_alignment.py
new file mode 100755
index 000000000..af4ba0ac5
--- /dev/null
+++ b/xmipp3/protocols/protocol_angular_resolution_alignment.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+# **************************************************************************
+# *
+# * Authors:     Jose Luis Vilas (jlvilas@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+
+import os
+from pyworkflow import VERSION_2_0
+from pyworkflow.utils import getExt
+from pyworkflow.protocol.params import (PointerParam, BooleanParam, FloatParam,
+                                        LEVEL_ADVANCED)
+from pwem.protocols import ProtAnalysis3D
+
+RADIAL_RESOLUTION_FN = 'radial_FSC_resolution.xmd'
+
+class XmippProtResolutionAlignment(ProtAnalysis3D):
+    """    
+    Given two half maps the protocol estimates if the reconstruction presents angular
+    alignment errors. To do that, a set of directional FSC along all possible directions
+    are estimated. The result is a curve Resolution-radius. If this curve presents a slope
+    then the map present angular assignment errors, but it the graph is flat (horizontal), the map
+    is error free. Note that this protocol generates a plot, not a Scipion object. Its result
+    can only be visualized.
+    """
+    _label = 'resolution alignment'
+    _lastUpdateVersion = VERSION_2_0
+
+    def __init__(self, **args):
+        ProtAnalysis3D.__init__(self, **args)
+
+    # --------------------------- DEFINE param functions ----------------------
+    def _defineParams(self, form):
+        form.addSection(label='Input')
+
+        form.addParam('halfVolumesFile', BooleanParam, default=False,
+                      label="Are the half volumes stored with the input volume?",
+                      help='Usually, the half volumes are stored as properties of '
+                           'the input volume. If this is not the case, set this to '
+                           'False and specify the two halves you want to use.')
+
+        form.addParam('inputHalves', PointerParam, pointerClass='Volume',
+                      label="Input Half Maps",
+                      condition='halfVolumesFile',
+                      help='Select a half maps for determining its '
+                           ' resolution anisotropy and resolution.')
+
+        form.addParam('half1', PointerParam, pointerClass='Volume',
+                      condition="not halfVolumesFile",
+                      label="Half Map 1", important=True,
+                      help='Select one map for determining the '
+                           'directional FSC resolution.')
+
+        form.addParam('half2', PointerParam, pointerClass='Volume',
+                      condition="not halfVolumesFile",
+                      label="Half Map 2", important=True,
+                      help='Select the second map for determining the '
+                           'directional FSC resolution.')
+
+        form.addParam('mask', PointerParam, pointerClass='VolumeMask',
+                      allowsNull=True,
+                      label="Mask",
+                      help='The mask determines which points are specimen'
+                           ' and which are not')
+
+        form.addParam('helicalReconstruction', BooleanParam, default=False,
+                      label="Is a the protein a helix",
+                      help='blablabla')
+
+        form.addParam('limRadius', BooleanParam, default=True,
+                      expertLevel=LEVEL_ADVANCED,
+                      label="Limit the protein radius",
+                      help='blablabla')
+
+        form.addParam('usedirectionalfsc', BooleanParam, default=True,
+                      expertLevel=LEVEL_ADVANCED,
+                      label="use directional fsc",
+                      help='blablabla')
+
+        form.addParam('coneAngle', FloatParam, default=17.0,
+                      expertLevel=LEVEL_ADVANCED,
+                      label="Cone Angle",
+                      help='Angle between the axis of the cone and the generatrix. '
+                           'An angle of 17 degrees is the best angle (see publication'
+                           'Vilas 2021) to measuare directional FSCs')
+
+        form.addParam('threshold', FloatParam, expertLevel=LEVEL_ADVANCED,
+                      default=0.143,
+                      label="FSC Threshold",
+                      help='Threshold for the fsc. By default the standard 0.143. '
+                           'Other common thresholds are 0.5 and 0.3.')
+
+        form.addParallelSection(threads=4, mpi=0)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertAllSteps(self):
+        self._insertFunctionStep(self.convertInputStep)
+        self._insertFunctionStep(self.angularResolutionAlignmentStep)
+
+    def convertInputStep(self):
+        """ This function sets the maps properly into mrc
+        """
+
+        if self.halfVolumesFile:
+            self.vol1Fn, self.vol2Fn = self.inputHalves.get().getHalfMaps().split(',')
+        else:
+            self.vol1Fn = self.half1.get().getFileName()
+            self.vol2Fn = self.half2.get().getFileName()
+
+        extVol1 = getExt(self.vol1Fn)
+        extVol2 = getExt(self.vol2Fn)
+
+        if (extVol1 == '.mrc') or (extVol1 == '.map'):
+            self.vol1Fn = self.vol1Fn + ':mrc'
+        if (extVol2 == '.mrc') or (extVol2 == '.map'):
+            self.vol2Fn = self.vol2Fn + ':mrc'
+
+        if self.mask.hasValue():
+            self.maskFn = self.mask.get().getFileName()
+            extMask = getExt(self.maskFn)
+            if (extMask == '.mrc') or (extMask == '.map'):
+                self.maskFn = self.maskFn + ':mrc'
+
+    def angularResolutionAlignmentStep(self):
+        """
+        This function runs the algorithm to detect misalignment
+        """
+        fndir = self._getExtraPath("fsc")
+
+        os.mkdir(fndir)
+
+        params = ' --half1 "%s"' % self.vol1Fn
+        params += ' --half2 "%s"' % self.vol2Fn
+        params += ' -o %s' % self._getExtraPath(RADIAL_RESOLUTION_FN)
+        if self.halfVolumesFile:
+            params += ' --sampling %f' % self.inputHalves.get().getSamplingRate()
+        else:
+            params += ' --sampling %f' % self.half1.get().getSamplingRate()
+
+        if self.helicalReconstruction.get():
+            params += ' --helix '
+
+        if self.mask.hasValue():
+            params += ' --mask "%s"' % self.maskFn
+        if self.limRadius.get():
+            params += ' --limit_radius '
+        if self.usedirectionalfsc.get():
+            params += ' --directional_resolution '
+            params += ' --anglecone %f' % self.coneAngle.get()
+
+        params += ' --threshold %s' % self.threshold.get()
+        params += ' --threads %s' % self.numberOfThreads.get()
+
+        self.runJob('xmipp_angular_resolution_alignment', params)
+
+    # --------------------------- INFO functions ------------------------------
+
+    def _methods(self):
+        messages = []
+        ARTICLE_URL = 'Pending on publication'
+        messages.append('Information about the method/article in ' + ARTICLE_URL)
+
+        return messages
+
+    def _validate(self):
+        errors = []
+
+        if self.halfVolumesFile.get():
+            if not self.inputHalves.get():
+                errors.append("You need to select the Associated halves")
+        else:
+            if not self.half1.get():
+                errors.append("You need to select the half1")
+            if not self.half2.get():
+                errors.append("You need to select the half2")
+
+        return errors
+
+    def _summary(self):
+        summary = []
+        summary.append("This protocol does not produce Scipion Objects as output. Click on Analyze results to visualize the results")
+        return summary
+
diff --git a/xmipp3/protocols/protocol_center_particles.py b/xmipp3/protocols/protocol_center_particles.py
index 713e1f28a..391842a04 100644
--- a/xmipp3/protocols/protocol_center_particles.py
+++ b/xmipp3/protocols/protocol_center_particles.py
@@ -29,7 +29,7 @@
 import os
 from pyworkflow import VERSION_2_0
 from pwem.constants import ALIGN_2D
-from pwem.objects import Class2D, Particle, Coordinate, Transform
+from pwem.objects import Class2D, Particle, Coordinate, Transform, SetOfClasses2D, SetOfParticles
 from pwem.protocols import ProtClassify2D
 import pwem.emlib.metadata as md
 import pyworkflow.protocol.params as params
@@ -38,11 +38,14 @@
 from xmipp3.convert import (rowToAlignment, alignmentToRow,
                             rowToParticle, writeSetOfClasses2D, xmippToLocation)
 
+OUTPUT_CLASSES = 'outputClasses'
+OUTPUT_PARTICLES = 'outputParticles'
 
 class XmippProtCenterParticles(ProtClassify2D):
     """ Realignment of un-centered particles. """
     _label = 'center particles'
     _lastUpdateVersion = VERSION_2_0
+    _possibleOutputs = {OUTPUT_CLASSES:SetOfClasses2D, OUTPUT_PARTICLES:SetOfParticles}
 
     # --------------------------- DEFINE param functions -----------------------
     def _defineParams(self, form):
@@ -176,16 +179,13 @@ def createOutputStep(self):
         inputParticles = self.inputClasses.get().getImages()
         outputClasses = self._createSetOfClasses2D(self.inputClasses.get().getImagesPointer())
         self._fillClasses(outputClasses)
-        result = {'outputClasses': outputClasses}
-        self._defineOutputs(**result)
-        self._defineSourceRelation(self.inputClasses, outputClasses)
-
         outputParticles = self._createSetOfParticles()
         outputParticles.copyInfo(inputParticles)
-
         self._fillParticles(outputParticles)
-        result = {'outputParticles': outputParticles}
+
+        result = {OUTPUT_CLASSES: outputClasses, OUTPUT_PARTICLES: outputParticles}
         self._defineOutputs(**result)
+        self._defineSourceRelation(self.inputClasses, outputClasses)
         self._defineSourceRelation(self.inputClasses, outputParticles)
 
     # --------------------------- UTILS functions ------------------------------
diff --git a/xmipp3/protocols/protocol_compare_reprojections.py b/xmipp3/protocols/protocol_compare_reprojections.py
index 75756be15..03eb274c9 100644
--- a/xmipp3/protocols/protocol_compare_reprojections.py
+++ b/xmipp3/protocols/protocol_compare_reprojections.py
@@ -265,11 +265,11 @@ def _computeResiduals(self, fnVol):
         if fnVol.endswith('.mrc'):
             fnVol += ':mrc'
         program = "xmipp_subtract_projection"
-        args = '-i %s --ref %s -o %s --save %s --max_resolution %f --sigma %d' % \
-               (self.imgsFn, fnVol, self._getExtraPath("residuals"), self._getExtraPath(''), self.resol.get(),
-                self.sigma.get())
+        args = '-i %s --ref %s -o %s --save %s --max_resolution %f --sigma %d --oroot %s' % \
+               (self.imgsFn, fnVol, self._getExtraPath("residuals.xmd"), self._getExtraPath(''), self.resol.get(),
+                self.sigma.get(), self._getExtraPath("residual_part"))
         self.runJob(program, args, numberOfMpi=1)
-        mrcsresiduals = self._getExtraPath("residuals.mrcs")
+        mrcsresiduals = self._getExtraPath("residuals.xmd")
         args2 = " -i %s -o %s" % (mrcsresiduals, self.fnResiduals)
         self.runJob("xmipp_image_convert", args2, numberOfMpi=1)
         fnNewParticles = self._getExtraPath("images.stk")
diff --git a/xmipp3/protocols/protocol_consensus_classes.py b/xmipp3/protocols/protocol_consensus_classes.py
new file mode 100644
index 000000000..7434c7d6a
--- /dev/null
+++ b/xmipp3/protocols/protocol_consensus_classes.py
@@ -0,0 +1,617 @@
+# **************************************************************************
+# * Authors:     David Maluenda (dmaluenda@cnb.csic.es)
+# *              Daniel del Hoyo Gomez (daniel.delhoyo.gomez@alumnos.upm.es)
+# *              Jorge Garcia Condado (jgcondado@cnb.csic.es)
+# *              Oier Lauzirika Zarrabeitia (oierlauzi@bizkaia.eu)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from typing import Iterable, Sequence, Optional, Tuple, List, Set
+import itertools
+
+from pwem.protocols import ProtClassify3D
+from pwem.objects import Pointer, Image, SetOfClasses, SetOfImages
+from pwem import emlib
+
+from pyworkflow.protocol.params import Form, MultiPointerParam, IntParam, EnumParam
+from pyworkflow.protocol import LEVEL_ADVANCED
+from pyworkflow.constants import BETA
+from pyworkflow.object import Float
+
+from xmipp3.convert import setXmippAttribute
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+import os
+import json
+import numpy as np
+import scipy.stats
+import scipy.cluster
+import scipy.spatial
+
+class XmippProtConsensusClasses(ProtClassify3D):
+    """ Compare several SetOfClasses.
+        Return the consensus clustering based on a objective function
+        that uses the similarity between clusters intersections and
+        the entropy of the clustering formed.
+    """
+    _label = 'consensus classes'
+    _devStatus = BETA
+
+    METRICS = [
+        'cosine',
+        'euclidean',
+        'cityblock',
+        'correlation',
+    ]
+
+    def __init__(self, *args, **kwargs):
+        ProtClassify3D.__init__(self, *args, **kwargs)
+        
+    def _defineParams(self, form: Form):
+        # Input data
+        form.addSection(label='Input')
+        form.addParam('inputClassifications', MultiPointerParam, pointerClass='SetOfClasses', 
+                      label="Input classes", important=True,
+                      help='Select several sets of classes where to evaluate the '
+                           'intersections.')
+
+        form.addSection(label='Pruning')
+        form.addParam('minClassSize', IntParam, label="Minimum class size",
+                      default = 0,
+                      help='Minimum output class size. If set to zero it will not have '
+                      'any effect')
+        
+        
+        form.addSection(label='Clustering', expertLevel=LEVEL_ADVANCED)
+        form.addParam('distanceMetric', EnumParam, label='Metric',
+                      choices=self.METRICS, default=0,
+                      help='Distance metric used when comparing clusters')
+        
+
+    # --------------------------- INSERT steps functions -----------------------
+    def _insertAllSteps(self):
+        self._insertFunctionStep('referenceIntersectionStep')
+        self._insertFunctionStep('intersectStep')
+        self._insertFunctionStep('mergeStep')
+        self._insertFunctionStep('findElbowsStep')
+
+    def referenceIntersectionStep(self):
+        sizes = self._getInputClassificationSizes()
+        nImages = sizes[0].sum()
+        sourceProbabilities = self._calculateIntersectionSourceProbabilities(sizes)
+        intersectionProbabilities = self._calculateIntersectionProbabilities(sourceProbabilities)
+        normIntersectionProbabilities = self._calculateNormalizedIntersectionProbabilities(
+            sourceProbabilities, 
+            intersectionProbabilities
+        )
+        intersectionSizes = np.sort(intersectionProbabilities * nImages)
+        normalizedIntersectionSizes = np.sort(normIntersectionProbabilities) # No need to multiply
+
+        # Write
+        self._writeReferenceIntersectionSizes(intersectionSizes, normalizedIntersectionSizes)
+
+    def intersectStep(self):
+        classifications = self._getInputClassifications()
+        classParticleIds = self._getInputClassParticleIds()
+        classIds = self._getInputClassIds()
+        referenceSizes, normalizedReferenceSizes = self._readReferenceIntersectionSizes()
+
+        # Perform the intersections between input classes
+        intersections, traces = self._calculateIntersections(classParticleIds, classIds)
+
+        # Prune if requested
+        intersections = self._pruneIntersections(intersections, int(self.minClassSize))
+
+        # Create the output
+        outputClasses = self._createSetOfClasses(
+            images=self._getInputImages(),
+            classifications=classifications,
+            clustering=intersections,
+            suffix=self._getMergedIntersectionSuffix(len(intersections)),
+            comments=traces,
+            referenceSizes=referenceSizes,
+            normalizedReferenceSizes=normalizedReferenceSizes
+        )
+        
+        self._defineOutputs(outputClasses=outputClasses)
+        self._defineSourceRelation(self.inputClassifications, outputClasses)
+
+    def mergeStep(self):
+        classParticleIds = self._getInputClassParticleIds()
+        allClasses = list(itertools.chain(*classParticleIds))
+        intersections = self._getOutputIntersectionIds()
+        
+        metric = self.METRICS[int(self.distanceMetric)]
+        linkage = self._calculateLinkage(intersections, allClasses, metric)
+
+        # Create outputs
+        self._writeLinkageMatrix(linkage)
+
+    def findElbowsStep(self):
+        linkage = np.load(self._getLinkageMatrixFilename())
+        cost = linkage[:,2]
+        
+        def convert_index(i: int) -> int:
+            return len(cost) - i
+        
+        elbows = {
+            'profile_likelihood': convert_index(self._calculateElbowProfileLikelihood(cost)),
+            'origin': convert_index(self._calculateElbowOrigin(cost)),
+            'distance_line': convert_index(self._calculateElbowDistanceToLine(cost)),
+            'slope': convert_index(self._calculateElbowSlope(cost)),
+        }
+        
+        self._writeElbows(elbows)
+
+    # --------------------------- INFO functions -------------------------------
+    """
+    def _validate(self):
+        errors = []
+
+        # Ensure that all classifications are made of the same images
+        items = self._getInputImages(0)
+        for i in range(1, len(self.inputClassifications)):
+            if self._getInputImages(i) != items:
+                errors.append(f'Classification {i} has been done with different images')
+
+        return errors
+    """
+    
+    # --------------------------- UTILS functions ------------------------------
+    def _getInputClassificationCount(self) -> int:
+        return len(self.inputClassifications)
+    
+    def _getInputClassification(self, classification: int) -> SetOfClasses:
+        return self.inputClassifications[classification].get()
+
+    def _getInputClassifications(self) -> Sequence[SetOfClasses]:
+        return list(map(Pointer.get, self.inputClassifications))
+
+    def _getInputClassParticleIds(self) -> Sequence[Sequence[Set[int]]]:
+        def convertClassification(classification: Pointer):
+            return list(map(SetOfImages.getIdSet, classification.get()))
+            
+        return list(map(convertClassification, self.inputClassifications))
+
+    def _getInputClassIds(self) -> Sequence[Sequence[int]]:
+        def convertClassification(classification: Pointer):
+            return list(map(SetOfImages.getObjId, classification.get()))
+            
+        return list(map(convertClassification, self.inputClassifications))
+
+    def _getInputClassificationSizes(self) -> Sequence[np.ndarray]:
+        def convertClassification(classification: Pointer):
+            return np.array(list(map(len, classification.get())))
+            
+        return list(map(convertClassification, self.inputClassifications))
+
+    def _getInputImages(self, classification: int = 0) -> SetOfImages:
+        return self._getInputClassification(classification).getImages()
+ 
+    def _getSetOfClassesSubtype(self) -> type:
+        return type(self._getInputClassification(0))
+ 
+    def _getSetOfImagesSubtype(self) -> type:
+        return type(self._getInputImages())
+
+    def _getOutputIntersectionIds(self):
+        return list(map(SetOfImages.getIdSet, self.outputClasses))
+
+    def _getReferenceIntersectionSizeFilename(self) -> str:
+        return self._getExtraPath('reference_sizes.npy')
+    
+    def _getReferenceIntersectionNormalizedSizeFilename(self) -> str:
+        return self._getExtraPath('reference_norm_sizes.npy')
+    
+    def _getIntersectionDistancesFilename(self) -> str:
+        return self._getExtraPath('intersenction_distances.npy')
+
+    def _getLinkageMatrixFilename(self) -> str:
+        return self._getExtraPath('linkage.npy')
+
+    def _getElbowsFilename(self) -> str:
+        return self._getExtraPath('elbows.json')
+
+    def _getMergedIntersectionSuffix(self, numel: int) -> str:
+        return 'merged_%06d' % numel
+
+    def _getImagesSuffix(self) -> str:
+        return 'used'
+ 
+    def _getOutputClassesSqliteTemplate(self) -> str:
+        return 'classes_%s.sqlite'
+
+    def _getOutputImagesSqliteTemplate(self) -> str:
+        return 'images_%s.sqlite'
+
+    def _getOutputClassesSqliteFilename(self, suffix: str = '') -> str:
+        return self._getPath(self._getOutputClassesSqliteTemplate() % suffix)
+
+    def _getOutputImagesSqliteFilename(self, suffix: str = '') -> str:
+        return self._getPath(self._getOutputImagesSqliteTemplate() % suffix)
+ 
+    def _writeReferenceIntersectionSizes(self, sizes, normalizedSizes):
+        np.save(self._getReferenceIntersectionSizeFilename(), sizes)
+        np.save(self._getReferenceIntersectionNormalizedSizeFilename(), normalizedSizes)
+
+    def _readReferenceIntersectionSizes(self) -> Tuple[np.ndarray, np.ndarray]:
+        sizes = np.load(self._getReferenceIntersectionSizeFilename())
+        normalizedSizes = np.load(self._getReferenceIntersectionNormalizedSizeFilename())
+        return sizes, normalizedSizes
+ 
+    def _writeLinkageMatrix(self, linkage):
+        np.save(self._getLinkageMatrixFilename(), linkage)
+ 
+    def _readLinkageMatrix(self):
+        return np.load(self._getLinkageMatrixFilename())
+    
+    def _writeElbows(self, elbows: dict):
+        with open(self._getElbowsFilename(), 'w') as f:
+            f.write(json.dumps(elbows))
+    
+    def _readElbows(self) -> dict:
+        with open(self._getElbowsFilename(), 'r') as f:
+            return json.load(f)
+        
+        
+            
+    def _calculateIntersections(self, 
+                                classifications: Iterable[Iterable[Set[int]]],
+                                ids: Iterable[Iterable[int]] ) -> Sequence[Set[int]]:
+        # Start with the first classification
+        itCls = iter(classifications)
+        itId = iter(ids)
+
+        result = next(itCls)
+        traces = list(map('001.{:d}'.format, next(itId)))
+        
+        for i, (classification, ids) in enumerate(zip(itCls, itId), start=2):
+            intersections = []
+            intersection_traces = []
+
+            # Perform the intersection with previous classes
+            for cls0, trace in zip(result, traces):
+                for cls1, id in zip(classification, ids):
+                    intersection = cls0.intersection(cls1)
+                    if len(intersection) > 0:
+                        intersections.append(intersection)
+                        intersection_traces.append(trace + ' & {:03d}.{:d}'.format(i, id))
+
+            # Use the new intersection for the next step
+            result = intersections
+            traces = intersection_traces
+
+        assert(len(result) == len(traces))
+        return result, traces
+
+    def _pruneIntersections(self,
+                            intersections: Iterable[Set[int]],
+                            minSize: int = 0 ):
+
+        if minSize > 1:
+            def size_criteria(intersection: Set[int]) -> bool:
+                return len(intersection) >= minSize
+        
+            filtered = filter(size_criteria, intersections)
+
+            return list(filtered)
+        else:
+            # Nothing to do
+            return intersections
+
+    def _calculateIntersectionSourceProbabilities(self, 
+                                                  classSizes: Iterable[np.ndarray]) -> np.ndarray:
+        
+        def calculateProbability(sizes: np.ndarray) -> np.ndarray:
+            return sizes / np.sum(sizes)
+        
+        probabilities = map(calculateProbability, classSizes)
+        
+        # Consider all combinations
+        prod = list(itertools.product(*probabilities))
+        return np.array(prod)
+    
+    def _calculateIntersectionProbabilities(self, 
+                                            sourceProbabilities: np.ndarray) -> np.ndarray:
+        return np.product(sourceProbabilities, axis=1)
+
+    def _calculateNormalizedIntersectionProbabilities(self, 
+                                                      sourceProbabilities: np.ndarray,
+                                                      probabilities: Optional[np.ndarray] = None ) -> np.ndarray:
+        if probabilities is None:
+            probabilities = self._calculateIntersectionProbabilities(sourceProbabilities)
+            
+        norm = np.min(sourceProbabilities, axis=1)
+        return probabilities / norm
+    
+    def _calculateClassificationLengths(self, classifications: Iterable[SetOfClasses]) -> Sequence[np.ndarray]:
+        def getLengths(classification: SetOfClasses) -> np.ndarray:
+            return np.ndarray(list(map(len, classification)))
+        
+        return list(map(getLengths, classifications))
+    
+    def _calculateClusterSimilarity(self, x: Set[int], y: Set[int]):
+        nCommon = len(x & y)
+        nTotal = len(x | y)
+        return nCommon / nTotal
+    
+    def _calculateClusterSimilarityVector(self, x, classes):
+        result = np.empty(len(classes))
+        
+        for i, y in enumerate(classes):
+            result[i] = self._calculateClusterSimilarity(x, y)
+
+        return result
+    
+    def _calculateLinkage(self,
+                          intersections: List[Set[int]],
+                          classes: List[Set[int]],
+                          metric: str ) -> np.ndarray:
+        
+        linkage = np.zeros((len(intersections)-1, 4))
+        
+        # Initialize the working data structures
+        similarities = list(map(lambda x : self._calculateClusterSimilarityVector(x, classes), intersections))
+        clusters = list(enumerate(intersections))
+        
+        for id, row in enumerate(linkage, start=len(intersections)):
+            # Determine the indices to be merged
+            # TODO optimize to not use squareform
+            distances = scipy.spatial.distance.pdist(similarities, metric=metric) 
+            distance_matrix = scipy.spatial.distance.squareform(distances, 'tomatrix')
+            np.fill_diagonal(distance_matrix, np.inf)
+            idx0, idx1 = np.unravel_index(np.argmin(distance_matrix), distance_matrix.shape)
+            
+            # Be careful to pop from the back first
+            idx0, idx1 = max(idx0, idx1), min(idx0, idx1)
+            
+            # Merge clusters
+            row[0], cluster0 = clusters.pop(idx0)
+            row[1], cluster1 = clusters.pop(idx1)
+            row[2] = distance_matrix[idx0, idx1]
+            # TODO row[3] is not written
+            merged = cluster0.union(cluster1)
+            clusters.append((id, merged))
+            
+            # Add the new similarity
+            similarities.pop(idx0)
+            similarities.pop(idx1)
+            similarities.append(self._calculateClusterSimilarityVector(merged, classes))
+        
+            assert(len(similarities) == len(clusters))
+        
+        assert(len(similarities) == 1)
+        assert(len(clusters) == 1)
+        return linkage 
+            
+    def _calculateMergedIntersections(self, 
+                                      intersections: List[Set[int]],
+                                      linkage: np.ndarray,
+                                      stop: int = 1):
+        merged = [intersections]
+        clusters = intersections.copy()
+        
+        for idx0, idx1, _, _ in linkage:
+            # Construct a new cluster joining two of the clusters
+            cluster0: Set[int] = clusters[int(idx0)]
+            cluster1: Set[int] = clusters[int(idx1)]
+            clusterMerged = cluster0.union(cluster1)
+            clusters.append(clusterMerged)
+
+            # Copy the previous row except for the
+            # merged sets. Add the new merged set
+            assert(cluster0 in merged[-1])
+            assert(cluster1 in merged[-1])
+            mergedNew = [clusterMerged]
+            for cluster in merged[-1]:
+                if cluster not in (cluster0, cluster1):
+                    mergedNew.append(cluster)
+            
+            merged.append(mergedNew)
+
+            # Stop
+            if len(mergedNew) <= stop:
+                break
+            
+        return merged
+    
+    def _calculatePercentile(self, data: np.ndarray, value: float):
+            """ Given an array of values (data), finds the corresponding percentile of the value.
+                Percentile is returned in range [0, 1] """
+            assert(np.array_equal(sorted(data), data))  # In order to iterate it in ascending order
+
+            # Count the number of elements that are smaller than the given value
+            i = 0
+            while i < len(data) and data[i] < value:
+                i += 1
+
+            # Convert it into a percentage
+            return float(i) / float(len(data))
+    
+    def _calculateClusterRepresentativeClass(self,
+                                             cluster: Set[int],
+                                             classifications: Iterable[SetOfClasses]):
+        
+        def computeSimilarity(items: SetOfImages) -> float:
+            return self._calculateClusterSimilarity(cluster, items.getIdSet())
+
+        def iterClasses():
+            return itertools.chain(*classifications)
+         
+        similarities = np.array(list(map(computeSimilarity, iterClasses())))
+        index = np.argmax(similarities)
+
+        # FIXME avoid iterating twice
+        it = iterClasses()
+        for _ in range(index):
+            next(it)
+        
+        return next(it)
+
+    def _calculateElbowProfileLikelihood(self, cost: np.ndarray) -> int:
+        def cost_fun(i: int) -> float:
+            # Partition the function in q
+            d1 = cost[:i]
+            d2 = cost[i:]
+            
+            # Compute the average and mean of each partition
+            mu1 = np.mean(d1)
+            mu2 = np.mean(d2)
+            sigma1 = np.std(d1, ddof=1)
+            sigma2 = np.std(d2, ddof=1)
+            #sigma = (len(d1)*sigma1 + len(d2)*sigma2) / (len(d1) + len(d2)) # Weighted average
+        
+            # Compute the log likelihood
+            logLikelihoods1 = scipy.stats.norm.logpdf(d1, mu1, sigma1)
+            logLikelihoods2 = scipy.stats.norm.logpdf(d2, mu2, sigma2)
+            return np.sum(logLikelihoods1) + np.sum(logLikelihoods2)
+            
+        if len(cost) > 4:
+            return max(range(2, len(cost)-1), key=cost_fun)
+        else:
+            return len(cost) // 2
+    
+    def _calculateElbowOrigin(self, cost: np.ndarray) -> int:
+        y = (cost - cost.min()) / (cost.max() - cost.min())
+        x = np.linspace(1.0, 0.0, len(y))
+        dist2 = x*x + y*y
+            
+        return int(dist2.argmin())
+    
+    def _calculateElbowDistanceToLine(self, cost: np.ndarray) -> int:
+        # Based on:
+        # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
+        a = cost - cost[0]
+        b = np.linspace(0.0, cost[-1] - cost[0], len(cost))
+        d = np.abs(a - b)
+        
+        return int(d.argmax())
+
+    def _calculateElbowSlope(self, cost: np.ndarray) -> int:
+        diff = np.diff(cost)
+        avgDiff = (cost[-1] - cost[0]) / (len(cost) - 1)
+        delta = np.abs(diff - avgDiff)
+        return int(delta.argmin() + 1)
+    
+    def _obtainMergedIntersections(self, n: int) -> SetOfClasses:
+        suffix = self._getMergedIntersectionSuffix(n)
+        filename = self._getOutputClassesSqliteFilename(suffix)
+    
+        if os.path.exists(filename):
+            SetOfClassesSubtype = self._getSetOfClassesSubtype()
+            return SetOfClassesSubtype(filename=filename)
+            
+        else:
+            # Load from input
+            SetOfImagesSubtype = self._getSetOfImagesSubtype()
+            if os.path.exists(self._getOutputImagesSqliteFilename(self._getImagesSuffix())):
+                images = SetOfImagesSubtype(filename=self._getOutputImagesSqliteFilename(self._getImagesSuffix()))
+            else:
+                images = self._getInputImages()
+            classifications = self._getInputClassifications()
+            intersections = self._getOutputIntersectionIds()
+            linkage = self._readLinkageMatrix()
+            referenceSizes, normalizedReferenceSizes = self._readReferenceIntersectionSizes()
+            
+            # Merge and create the set of classes
+            merging = self._calculateMergedIntersections(intersections, linkage, stop=n)
+            merged = merging[-1]
+            outputClasses = self._createSetOfClasses(
+                images=images,
+                classifications=classifications,
+                clustering=merged,
+                suffix=suffix,
+                referenceSizes=referenceSizes,
+                normalizedReferenceSizes=normalizedReferenceSizes
+            )
+
+            self._defineOutputs(**{'merged' + str(n): outputClasses})
+            self._defineSourceRelation(self.inputClassifications, outputClasses)
+            return outputClasses
+        
+    # -------------------------- Convert functions -----------------------------
+    def _createSetOfClasses(self, 
+                            images: SetOfImages,
+                            classifications: Sequence[SetOfClasses],
+                            clustering: Sequence[Set[int]], 
+                            suffix: str,
+                            comments=None,
+                            referenceSizes=None, 
+                            normalizedReferenceSizes=None ):
+
+        # Create an empty set with the same images as the input classification
+        result: SetOfClasses = self._EMProtocol__createSet( # HACK
+            self._getSetOfClassesSubtype(), 
+            self._getOutputClassesSqliteTemplate(),
+            suffix
+        ) 
+        result.setImages(images)
+    
+        # Fill the output
+        def updateItem(item: Image, _):
+            objId: int = item.getObjId()
+            
+            for cls, objIds in enumerate(clustering):
+                if objId in objIds:
+                    classId = cls + 1
+                    item.setClassId(classId)
+                    break # Found!
+                
+            else:
+                # Do not classify (exclude from output)
+                item.setClassId(0)
+                item._appendItem = False
+        
+        def updateClass(item: SetOfImages):
+            classId: int = item.getObjId()
+            classIdx = classId - 1
+            cluster = clustering[classIdx]
+            representativeClass = self._calculateClusterRepresentativeClass(
+                cluster,
+                classifications
+            )
+            size = len(cluster)
+            relativeSize = size / len(representativeClass)
+            
+            item.setRepresentative(representativeClass.getRepresentative().clone())
+
+            if comments is not None:
+                item.setObjComment(comments[classIdx])
+            
+            if referenceSizes is not None:
+                sizePercentile = self._calculatePercentile(referenceSizes, size)
+                pValue = 1 - sizePercentile
+                setXmippAttribute(item, emlib.MDL_CLASS_INTERSECTION_SIZE_PVALUE, Float(pValue))
+                
+            if normalizedReferenceSizes is not None:
+                relativeSizePercentile = self._calculatePercentile(normalizedReferenceSizes, relativeSize)
+                pValue = 1 - relativeSizePercentile
+                setXmippAttribute(item, emlib.MDL_CLASS_INTERSECTION_RELATIVE_SIZE_PVALUE, Float(pValue))
+
+        result.classifyItems(
+            updateItemCallback=updateItem,
+            updateClassCallback=updateClass,
+            doClone=True
+        )
+    
+        return result
diff --git a/xmipp3/protocols/protocol_consensus_classes3D.py b/xmipp3/protocols/protocol_consensus_classes3D.py
deleted file mode 100644
index 19726dd4c..000000000
--- a/xmipp3/protocols/protocol_consensus_classes3D.py
+++ /dev/null
@@ -1,782 +0,0 @@
-# **************************************************************************
-# * Authors:     David Maluenda (dmaluenda@cnb.csic.es)
-# *              Daniel del Hoyo Gomez (daniel.delhoyo.gomez@alumnos.upm.es)
-# *              Jorge Garcia Condado (jgcondado@cnb.csic.es)
-# *              Oier Lauzirika Zarrabeitia (oierlauzi@bizkaia.eu)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-from unittest import result
-from pwem import emlib
-from pwem.protocols import EMProtocol
-from pwem.objects import Volume
-
-from pyworkflow.protocol.params import MultiPointerParam, EnumParam, IntParam
-from pyworkflow.protocol.params import Range, GE, GT, LE, LT
-from pyworkflow.object import Float
-from pyworkflow.constants import BETA
-from pyworkflow.protocol.constants import STEPS_PARALLEL
-from pyworkflow.utils.path import makePath
-
-from xmipp3.convert import setXmippAttribute
-
-import math
-import csv
-import collections
-import itertools
-import os.path
-import numpy as np
-import multiprocessing as mp
-from multiprocessing.pool import ThreadPool
-
-class XmippProtConsensusClasses3D(EMProtocol):
-    """ Compare several SetOfClasses3D.
-        Return the consensus clustering based on a objective function
-        that uses the similarity between clusters intersections and
-        the entropy of the clustering formed.
-    """
-    _label = 'consensus clustering 3D'
-    _devStatus = BETA
-
-    def __init__(self, *args, **kwargs):
-        EMProtocol.__init__(self, *args, **kwargs)
-        self._createFileNames()
-        
-    def _createFileNames(self):
-        iterFmt='i%(iter)06d'
-
-        myDict = {
-            'intersections': f'intersections.csv',
-            'clustering': f'clusterings/{iterFmt}.csv',
-            'objective_values': f'objective_values.csv',
-            'elbows': f'elbows.csv',
-            'reference_sizes': f'reference_sizes.csv',
-            'reference_relative_sizes': f'reference_relative_sizes.csv',
-        }
-        self._updateFilenamesDict(myDict)
-
-    def _defineParams(self, form):
-        # Input data
-        form.addSection(label='Input')
-        form.addParam('inputClassifications', MultiPointerParam, pointerClass='SetOfClasses3D', label="Input classes", 
-                      important=True,
-                      help='Select several sets of classes where to evaluate the '
-                           'intersections.')
-
-        # Consensus
-        form.addSection(label='Consensus clustering')
-        form.addParam('clusteringManualCount', IntParam, label='Manual cluster count',
-                      validators=[GE(2)], default=3,
-                      help='Number of clusters to be obtained manually')
-        form.addParam('clusteringAngle', IntParam, label='Objective function threshold angle',
-                      validators=[Range(0, 90)], default=45,
-                      help='Angle to determine the cluster count')
-        
-        # Reference random classification
-        form.addSection(label='Reference random classification')
-        form.addParam('randomClassificationCount', IntParam, label='Number of random classifications',
-                      validators=[GE(0)], default=0,
-                      help='Number of random classifications used for computing the significance of the actual '
-                           'clusters. 0 for skipping this step')
-        form.addParallelSection(mpi=0, threads=mp.cpu_count())
-
-    # --------------------------- INSERT steps functions -----------------------
-    def _insertAllSteps(self):
-        """ Inserting one step for each intersections analysis """
-        outputPrerequisites = []
-        self._setupThreadPool(int(self.numberOfThreads))
-
-        # Intersect and ensemble
-        self._insertFunctionStep('intersectStep', prerequisites=[])
-        self._insertFunctionStep('ensembleStep')
-        outputPrerequisites.append(self._insertFunctionStep('findElbowsStep'))
-
-        # Perform a random classification and use it as a reference
-        if self.randomClassificationCount.get() > 0:
-            outputPrerequisites.append(self._insertFunctionStep('checkSignificanceStep', prerequisites=[]))
-
-        # Lastly create the output    
-        self._insertFunctionStep('createOutputStep', prerequisites=outputPrerequisites)
-
-    def intersectStep(self):
-        classifications = self._getInputClassifications()
-
-        # Compute the intersection among all classes
-        intersections = self._calculateClassificationIntersections(classifications)
-
-        # Write the result to disk
-        self._writeIntersections(intersections)
-        
-    def ensembleStep(self, numClusters=1):
-        # Read input data
-        classifications = self._getInputClassifications()
-        intersections = self._readIntersections()
-
-        # Iteratively ensemble intersections until only 1 remains
-        allClusterings, allObValues = self._ensembleIntersections(classifications, intersections, numClusters)
-
-        # Reverse them so that their size increases with the index
-        allClusterings.reverse()
-        allObValues.reverse()
-
-        # Save the results
-        self._writeAllClusterings(allClusterings)
-        self._writeObjectiveValues(allObValues)
-
-    def findElbowsStep(self):
-        obValues = self._readObjectiveValues()
-        numClusters = list(range(1, 1+len(obValues)))
-
-        # Normalize clusters and obValues
-        normObValues = self._normalizeValues(obValues)
-        normNumClusters = self._normalizeValues(numClusters)
-
-        # Calculate the elbows
-        elbows = {
-            'origin': self._findElbowOrigin(normNumClusters, normObValues),
-            'angle': self._findElbowAngle(normNumClusters, normObValues, -np.radians(self.clusteringAngle.get())),
-            'pll': self._findElbowPll(obValues)
-        }
-
-        # Start counting at 1
-        for key in elbows.keys():
-            elbows[key] += 1
-
-        # Save the results
-        self._writeElbows(elbows)
-
-    def checkSignificanceStep(self):
-        classifications = self._getInputClassifications()
-        numRand = self.randomClassificationCount.get()
-        threadPool = self._getThreadPool()
-
-        # Compute the random consensus
-        consensusSizes, consensusRelativeSizes = self._calculateReferenceClassification(
-            classifications, numRand, threadPool
-        )
-
-        # Sort the sizes so that percentile calculation is easy
-        consensusSizes.sort()
-        consensusRelativeSizes.sort()
-
-        # Write results to disk
-        self._writeReferenceClassificationSizes(consensusSizes)
-        self._writeReferenceClassificationRelativeSizes(consensusRelativeSizes)
-
-    def createOutputStep(self):
-        # Read all the necessary data from disk
-        manualClusterCount = self.clusteringManualCount.get()
-        initialClusterCount = self._readClusteringCount()
-        elbows = self._readElbows()
-        consensusSizes = self._readReferenceClassificationSizes()
-        consensusRelativeSizes = self._readReferenceClassificationRelativeSizes()
-
-        # Append the manual and initial indices to the elbow
-        if manualClusterCount > 0:
-            elbows['manual'] = manualClusterCount
-        elbows['initial'] =  initialClusterCount
-
-        # Read selected clusters
-        clusterings = dict(zip(elbows.keys(), map(self._readClustering, elbows.values())))
-
-        # Create the output classes and define them
-        particles = self._getInputParticles()
-        outputClasses = dict(zip(
-            map(lambda name : 'outputClasses_'+name,
-                clusterings.keys() 
-            ),
-            map(self._createOutputClasses3D,
-                itertools.repeat(particles),
-                clusterings.values(),
-                clusterings.keys(),
-                itertools.repeat(consensusSizes),
-                itertools.repeat(consensusRelativeSizes)
-            )
-        ))
-        self._defineOutputs(**outputClasses)
-
-        # Stablish source output relationships
-        sources = list(self.inputClassifications)
-        destinations = list(outputClasses.values())
-        for src in sources:
-            for dst in destinations:
-                self._defineSourceRelation(src, dst)
-
-    # --------------------------- INFO functions -------------------------------
-    def _validate(self):
-        errors = []
-
-        # Ensure that all classifications are made of the same images
-        images = self._getInputParticles(0)
-        for i in range(1, len(self.inputClassifications)):
-            if self._getInputParticles(i) != images:
-                errors.append(f'Classification {i} has been done with different images')
-
-        return errors
-
-    # --------------------------- UTILS functions ------------------------------
-    def _getThreadPool(self):
-        return getattr(self, '_threadPool', None)
-
-    def _setupThreadPool(self, maxThreads):
-        if maxThreads > 1:
-            self._threadPool = ThreadPool(processes=maxThreads)
-
-    def _getInputParticles(self, i=0):
-        return self.inputClassifications[i].get().getImages()
-
-    def _getInputClassifications(self):
-        if not hasattr(self, '_classifications'):
-            self._classifications = self._convertInputClassifications(self.inputClassifications)
-        
-        return self._classifications
-
-    def _writeTable(self, path, table, fmt='%s'):
-        np.savetxt(path, table, delimiter=',', fmt=fmt)
-
-    def _readTable(self, path, dtype=str):
-        return np.genfromtxt(path, delimiter=',', dtype=dtype)
-
-    def _writeList(self, path, lst, fmt='%s'):
-        self._writeTable(path, lst, fmt)
-
-    def _readList(self, path, dtype=str):
-        return self._readTable(path, dtype=dtype)
-        
-    def _writeClassification(self, path, classification):
-        with open(path, 'w') as file:
-            writer = csv.writer(file)
-            
-            # Write the header
-            header = ['Representative index', 'Representative path', 'Source size', 'Particle ids']
-            writer.writerow(header)
-
-            # Transform the classification
-            def f(cls):
-                result = []
-                
-                # Add the location of the representative
-                representative = cls.getRepresentative()
-                location = representative.getLocation() if representative is not None else (0, '')
-                result += list(location)
-
-                # Add the source size
-                result += [cls.getSourceSize()]
-
-                # Add the particles
-                result += list(cls.getParticleIds())
-
-                assert(len(result) == (len(cls) + 3))
-                return result
-            writer.writerows(map(f, classification))
-
-    def _readClassification(self, path):
-        with open(path, 'r') as file:
-            reader = csv.reader(file)
-
-            # Ignore the header
-            next(reader)
-
-            # Transform each row to a ParticleCluster in a classification
-            def f(row):
-                ite = iter(row)
-
-                # Parse the representative
-                index = int(next(ite))
-                path = next(ite)
-                representative = Volume(location=(index, path))
-
-                # Parse the source size
-                sourceSize = int(next(ite))
-
-                # Parse the id set
-                particleIds = set(map(lambda id : int(id), ite))
-
-                result = XmippProtConsensusClasses3D.ParticleCluster(particleIds, representative, sourceSize)
-                assert(len(row) == (len(result) + 3))
-                return result
-            return list(map(f, reader))
-
-    def _writeDictionary(self, path, d):
-        with open(path, 'w') as file:
-            writer = csv.writer(file)
-            writer.writerow(d.keys()) # Header
-            writer.writerow(d.values()) # Values
-
-    def _readDictionary(self, path, dtype=str):
-        with open(path, 'r') as file:
-            reader = csv.reader(file)
-            keys = list(next(reader))
-            values = list(map(lambda value : dtype(value), next(reader)))
-            return dict(zip(keys, values))
-
-    def _writeIntersections(self, intersections):
-        self._writeClassification(self._getExtraPath(self._getFileName('intersections')), intersections)
-
-    def _readIntersections(self):
-        return self._readClassification(self._getExtraPath(self._getFileName('intersections')))
-
-    def _writeClustering(self, iter, clustering):
-        self._writeClassification(self._getExtraPath(self._getFileName('clustering', iter=iter)), clustering)
-
-    def _readClustering(self, iter):
-        return self._readClassification(self._getExtraPath(self._getFileName('clustering', iter=iter)))
-
-    def _readClusteringCount(self):
-        count = 0
-        while os.path.exists(self._getExtraPath(self._getFileName('clustering', iter=count+1))):
-            count += 1
-        return count
-
-    def _writeAllClusterings(self, clusterings):   
-        # Create the path
-        makePath(os.path.dirname(self._getExtraPath(self._getFileName('clustering', iter=0))))
-
-        # Write all
-        for iter, clustering in enumerate(clusterings, start=1):
-            assert(len(clustering) == iter)
-            self._writeClustering(iter, clustering)
-
-    def _readAllClusterings(self):
-        return list(map(self._readClustering, range(1, 1+self._readClusteringCount())))
-
-    def _writeObjectiveValues(self, allObValues):
-        self._writeList(self._getExtraPath(self._getFileName('objective_values')), allObValues, '%f')
-
-    def _readObjectiveValues(self):
-        return self._readList(self._getExtraPath(self._getFileName('objective_values')), dtype=float)
-
-    def _writeElbows(self, elbows):
-        self._writeDictionary(self._getExtraPath(self._getFileName('elbows')), elbows)
-
-    def _readElbows(self):
-        return self._readDictionary(self._getExtraPath(self._getFileName('elbows')), dtype=int)
-
-    def _writeReferenceClassificationSizes(self, sizes):
-        self._writeList(self._getExtraPath(self._getFileName('reference_sizes')), sizes, '%d')
-
-    def _readReferenceClassificationSizes(self):
-        path = self._getExtraPath(self._getFileName('reference_sizes'))
-        if os.path.exists(path):
-            return self._readList(path, dtype=int)
-        else:
-            return None
-
-    def _writeReferenceClassificationRelativeSizes(self, sizes):
-        self._writeList(self._getExtraPath(self._getFileName('reference_relative_sizes')), sizes, '%f')
-
-    def _readReferenceClassificationRelativeSizes(self):
-        path = self._getExtraPath(self._getFileName('reference_relative_sizes'))
-        if os.path.exists(path):
-            return self._readList(path, dtype=float)
-        else:
-            return None
-
-    def _convertInputClassifications(self, classifications):
-        """ Returns the list of lists of sets that stores the set of ids of each class"""
-        def class3dToParticleCluster(cls):
-            ids = cls.getIdSet()
-            rep = cls.getRepresentative().clone() if cls.hasRepresentative() else None
-            return XmippProtConsensusClasses3D.ParticleCluster(ids, rep)
-
-        f = lambda classification : list(map(class3dToParticleCluster, classification.get()))
-        result = list(map(f, classifications))
-        return result
-    
-    def _calculateClassificationIntersections(self, classifications):
-        # Start with the first classification
-        result = classifications[0]
-
-        for i in range(1, len(classifications)):
-            classification = classifications[i]
-            intersections = []
-
-            # Perform the intersection with previous classes
-            for cls0 in result:
-                for cls1 in classification:
-                    intersection = cls0.intersection(cls1)
-                    if intersection:
-                        intersections.append(intersection)
-
-            # Use the new intersection for the next step
-            result = intersections
-
-        return result
-
-    def _calculateSubsetSimilarity(self, n, c):
-        """ Return the similarity of intersection subset (n) with class subset (c) """
-        inter = len(n.intersection(c))
-        return inter / len(n)
-
-    def _buildSimilarityVector(self, n, cs):
-        """ Build the similarity vector of n subset with each c subset in cs """
-        return list(map(self._calculateSubsetSimilarity, itertools.repeat(n), cs))
-
-    def _calculateEntropy(self, p):
-        """ Return the entropy of the elements in a vector """
-        result = -sum(map(lambda x : x*math.log2(x), p))
-        assert(result >= 0) # Entropy cannot be negative
-        return result
-
-    def _calculateDistribution(self, ns):
-        """ Return the vector of distribution (p) from the n subsets in ns
-        """
-        p = list(map(len, ns))
-        return np.array(p) / sum(p)
-
-    def _calculateCosineSimilarity(self, v1, v2):
-        """ Return the cosine similarity of two vectors that is used as similarity """
-        return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
-
-    def _objectiveFunction(self, ns, indices, vs, eps=0.01):
-        """Objective function to minimize based on the entropy and the similarity between the clusters to merge
-        """
-        p = self._calculateDistribution(ns)
-        pi = self._calculateDistribution(self._ensembleIntersectionPair(ns, indices))
-        h = self._calculateEntropy(p)
-        hi = self._calculateEntropy(pi)
-
-        sv = self._calculateCosineSimilarity(vs[indices[0]], vs[indices[1]])
-
-        return (h - hi) / (sv + eps)
-
-    def _ensembleIntersectionPair(self, intersections, indices):
-        """ Return a list of subsets where the intersections[indices] have been merged """
-        # Divide the input intersections into the ones referred by indices and not
-        selection = [intersections[i] for i in indices]  # intersection[indices]
-        result = [intersections[i] for i in range(len(intersections)) if i not in indices]  # intersections - selection
-
-        # Merge all the selected clusters into the same one
-        merged = XmippProtConsensusClasses3D.ParticleCluster.union(*selection)
-        
-        # Add the merged items to the result
-        result.append(merged)
-        return result
-
-    def _ensembleCheapestIntersectionPair(self, allClasses, intersections):
-        """ Merges the lest costly pair of intersections """
-        # Compute the similarity vectors
-        similarityVectors = list(map(self._buildSimilarityVector, intersections, itertools.repeat(allClasses)))
-
-        # For each possible pair of intersections compute the cost of merging them
-        obValues = {}
-        for pair in itertools.combinations(range(len(intersections)), r=2):
-            obValues[pair] = self._objectiveFunction(intersections, pair, similarityVectors)
-
-        values = list(obValues.values())
-        keys = list(obValues.keys())
-
-        # Select the minimum cost pair and merge them
-        obValue = min(values)
-        mergePair = keys[values.index(obValue)]
-        mergedIntersections = self._ensembleIntersectionPair(intersections, mergePair)
-
-        return mergedIntersections, obValue
-
-    def _ensembleIntersections(self, classifications, intersections, numClusters=1):
-        """ Ensemble clustering by COSS that merges interactions of clusters based
-            on entropy minimization """
-        # Results
-        allIntersections = [intersections]
-        allObValues = [0.0]
-
-        # Obtain a list with all source classes
-        allClasses = list(itertools.chain(*classifications))
-
-        # Iteratively merge intersections
-        while len(allIntersections[-1]) > numClusters:
-            mergedIntersections, obValue = self._ensembleCheapestIntersectionPair(allClasses, allIntersections[-1])
-            allIntersections.append(mergedIntersections)
-            allObValues.append(obValue)
-
-        assert(len(allIntersections) == len(allObValues))
-        return allIntersections, allObValues
-            
-    def _normalizeValues(self, x):
-        """Normalize values to range 0 to 1"""
-        return (x-np.min(x))/(np.max(x)-np.min(x))
-
-    def _calculateGaussianMleEstimates(self, d, q):
-        """ MLE estimates of guassian distributions
-        with common variance parameter
-        """
-        p = len(d)
-        d1 = d[:q]
-        d2 = d[q:]
-        mu1 = np.mean(d1)
-        mu2 = np.mean(d2)
-        var1 = np.var(d1)
-        var2 = np.var(d2)
-        sigma = ((q-1)*var1+(p-q-1)*var2)/(p-2)
-        theta1 = [mu1, sigma]
-        theta2 = [mu2, sigma]
-        return theta1, theta2
-
-    def _calculateGaussianDistribution(self, d, theta):
-        """ Gaussian pdf """
-        m = theta[0]
-        s = theta[1]
-        gauss = 1/(np.sqrt(2*np.pi*s))*np.exp(-1/(2*s)*(d-m)**2)
-        return gauss
-
-    def _calculateProfileLogLikelihood(self, d, q, theta1, theta2):
-        """ Profile log likelihood for given parameters """
-        log_f_q = np.log(self._calculateGaussianDistribution(d[:q], theta1))
-        log_f_p = np.log(self._calculateGaussianDistribution(d[q:], theta2))
-        l_q = np.sum(log_f_q) + np.sum(log_f_p)
-        return l_q
-
-    def _calculateFullProfileLogLikelihood(self, d):
-        """ Calculate profile log likelihood for each partition
-            of the data """
-        pll = []
-        for q in range(1, len(d)):
-            theta1, theta2 = self._calculateGaussianMleEstimates(d, q)
-            pll.append(self._calculateProfileLogLikelihood(d, q, theta1, theta2))
-        return pll
-
-    def _findElbowPll(self, obValues):
-        """ Find the elbow according to the full profile likelihood
-        """
-        # Get profile log likelihood for log of objective values
-        # Remove last obValue as it is zero and log(0) is undefined
-        pll = self._calculateFullProfileLogLikelihood(np.log(obValues[:-1]))
-        return np.argmax(pll)
-
-    def _findElbowOrigin(self, x, y):
-        """ Find the point closest to origin from normalied data
-        """
-        coors = np.column_stack((x, y))
-        distances = np.linalg.norm(coors, axis=1)
-        assert(len(x) == len(distances))
-        return np.argmin(distances)
-
-    def _findElbowAngle(self, x, y, angle=-math.pi/4):
-        """ Find the angle the slope of the function makes and
-            return the point at which the slope crosses the given angle
-        """
-        dx = np.diff(x)
-        dy = np.diff(y)
-        angles = np.arctan2(dy, dx)
-
-        # Find the first point with an angle greater than the given one
-        crossing = np.argmax(angles >= angle)
-
-        return crossing
-
-    def _getClassificationLengths(self, classifications):
-        """ Returns a list of lists that stores the lengths of each classification """
-        return list(map(lambda classification : list(map(len, classification)), classifications))
-
-    def _calculateRandomClassification(self, C, N):
-        """ Randomly classifies N element indices into groups with
-        sizes defined by rows of C """
-        Cp = []
-
-        for Ci in C:
-            assert(sum(Ci) == N)  # The groups should address all the elements TODO: Maybe LEQ?
-            x = np.argsort(np.random.uniform(size=N)).tolist()  # Shuffles a [0, N) iota
-
-            # Select the number random indices requested by each group
-            Cip = []
-            first = 0
-            for s in Ci:
-                Cip.append(XmippProtConsensusClasses3D.ParticleCluster(x[first:first + s]))
-                first += s
-
-            # Add the random classification to the result
-            Cp.append(Cip)
-
-        return Cp
-
-    def _calculateRandomClassificationConsensus(self, C, N):
-        """ Obtains the intersections of a consensus
-            of a random classification of sizes defined in C of
-            N elements. C is a list of lists, where de sum of each
-            row must equal N """
-
-        # Create random partitions of same size
-        randomClassification = self._calculateRandomClassification(C, N)
-
-        # Compute the repeated classifications
-        return self._calculateClassificationIntersections(randomClassification)
-
-    def _calculateReferenceClassification(self, classifications, numExec, threadPool=None):
-        """ Create random partitions of same size to compare the quality
-         of the classification """
-        clusterLengths = self._getClassificationLengths(classifications)
-        numParticles = sum(clusterLengths[0])
-
-        # Repeatedly obtain a consensus of a random classification of same size
-        if threadPool:
-            consensus = threadPool.starmap(
-                self._calculateRandomClassificationConsensus,
-                itertools.repeat((clusterLengths, numParticles), numExec)
-            )
-        else:
-            consensus = list(map(
-                self._calculateRandomClassificationConsensus, 
-                itertools.repeat(clusterLengths, numExec),
-                itertools.repeat(numParticles, numExec)
-            ))
-        assert(len(consensus) == numExec)
-
-        # Concatenate all consensuses
-        consensus = list(itertools.chain(*consensus))
-
-        # Obtain the consensus sizes
-        consensusSizes = list(map(len, consensus))
-        consensusSizeRatios = list(map(XmippProtConsensusClasses3D.ParticleCluster.getRelativeSize, consensus))
-        return consensusSizes, consensusSizeRatios
-
-    def _createOutputClasses3D(self, particles, clustering, name, randomConsensusSizes=None, randomConsensusRelativeSizes=None):
-        outputClasses = self._createSetOfClasses3D(particles, suffix=name)
-
-        # Create a list with the filenames of the representatives
-        representatives = list(map(
-            XmippProtConsensusClasses3D.ParticleCluster.getRepresentative,
-            clustering
-        ))
-
-        # Fill the output
-        loader = XmippProtConsensusClasses3D.ClassesLoader(
-            clustering, 
-            representatives,
-            randomConsensusSizes,
-            randomConsensusRelativeSizes
-        )
-        loader.fillClasses(outputClasses)
-
-        return outputClasses
-    
-    class ParticleCluster:
-        """ Keeps track of the information related to successive class intersections.
-            It is instantiated with a single class and allows to perform intersections
-            and unions with it"""
-        def __init__(self, particleIds, representative=None, sourceSize=None):
-            self._particleIds = set(particleIds)  # Particle ids belonging to this intersection
-            self._representative = representative
-            self._sourceSize = sourceSize if sourceSize is not None else len(self._particleIds)  # The size of the representative class
-
-        def __len__(self):
-            return len(self.getParticleIds())
-
-        def __eq__(self, other):
-            return self.getParticleIds() == other.getParticleIds()
-
-        def intersection(self, *others):
-            return self._combine(set.intersection, min, XmippProtConsensusClasses3D.ParticleCluster.getSourceSize, *others)
-
-        def union(self, *others):
-            return self._combine(set.union, max, len, *others)
-
-        def getParticleIds(self):
-            return self._particleIds
-
-        def getRepresentative(self):
-            return self._representative
-
-        def getSourceSize(self):
-            return self._sourceSize
-
-        def getRelativeSize(self):
-            return len(self) / self.getSourceSize()
-
-        def _combine(self, operation, selector, selectionCriteria, *others):
-            allItems = (self, ) + others
-            
-            # Perform the requested operation among the id sets
-            allParticleIds = map(XmippProtConsensusClasses3D.ParticleCluster.getParticleIds, allItems)
-            particleIds = operation(*allParticleIds)
-
-            # Select the class with the appropriate criteria
-            selection = selector(allItems, key=selectionCriteria)
-            representative = selection.getRepresentative()
-            sourceSize = selection.getSourceSize()
-
-            return XmippProtConsensusClasses3D.ParticleCluster(particleIds, representative, sourceSize)
-
-    class ClassesLoader:
-        """ Helper class to produce classes
-        """
-        def __init__(self, clustering, representatives, randomConsensusSizes=None, randomConsensusRelativeSizes=None):
-            self.clustering = clustering
-            self.classification = self._createClassification(clustering)
-            self.representatives = representatives
-            self.randomConsensusSizes = randomConsensusSizes
-            self.randomConsensusRelativeSizes = randomConsensusRelativeSizes
-
-        def fillClasses(self, clsSet):
-            clsSet.classifyItems(updateItemCallback=self._updateParticle,
-                                updateClassCallback=self._updateClass,
-                                itemDataIterator=iter(self.classification.items()),
-                                doClone=False)
-
-        def _updateParticle(self, item, row):
-            particleId, classId = row
-            assert(item.getObjId() == particleId)
-            item.setClassId(classId)
-
-        def _updateClass(self, item):
-            classId = item.getObjId()
-            classIdx = classId-1
-
-            # Set the representative
-            item.setRepresentative(self.representatives[classIdx])
-
-            # Set the size p-value
-            if self.randomConsensusSizes is not None:
-                size = len(self.clustering[classIdx])
-                percentile = self._findPercentile(self.randomConsensusSizes, size)
-                pValue = 1 - percentile
-                setXmippAttribute(item, emlib.MDL_CLASS_INTERSECTION_SIZE_PVALUE, Float(pValue))
-
-
-            # Set the relative size p-value
-            if self.randomConsensusRelativeSizes is not None:
-                size = self.clustering[classIdx].getRelativeSize()
-                percentile = self._findPercentile(self.randomConsensusRelativeSizes, size)
-                pValue = 1 - percentile
-                setXmippAttribute(item, emlib.MDL_CLASS_INTERSECTION_RELATIVE_SIZE_PVALUE, Float(pValue))
-
-        def _createClassification(self, clustering):
-            # Fill the classification data (particle-class mapping)
-            result = {}
-            for cls, data in enumerate(clustering):
-                for particleId in data.getParticleIds():
-                    result[particleId] = cls + 1 
-
-            return collections.OrderedDict(sorted(result.items())) 
-
-        def _findPercentile(self, data, value):
-            """ Given an array of values (data), finds the corresponding percentile of the value.
-                Percentile is returned in range [0, 1] """
-            assert(np.array_equal(sorted(data), data))  # In order to iterate it in ascending order
-
-            # Count the number of elements that are smaller than the given value
-            i = 0
-            while i < len(data) and data[i] < value:
-                i += 1
-
-            # Convert it into a percentage
-            return float(i) / float(len(data))
diff --git a/xmipp3/protocols/protocol_consensus_local_ctf.py b/xmipp3/protocols/protocol_consensus_local_ctf.py
index 444f44acb..0d9326e2e 100644
--- a/xmipp3/protocols/protocol_consensus_local_ctf.py
+++ b/xmipp3/protocols/protocol_consensus_local_ctf.py
@@ -34,14 +34,20 @@
 
 from pwem.protocols import ProtAnalysis3D
 from pwem import emlib
+from pwem.objects import SetOfParticles
 
 from xmipp3.convert import setXmippAttribute
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
+OUTPUTNAME = "outputParticles"
 
 class XmippProtConsensusLocalCTF(ProtAnalysis3D):
     """This protocol compares the estimations of local defocus computed by different protocols for a set of particles"""
     _label = 'consensus local defocus'
+    _devStatus = UPDATED
+
     _lastUpdateVersion = VERSION_2_0
+    _possibleOutputs = {OUTPUTNAME:SetOfParticles}
 
     def __init__(self, **args):
         ProtAnalysis3D.__init__(self, **args)
@@ -60,46 +66,94 @@ def _defineParams(self, form):
 
     #--------------------------- INSERT steps functions --------------------------------------------
     def _insertAllSteps(self):
-        self._insertFunctionStep("compareDefocus")
-        self._insertFunctionStep("createOutputStep")
+        self._insertFunctionStep(self.compareDefocus)
+        self._insertFunctionStep(self.createOutputStep)
 
     #--------------------------- STEPS functions ---------------------------------------------------
     def compareDefocus(self):
         """compare with median, mad and correlation matrix the local defocus value for each pixel estimated by
         different programs (Xmipp, Relion, gCTF, ...)"""
-        allParticlesDef = {}
+        allParticlesDefU = {}
+        allParticlesDefV = {}
+        allParticlesDefAngle = {}
         for defsetP in self.inputSets:
             defset = defsetP.get()
             for particle in defset:
                 pIDs = []
-                pDefAs = []
+                pDefUs = []
+                pDefVs = []
+                pDefAngles = []
                 pIDs.append(particle.getObjId())
-                defU = particle.getCTF().getDefocusU()
-                defV = particle.getCTF().getDefocusV()
-                pDefAs.append((defU+defV)/2)
-                for pId, pDefA in zip(pIDs,pDefAs):
-                    if not pId in allParticlesDef.keys():
-                        allParticlesDef[pId]=[]
-                    allParticlesDef[pId].append(pDefA)
-        self.defMatrix = np.full(shape=(len(allParticlesDef.keys()),len(self.inputSets)),fill_value=np.NaN)
+                pDefUs.append(particle.getCTF().getDefocusU())
+                pDefVs.append(particle.getCTF().getDefocusV())
+                pDefAngles.append(particle.getCTF().getDefocusAngle())
+                for pId, pDefU, pDefV, pDefAngle in zip(pIDs, pDefUs, pDefVs, pDefAngles):
+                    if not pId in allParticlesDefU.keys():
+                        allParticlesDefU[pId]=[]
+                        allParticlesDefV[pId] = []
+                        allParticlesDefAngle[pId] = []
+                    allParticlesDefU[pId].append(pDefU)
+                    allParticlesDefV[pId].append(pDefV)
+                    allParticlesDefAngle[pId].append(pDefAngle)
+        self.defMatrixU = np.full(shape=(len(allParticlesDefU.keys()),len(self.inputSets)),fill_value=np.NaN)
+        self.defMatrixV = np.full(shape=(len(allParticlesDefV.keys()),len(self.inputSets)),fill_value=np.NaN)
+        self.defMatrixAngle = np.full(shape=(len(allParticlesDefAngle.keys()),len(self.inputSets)),fill_value=np.NaN)
         i=0
         self.pMatrixIdx={}
-        for pId,pdefi in allParticlesDef.items():
-            self.pMatrixIdx[pId]=i
-            for j in enumerate(pdefi):
-                self.defMatrix[i,j[0]]=j[1]
+        for pIdU, pdefiU in allParticlesDefU.items():
+            self.pMatrixIdx[pIdU]=i
+            for j in enumerate(pdefiU):
+                self.defMatrixU[i,j[0]]=j[1]
             i+=1
-
-        self.median = np.nanmedian(self.defMatrix, axis=1)
-        self.mad = np.empty_like(self.median)
-        for k in enumerate(self.median):
-            self.mad[k[0]] = np.nanmedian(np.abs(self.defMatrix[k[0],:] - self.median[k[0]]))
-        self.defMatrix = np.hstack((self.defMatrix, self.median.reshape(len(self.median),1)))
-        self.corrMatrix = np.zeros((self.defMatrix.shape[0],self.defMatrix.shape[1]))
-        defMatrixInvalid = np.ma.masked_invalid(self.defMatrix)
-        self.corrMatrix = np.ma.corrcoef(defMatrixInvalid,rowvar=False)
-        np.savetxt(self._getExtraPath("defocusMatrix.txt"),self.defMatrix)
-        np.savetxt(self._getExtraPath("correlationMatrix.txt"),self.corrMatrix)
+        l=0
+        for pIdV, pdefiV in allParticlesDefV.items():
+            self.pMatrixIdx[pIdV] = l
+            for k in enumerate(pdefiV):
+                self.defMatrixV[l, k[0]] = k[1]
+            l+=1
+        n=0
+        for pIdAngle, pdefiAngle in allParticlesDefAngle.items():
+            self.pMatrixIdx[pIdAngle] = n
+            for p in enumerate(pdefiAngle):
+                self.defMatrixAngle[n, p[0]] = p[1]
+            n+=1
+
+        self.medianU = np.nanmedian(self.defMatrixU, axis=1)
+        self.medianV = np.nanmedian(self.defMatrixV, axis=1)
+        self.medianAngle = np.nanmedian(self.defMatrixAngle, axis=1)
+        print(self.medianAngle)
+
+        self.madU = np.empty_like(self.medianU)
+        self.madV = np.empty_like(self.medianV)
+        self.madAngle = np.empty_like(self.medianAngle)
+
+        for l in enumerate(self.medianU):
+            self.madU[l[0]] = np.nanmedian(np.abs(self.defMatrixU[l[0],:] - self.medianU[l[0]]))
+        for m in enumerate(self.medianV):
+            self.madV[m[0]] = np.nanmedian(np.abs(self.defMatrixV[m[0], :] - self.medianV[m[0]]))
+        for o in enumerate(self.medianAngle):
+            self.madAngle[o[0]] = np.nanmedian(np.abs(self.defMatrixAngle[o[0], :] - self.medianAngle[o[0]]))
+
+        self.defMatrixU = np.hstack((self.defMatrixU, self.medianU.reshape(len(self.medianU),1)))
+        self.corrMatrixU = np.zeros((self.defMatrixU.shape[0],self.defMatrixU.shape[1]))
+        defMatrixInvalidU = np.ma.masked_invalid(self.defMatrixU)
+        self.corrMatrixU = np.ma.corrcoef(defMatrixInvalidU,rowvar=False)
+        np.savetxt(self._getExtraPath("defocusMatrixU.txt"),self.defMatrixU)
+        np.savetxt(self._getExtraPath("correlationMatrixU.txt"),self.corrMatrixU)
+
+        self.defMatrixV = np.hstack((self.defMatrixV, self.medianV.reshape(len(self.medianV),1)))
+        self.corrMatrixV = np.zeros((self.defMatrixV.shape[0],self.defMatrixV.shape[1]))
+        defMatrixInvalidV = np.ma.masked_invalid(self.defMatrixV)
+        self.corrMatrixV = np.ma.corrcoef(defMatrixInvalidV,rowvar=False)
+        np.savetxt(self._getExtraPath("defocusMatrixV.txt"),self.defMatrixV)
+        np.savetxt(self._getExtraPath("correlationMatrixV.txt"),self.corrMatrixV)
+
+        self.defMatrixAngle = np.hstack((self.defMatrixAngle, self.medianAngle.reshape(len(self.medianAngle),1)))
+        self.corrMatrixAngle = np.zeros((self.defMatrixAngle.shape[0],self.defMatrixAngle.shape[1]))
+        defMatrixInvalidAngle = np.ma.masked_invalid(self.defMatrixAngle)
+        self.corrMatrixAngle = np.ma.corrcoef(defMatrixInvalidAngle,rowvar=False)
+        np.savetxt(self._getExtraPath("defocusMatrixAngle.txt"),self.defMatrixAngle)
+        np.savetxt(self._getExtraPath("correlationMatrixAngle.txt"),self.corrMatrixAngle)
 
     def createOutputStep(self):
         """create as output a setOfParticles with a consensus estimation of local defocus (median) and its median
@@ -115,26 +169,34 @@ def createOutputStep(self):
             if id in self.pMatrixIdx:
                 index = self.pMatrixIdx[id]
                 newPart = part.clone()
-                pMedian = Float(self.median[index])
-                pMad = Float(self.mad[index])
-                setXmippAttribute(newPart.getCTF(), emlib.MDL_CTF_DEFOCUSA, pMedian)
-                setXmippAttribute(newPart.getCTF(), emlib.MDL_CTF_DEFOCUS_RESIDUAL, pMad)
+                pMedianU = Float(self.medianU[index]) # consensus defocus U
+                pMedianV = Float(self.medianV[index]) # consensus defocus V
+                pMedianAngle = Float(self.medianAngle[index]) # consensus defocus Angle
+
+                pMadU = Float(self.madU[index]) # residual consensus defocus U
+                newPart._ctfModel._defocusU.set(pMedianU)
+                newPart._ctfModel._defocusV.set(pMedianV)
+                newPart._ctfModel._defocusAngle.set(pMedianAngle)
+
+                setXmippAttribute(newPart.getCTF(), emlib.MDL_CTF_DEFOCUS_RESIDUAL, pMadU)
                 outputSet.append(newPart)
 
-        self._defineOutputs(outputParticles=outputSet)
+        self._defineOutputs(**{OUTPUTNAME:outputSet})
         self._defineSourceRelation(self.inputSet, outputSet)
 
 
     def _updateItem(self, particle, row):
         """update each particle in the set with the values computed"""
         pId = particle.getObjId()
-        setXmippAttribute(particle,emlib.MDL_CTF_DEFOCUSA,self.median[self.pMatrixIdx[pId]])
-        setXmippAttribute(particle,emlib.MDL_CTF_DEFOCUS_RESIDUAL,self.mad[self.pMatrixIdx[pId]])
+        setXmippAttribute(particle,emlib.MDL_CTF_DEFOCUSA,self.medianU[self.pMatrixIdx[pId]])
+        setXmippAttribute(particle,emlib.MDL_CTF_DEFOCUS_RESIDUAL,self.madU[self.pMatrixIdx[pId]])
+        setXmippAttribute(particle,emlib.MDL_CTF_DEFOCUS_ANGLE,self.medianAngle[self.pMatrixIdx[pId]])
+
 
     #--------------------------- INFO functions --------------------------------------------
     def _summary(self):
         summary = []
-        summary.append("Consensus local defocus and residual computed for %s particles" % self.getObjectTag('outputParticles'))
+        summary.append("Consensus local defocus and residual computed for %s particles" % self.getObjectTag(OUTPUTNAME))
         return summary
 
     def _methods(self):
diff --git a/xmipp3/protocols/protocol_convert_pdb.py b/xmipp3/protocols/protocol_convert_pdb.py
index 5fbf7ea6c..c9f271828 100644
--- a/xmipp3/protocols/protocol_convert_pdb.py
+++ b/xmipp3/protocols/protocol_convert_pdb.py
@@ -28,64 +28,78 @@
 # *
 # **************************************************************************
 
-import os
-
+# Scipion em imports
+from pwem.convert.headers import setMRCSamplingRate
+from pwem.emlib.image import ImageHandler
+from pwem.convert import cifToPdb, headers
+from pwem.objects import Volume, Transform, SetOfVolumes, AtomStruct, SetOfAtomStructs
+from pwem.protocols import ProtInitialVolume
 import pyworkflow.protocol.params as params
 import pyworkflow.protocol.constants as const
-from pyworkflow.utils import replaceBaseExt, removeExt, getExt, createLink
+from pyworkflow.utils import replaceBaseExt, removeExt, getExt, createLink, replaceExt, removeBaseExt
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
-from pwem.convert import cifToPdb, downloadPdb, headers
-from pwem.objects import Volume, Transform
-from pwem.protocols import ProtInitialVolume
+class XmippProtConvertPdb(ProtInitialVolume):
+    """ Convert atomic structure(s) into volume(s) """
+    _label = 'convert pdbs to volumes'
+    _devStatus = UPDATED
+    OUTPUT_NAME1 = "outputVolume"
+    OUTPUT_NAME2 = "outputVolumes"
+    OUTPUT_NAME3 = "outputPdb"
+    OUTPUT_NAME4 = "outputAtomStructs"
+    _possibleOutputs = {OUTPUT_NAME1: Volume, OUTPUT_NAME2: SetOfVolumes,
+                        OUTPUT_NAME3: AtomStruct, OUTPUT_NAME4: SetOfAtomStructs}
 
+    # --------------------------- Class constructor --------------------------------------------
+    def __init__(self, **args):
+        # Calling parent class constructor
+        super().__init__(**args)
 
-class XmippProtConvertPdb(ProtInitialVolume):
-    """ Convert a PDB file into a volume.  """
-    _label = 'convert a PDB'
-    IMPORT_FROM_ID = 0
-    IMPORT_OBJ = 1
-    IMPORT_FROM_FILES = 2 
+        # Defining execution mode. Steps will take place in parallel now
+        # Full tutorial on how to parallelize protocols can be read here:
+        # https://scipion-em.github.io/docs/release-3.0.0/docs/developer/parallelization.html
+        self.stepsExecutionMode = params.STEPS_PARALLEL
        
     # --------------------------- DEFINE param functions --------------------------------------------
     def _defineParams(self, form):
-        """ Define the parameters that will be input for the Protocol.
+        """
+        Define the parameters that will be input for the Protocol.
         This definition is also used to generate automatically the GUI.
         """
+        # Defining condition string for x, y, z coords
+        coordsCondition = 'setSize and not vol'
+
+        # Defining parallel arguments
+        form.addParallelSection(threads=4)
+
+        # Generating form
         form.addSection(label='Input')
-        form.addParam('inputPdbData', params.EnumParam, choices=['id', 'object', 'file'],
-                      label="Retrieve PDB from", default=self.IMPORT_FROM_ID,
-                      display=params.EnumParam.DISPLAY_HLIST,
-                      help='Retrieve PDB data from server, use a pdb Object, or a local file')
-        form.addParam('pdbId', params.StringParam, condition='inputPdbData == IMPORT_FROM_ID',
-                      label="Pdb Id ", allowsNull=True,
-                      help='Type a pdb Id (four alphanumeric characters).')
-        form.addParam('pdbObj', params.PointerParam, pointerClass='AtomStruct',
-                      label="Input pdb ", condition='inputPdbData == IMPORT_OBJ', allowsNull=True,
-                      help='Specify a pdb object.')
-        form.addParam('pdbFile', params.FileParam,
-                      label="File path", condition='inputPdbData == IMPORT_FROM_FILES', allowsNull=True,
-                      help='Specify a path to desired PDB structure.')
-        form.addParam('sampling', params.FloatParam, default=1.0, 
+        form.addParam('pdbObj', params.PointerParam, pointerClass='AtomStruct, SetOfAtomStructs',
+                      label="Input structure(s) ",
+                      help='Specify input atomic structure(s).')
+        form.addParam('sampling', params.FloatParam, default=1.0,
                       label="Sampling rate (Å/px)",
                       help='Sampling rate (Angstroms/pixel)')
         form.addParam('vol', params.BooleanParam, label='Use a volume as an empty template?', default=False,
+                      condition='not isinstance(pdbObj, SetOfAtomStructs)',
                       help='Use an existing volume to define the size and origin for the output volume. If this option'
                            'is selected, make sure that "Center PDB" in advanced parameters is set to *No*.')
         form.addParam('volObj', params.PointerParam, pointerClass='Volume',
-                      label="Input volume ", condition='vol', allowsNull=True,
+                      label="Input volume ", condition='not isinstance(pdbObj, SetOfAtomStructs) and vol', allowsNull=True,
                       help='The origin and the final size of the output volume will be taken from this volume.')
-        form.addParam('setSize', params.BooleanParam, label='Set final size?', default=False, condition='vol == False')
-        form.addParam('size_z', params.IntParam, condition='setSize', allowsNull=True,
-                      label="Final size (px) Z",
+        form.addParam('setSize', params.BooleanParam, label='Set final size?', default=False,
+                      condition='not vol and not isinstance(pdbObj, SetOfAtomStructs)')
+        form.addParam('size', params.IntParam,
+                      label="Box side size (px)", condition='isinstance(pdbObj, SetOfAtomStructs)', allowsNull=True,
+                      help='This size should apply to all volumes')
+        form.addParam('size_z', params.IntParam, condition=coordsCondition, allowsNull=True, label="Final size (px) Z",
                       help='Final size in Z in pixels. If no value is provided, protocol will estimate it.')
-        form.addParam('size_y', params.IntParam, condition='setSize', allowsNull=True,
-                      label="Final size (px) Y",
+        form.addParam('size_y', params.IntParam, condition=coordsCondition, allowsNull=True, label="Final size (px) Y",
                       help='Final size in Y in pixels. If no value is provided, protocol will estimate it.')
-        form.addParam('size_x', params.IntParam, condition='setSize', allowsNull=True,
-                      label="Final size (px) X",
+        form.addParam('size_x', params.IntParam, condition=coordsCondition, allowsNull=True, label="Final size (px) X",
                       help='Final size in X in pixels. If desired output size is x = y = z you can only fill this '
                            'field. If no value is provided, protocol will estimate it.')
-        form.addParam('centerPdb', params.BooleanParam, default=True, 
+        form.addParam('centerPdb', params.BooleanParam, default=True,
                       expertLevel=const.LEVEL_ADVANCED, 
                       label="Center PDB",
                       help='Center PDB with the center of mass')
@@ -94,86 +108,123 @@ def _defineParams(self, form):
                       label="Store centered PDB",
                       help='Set to \'Yes\' if you want to save centered PDB. '
                            'It will be stored in the output directory of this protocol')
+        form.addParam('clean', params.BooleanParam, default=True,
+                      expertLevel=const.LEVEL_ADVANCED, 
+                      label="Clean tmp files",
+                      help='Delete all non-output files once the protocol has finished producing them.')
     
     # --------------------------- INSERT steps functions --------------------------------------------
     def _insertAllSteps(self):
-        """ In this function the steps that are going to be executed should
+        """
+        In this function the steps that are going to be executed should
         be defined. Two of the most used functions are: _insertFunctionStep or _insertRunJobStep
         """
-        if self.inputPdbData == self.IMPORT_FROM_ID:
-            self._insertFunctionStep('pdbDownloadStep')
-        self._insertFunctionStep('convertPdbStep')
-        self._insertFunctionStep('createOutput')
-    
+        # Checking if input is set or not
+        isSet = not isinstance(self.pdbObj.get(), AtomStruct)
+
+        # Generating pdb list and obtaining input sampling rate
+        pdbList = self._getPdbFileNames()
+        samplingR = self.sampling.get()
+
+        # Defining list of function ids to be waited by the createOutput function
+        deps = []
+        for pdbFn in pdbList:
+            # Calling processConversion in parallel with each input data
+            deps.append(self._insertFunctionStep(self.processConversion, pdbFn, samplingR, isSet, prerequisites=[]))
+        
+        # Insert output conversion step
+        self._insertFunctionStep(self.createOutput, isSet, samplingR, prerequisites=deps)
+
     # --------------------------- STEPS functions --------------------------------------------
-    def pdbDownloadStep(self):
-        """Download all pdb files in file_list and unzip them."""
-        downloadPdb(self.pdbId.get(), self._getPdbFileName(), self._log)
+    def processConversion(self, pdb, samplingR, isSet):
+        """ This step runs the pdb conversion. """
+        # Converting all input atomic structures to .pdb
+        pdb = self._convertToPdb(pdb)
         
-    def convertPdbStep(self):
-        """ Although is not mandatory, usually is used by the protocol to
-        register the resulting outputs in the database.
-        """
-        pdbFn = self._getPdbFileName()
-        outFile = removeExt(self._getVolName())
-        if getExt(pdbFn)==".cif":
-            pdbFn2=replaceBaseExt(pdbFn, 'pdb')
-            cifToPdb(pdbFn, pdbFn2)
-            pdbFn = pdbFn2
-
-        if " " in pdbFn:
-            pdbFn_extra = self._getExtraPath(os.path.basename(pdbFn.replace(" ", "_")))
-        else:
-            pdbFn_extra = self._getExtraPath(os.path.basename(pdbFn))
+        # Generating output file for each input
+        outFile = removeExt(pdb)
 
-        createLink(pdbFn, pdbFn_extra)
+        # Getting args for program call
+        args = self._getConversionArgs(pdb, samplingR, outFile, isSet=isSet)
 
-        samplingR = self.sampling.get()
+        # Showing input and output file names
+        self.info("Input file: " + pdb)
+        self.info("Output file: " + outFile)
 
-        args = '-i %s --sampling %f -o %s' % (pdbFn_extra, samplingR, outFile)
-        
-        if self.centerPdb:
-            args += ' --centerPDB'
-            if self.outPdb:
-                args += ' --oPDB'
+        # Calling program
+        self.runJob("xmipp_volume_from_pdb", args)
 
-        if self.vol:
-            vol = self.volObj.get()
-            size = vol.getDim()
-            ccp4header = headers.Ccp4Header(vol.getFileName(), readHeader=True)
-            self.shifts = ccp4header.getOrigin()
-            args += ' --size %d %d %d --orig %d %d %d' % (size[2], size[1], size[0],
-                                                          self.shifts[0]/samplingR,
-                                                          self.shifts[1]/samplingR,
-                                                          self.shifts[2]/samplingR)
+    def createOutput(self, isSet, samplingR):
+        extraPath = self._getExtraPath()
 
-        if self.setSize:
-            args += ' --size'
+        # Saving centered pdbs if wanted
+        if self.centerPdb and self.outPdb:
+            centeredPdbs = self._getExtraPath('centerted')
+            self.runJob('mkdir -p',  centeredPdbs)
+            self.runJob('mv', '{}/*_centered.pdb {}'.format(extraPath, centeredPdbs))
 
-        if self.size_x.hasValue() and self.setSize:
-            args += ' %d' % self.size_x.get()
+        # Generating output objects
+        outputVol = self._createSetOfVolumes() if isSet else None
+        pdbOut = self.centerPdb and self.outPdb
+        if pdbOut:
+            outputPdb = self._createSetOfPDBs() if isSet else None
 
-        if self.size_y.hasValue() and self.size_z.hasValue() and self.setSize:
-            args += ' %d %d' % (self.size_y.get(), self.size_z.get())
+        # Instantiating image handler
+        ih = ImageHandler()
 
-        self.info("Input file: " + pdbFn)
-        self.info("Output file: " + outFile)
+        # Generating input list
+        pdbFns = self._getPdbFileNames()
+
+        # Converting volumes. Since xmipp generates always a .vol we do the conversion here
+        for pdbFn in pdbFns:
+            # Generating ouput mrc
+            volumeFn = self._getExtraPath(replaceBaseExt(pdbFn, 'vol'))
+            mrcFn = replaceExt(volumeFn, 'mrc')
+            ih.convert(volumeFn, mrcFn)
+            setMRCSamplingRate(mrcFn, samplingR)
+            # Generating output volume object from mrc
+            volume = Volume()
+            volume.setSamplingRate(samplingR)
+            volume.setFileName(mrcFn)
+            volume.fixMRCVolume(setSamplingRate=True)
+            # If input is set, append volume to set
+            if isSet:
+                outputVol.append(volume)
+            # Generating output PDB
+            if pdbOut:
+                centeredPdbFn = '{}/{}_centered.pdb'.format(centeredPdbs, removeBaseExt(pdbFn))
+                atom = AtomStruct(filename=centeredPdbFn)
+                atom.setVolume(volume)
+                if isSet:
+                    outputPdb.append(atom)
         
-        program = "xmipp_volume_from_pdb"
-        self.runJob(program, args)
-
-    def createOutput(self):
-        volume = Volume()
-        volume.setSamplingRate(self.sampling.get())
-        volume.setFileName(self._getVolName())
-        if self.vol:
-            origin = Transform()
-            origin.setShiftsTuple(self.shifts)
-            volume.setOrigin(origin)
-        self._defineOutputs(outputVolume=volume)
-        if self.inputPdbData == self.IMPORT_OBJ:
-            self._defineSourceRelation(self.pdbObj, volume)
-    
+        if not isSet:
+            # If input is single atom struct, get produced volume as output
+            outputVol = volume
+            if self.vol:
+                origin = Transform()
+                origin.setShiftsTuple(self.shifts)
+                outputVol.setOrigin(origin)
+
+            if pdbOut:
+                outputPdb = atom
+
+        # Setting ouput sampling rate
+        outputVol.setSamplingRate(samplingR)
+
+        # Removing temporary files
+        if self.clean:
+            self.runJob('rm', '-rf {}/*.vol {}/*.cif {}/*.pdb'.format(extraPath, extraPath, extraPath))
+
+        if pdbOut:
+            outputPdbName = self.OUTPUT_NAME4 if isSet else self.OUTPUT_NAME3
+            self._defineOutputs(**{outputPdbName: outputPdb})
+
+        # Defining output
+        outputVolName = self.OUTPUT_NAME2 if isSet else self.OUTPUT_NAME1
+        self._defineOutputs(**{outputVolName: outputVol})
+        self._defineSourceRelation(self.pdbObj, outputVol)
+
     # --------------------------- INFO functions --------------------------------------------
     def _summary(self):
         """ Even if the full set of parameters is available, this function provides
@@ -181,15 +232,8 @@ def _summary(self):
         """ 
         summary = []
         # Add some lines of summary information
-        if not hasattr(self, 'outputVolume'):
-            summary.append("outputVolume not ready yet.")
-        else:
-            if self.inputPdbData == self.IMPORT_FROM_ID:
-                summary.append("Input PDB ID: %s" % self.pdbId.get())
-            elif self.inputPdbData == self.IMPORT_OBJ:
-                summary.append("Input PDB File: %s" % self.pdbObj.get().getFileName())
-            else:
-                summary.append("Input PDB File: %s" % self.pdbFile.get())
+        if not (hasattr(self, self.OUTPUT_NAME1) or hasattr(self, self.OUTPUT_NAME2)):
+            summary.append("The output is not ready yet.")
         return summary
       
     def _validate(self):
@@ -198,21 +242,72 @@ def _validate(self):
         empty the protocol can be executed.
         """
         errors = []
-        if self.inputPdbData == self.IMPORT_FROM_ID:
-            lenStr = len(self.pdbId.get())
-            if lenStr != 4:
-                errors = ["Pdb id is composed only by four alphanumeric characters"]
-        
+
+        # Checking if MPI is selected (only threads are allowed)
+        if self.numberOfMpi > 1:
+            errors.append('MPI cannot be selected, because Scipion is going to drop support for it. Select threads instead.')
+
         return errors
     
     # --------------------------- UTLIS functions --------------------------------------------
-    def _getPdbFileName(self):
-        if self.inputPdbData == self.IMPORT_FROM_ID:
-            return self._getExtraPath('%s.cif' % self.pdbId.get())
-        elif self.inputPdbData == self.IMPORT_OBJ:
-            return self.pdbObj.get().getFileName()
+    def _getPdbFileNames(self):
+        """ This function returns the input file/s stored in a list. """
+        pbdObj = self.pdbObj.get()
+        if isinstance(pbdObj, AtomStruct):
+            # If it is a single AtomStruct, place it inside a list of 1 element
+            return [pbdObj.getFileName()]
         else:
-            return self.pdbFile.get()
+            # If it is a SetOfAtom Structs, get all of the elements iterating the set
+            return [i.getFileName() for i in pbdObj]
+
+    def _convertToPdb(self, pdbRaw):
+        """ This function receives an atomic struct file, and converts it to .pdb if it is in .cif format. """
+        # Get output path for pdb file
+        convertedPdb = self._getExtraPath(replaceBaseExt(pdbRaw, 'pdb')).replace(" ", "_")
+
+        # If file extension is .cif, convert to .pdb, or else we link it as is
+        if getExt(pdbRaw) == ".cif":
+            cifToPdb(pdbRaw, convertedPdb)
+        else:
+            createLink(pdbRaw, convertedPdb)
+
+        # Return optionally converted file
+        return convertedPdb
     
-    def _getVolName(self):
-        return self._getExtraPath(replaceBaseExt(self._getPdbFileName().replace(" ", "_"), "vol"))
+    def _getConversionArgs(self, pdb, samplingRate, outputFile, isSet=False):
+        """ This function generates the arguments to convert an input pdb to a volume. """
+        # Main arguments
+        args = '-i "%s" --sampling %f -o "%s"' % (pdb, samplingRate, outputFile)
+
+        # Flag for centered pdbs
+        if self.centerPdb:
+            args += ' --centerPDB'
+            # Flag for output pdbs
+            if self.outPdb:
+                args += ' --oPDB'
+        
+        # Setting size and origin if selected
+        if isSet:
+            args += ' --size %d' % (self.size.get())
+        else:
+            if self.vol:
+                vol = self.volObj.get()
+                size = vol.getDim()
+                ccp4header = headers.Ccp4Header(vol.getFileName(), readHeader=True)
+                self.shifts = ccp4header.getOrigin()
+                args += ' --size %d %d %d --orig %d %d %d' % (size[2], size[1], size[0],
+                                                            self.shifts[0]/samplingRate,
+                                                            self.shifts[1]/samplingRate,
+                                                            self.shifts[2]/samplingRate)
+            else:
+                if self.setSize:
+                    args += ' --size'
+
+                    if self.size_x.hasValue():
+                        args += ' %d' % self.size_x.get()
+
+                    if self.size_y.hasValue() and self.size_z.hasValue():
+                        args += ' %d %d' % (self.size_y.get(), self.size_z.get())
+        
+        # Returning produced args
+        return args
\ No newline at end of file
diff --git a/xmipp3/protocols/protocol_ctf_consensus.py b/xmipp3/protocols/protocol_ctf_consensus.py
index 6f81c5643..eb153012a 100644
--- a/xmipp3/protocols/protocol_ctf_consensus.py
+++ b/xmipp3/protocols/protocol_ctf_consensus.py
@@ -41,6 +41,7 @@
 from pwem.protocols import ProtCTFMicrographs
 from pwem.emlib.metadata import Row
 from pyworkflow.protocol.constants import (STATUS_NEW)
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 from pwem import emlib
 import xmipp3
@@ -58,6 +59,7 @@ class XmippProtCTFConsensus(ProtCTFMicrographs):
     the agreement with a secondary CTF for the same set of micrographs.
     """
     _label = 'ctf consensus'
+    _devStatus = UPDATED
     _lastUpdateVersion = VERSION_3_0
 
     def __init__(self, **args):
@@ -189,7 +191,7 @@ def _defineParams(self, form):
                            'If *No*, only the primary metadata (plus consensus '
                            'scores) will be in the resulting CTF.')
 
-        form.addParallelSection(threads=1, mpi=1)
+        form.addParallelSection(threads=4, mpi=1)
 
 # --------------------------- INSERT steps functions -------------------------
     def _insertAllSteps(self):
@@ -658,10 +660,8 @@ def compareValue(ctf, label, comp, crit):
 
             if self.xmippCTF == INPUT1:
                 ctfX = ctf
-            elif self.xmippCTF == INPUT2:
-                ctfX = self.allCtf2.get(ctfId)
             else:
-                raise Exception("No xmipp ctf was found")
+                ctfX = self.allCtf2.get(ctfId)
 
 
             secondCondition = (
diff --git a/xmipp3/protocols/protocol_deep_center.py b/xmipp3/protocols/protocol_deep_center.py
new file mode 100644
index 000000000..377075011
--- /dev/null
+++ b/xmipp3/protocols/protocol_deep_center.py
@@ -0,0 +1,174 @@
+# **************************************************************************
+# *
+# * Authors:     COS Sorzano and Adrian Sansinena
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow import VERSION_3_0
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, StringParam, FloatParam, EnumParam,
+                                        IntParam, BooleanParam, GPU_LIST)
+from pyworkflow.protocol.constants import LEVEL_ADVANCED
+from pyworkflow.utils.path import moveFile, cleanPattern
+from pwem.protocols import ProtAlign2D
+from pwem.emlib.metadata import iterRows, getFirstRow
+import pwem.emlib.metadata as md
+from xmipp3.convert import createItemMatrix, setXmippAttributes, readSetOfParticles, writeSetOfParticles
+from pwem import ALIGN_PROJ
+from pwem import emlib
+from pwem.emlib.image import ImageHandler
+import os
+import sys
+import numpy as np
+import math
+from shutil import copy
+from os import remove
+from os.path import exists, join
+import xmipp3
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+class XmippProtDeepCenter(ProtAlign2D, xmipp3.XmippProtocol):
+    """Learns a model to center particles using deep learning. Particles must be previously centered with respect
+       to a volume, and they must have 3D alignment information. """
+    _label = 'deep center'
+    _lastUpdateVersion = VERSION_3_0
+    _devStatus = BETA
+    _conda_env = 'xmipp_DLTK_v1.0'
+    _cond_modelPretrainTrue = 'modelPretrain==True'
+    _cond_modelPretrainFalse = 'modelPretrain==False'
+
+
+    def __init__(self, **args):
+        ProtAlign2D.__init__(self, **args)
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        form.addHidden(GPU_LIST, StringParam, default='0',
+                       expertLevel=LEVEL_ADVANCED,
+                       label="Choose GPU IDs",
+                       help="GPU may have several cores. Set it to zero"
+                            " if you do not know what we are talking about."
+                            " First core index is 0, second 1 and so on.")
+        form.addSection(label='Input')
+        form.addParam('inputTrainSet', PointerParam, label="Input training set",
+                      pointerClass='SetOfParticles',
+                      pointerCondition='hasAlignment2D or hasAlignmentProj',
+                      help='The set of particles previously aligned to be used as training set')
+
+        form.addParam('Xdim', IntParam, label="Size of the images for training", default=128)
+
+        form.addParam('modelPretrain', BooleanParam, default=False,
+                      label='Choose if you want to use a pretrained model',
+                      help='Set "yes" if you want to use a previously trained model. '
+                           'If you choose "no" new models will be trained.')
+
+        form.addParam('pretrainedModels', PointerParam,
+                      pointerClass='XmippProtDeepCenter',
+                      condition=self._cond_modelPretrainTrue,
+                      label='Pretrained model',
+                      help='Select the pretrained model. ')
+
+        form.addSection(label='Training parameters')
+        form.addParam('numCenModels', IntParam,
+                      label="Number of models for particle centering", default=5,
+                      help="Choose number of models you want to train. More than 1 is recommended only if next step "
+                           "is inference.")
+
+        form.addParam('numEpochs_cen', IntParam,
+                      label="Number of epochs",
+                      default=25, expertLevel=LEVEL_ADVANCED,
+                      help="Number of epochs for training.")
+
+        form.addParam('batchSize', IntParam,
+                      label="Batch size for training",
+                      default=32, expertLevel=LEVEL_ADVANCED,
+                      help="Batch size for training.")
+
+        form.addParam('learningRate', FloatParam,
+                      label="Learning rate",
+                      default=0.001, expertLevel=LEVEL_ADVANCED,
+                      help="Learning rate for training.")
+
+        form.addParam('sigma', FloatParam,
+                      label="Image shifting",
+                      default=10, expertLevel=LEVEL_ADVANCED,
+                      help="A measure of the number of pixels that particles can be shifted in each direction from the center.")
+
+        form.addParam('patience', IntParam,
+                      label="Patience",
+                      default=5, expertLevel=LEVEL_ADVANCED,
+                      help="Training will be stopped if the number of epochs without improvement is greater than "
+                           "patience.")
+
+        form.addParallelSection(threads=1, mpi=1)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertAllSteps(self):
+        self.trainImgsFn = self._getExtraPath('train_input_imgs.xmd')
+        if self.useQueueForSteps() or self.useQueue():
+            myStr = os.environ["CUDA_VISIBLE_DEVICES"]
+        else:
+            myStr = self.gpuList.get()
+            os.environ["CUDA_VISIBLE_DEVICES"] = self.gpuList.get()
+        numGPU = myStr.split(',')
+
+        self._insertFunctionStep("convertStep")
+        self._insertFunctionStep("train", numGPU[0])
+
+    # --------------------------- STEPS functions ---------------------------------------------------
+    def convertStep(self):
+        writeSetOfParticles(self.inputTrainSet.get(), self.trainImgsFn)
+        self.runJob("xmipp_image_resize",
+                    "-i %s -o %s --save_metadata_stack %s --fourier %d" %
+                    (self.trainImgsFn,
+                        self._getExtraPath('trainingResized.stk'),
+                        self._getExtraPath('trainingResized.xmd'),
+                        self.Xdim), numberOfMpi=self.numberOfThreads.get()*self.numberOfMpi.get())
+
+    def train(self, gpuId):
+
+        sig = self.sigma.get()
+
+        self.pretrained = 'no'
+        self.pathToModel = 'none'
+        if self.modelPretrain:
+            self.model = self.pretrainedModels.get()
+            self.pretrained = 'yes'
+            self.pathToModel = self.model._getExtraPath("modelCenter0.h5")
+
+        args = "%s %s %f %d %d %s %d %f %d %s %s" % (
+            self._getExtraPath("trainingResized.xmd"), self._getExtraPath("modelCenter"), sig,
+            self.numEpochs_cen, self.batchSize.get(), gpuId, self.numCenModels.get(), self.learningRate.get(),
+            self.patience.get(), self.pretrained, self.pathToModel)
+        print(args)
+        self.runJob("xmipp_deep_center", args, numberOfMpi=1, env=self.getCondaEnv())
+
+        remove(self._getExtraPath("trainingResized.xmd"))
+        remove(self._getExtraPath("trainingResized.stk"))
+
+    # --------------------------- INFO functions --------------------------------
+    def _methods(self):
+        methods = []
+        if hasattr(self, 'outputParticles'):
+            methods.append("We learned a model to center particles from %i input images (%s)." \
+                % (self.inputSet.get().getSize(), self.getObjectTag('inputSet')))
+        return methods
diff --git a/xmipp3/protocols/protocol_deep_center_predict.py b/xmipp3/protocols/protocol_deep_center_predict.py
new file mode 100644
index 000000000..23b3b4a02
--- /dev/null
+++ b/xmipp3/protocols/protocol_deep_center_predict.py
@@ -0,0 +1,158 @@
+# **************************************************************************
+# *
+# * Authors:     COS Sorzano, Erney Ramirez and Adrian Sansinena
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow import VERSION_3_0
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, StringParam, FloatParam,
+                                        IntParam, BooleanParam, GPU_LIST, EnumParam)
+from pyworkflow.protocol.constants import LEVEL_ADVANCED
+from pyworkflow.utils.path import moveFile, cleanPattern
+from pwem.protocols import ProtAlign2D
+from pwem.emlib.metadata import iterRows, getFirstRow
+import pwem.emlib.metadata as md
+from xmipp3.convert import createItemMatrix, setXmippAttributes, readSetOfParticles, writeSetOfParticles
+from pwem import ALIGN_PROJ
+from pwem import emlib
+from pwem.emlib.image import ImageHandler
+import os
+import sys
+import numpy as np
+import math
+from shutil import copy
+from os import remove
+from os.path import exists, join
+import xmipp3
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+class XmippProtDeepCenterPredict(ProtAlign2D, xmipp3.XmippProtocol):
+    """Predict the center particles using deep learning.""" 
+    _label = 'deep center predict'
+    _lastUpdateVersion = VERSION_3_0
+    _devStatus = BETA
+
+    _conda_env = 'xmipp_DLTK_v1.0'
+    _cond_predictAnglesTrue = 'predictAngles==True'
+    _cond_predictAnglesFalse = 'predictAngles==False'
+    _cond_trainedModelTrue = 'trainedModel==True'
+    _cond_trainedModelFalse = 'trainedModel==False'
+    _cond_needModel = '(trainedModel==True) or (predictAngles==True)'
+
+    def __init__(self, **args):
+        ProtAlign2D.__init__(self, **args)
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+
+        form.addHidden(GPU_LIST, StringParam, default='0',
+                       expertLevel=LEVEL_ADVANCED,
+                       label="Choose GPU IDs",
+                       help="GPU may have several cores. Set it to zero"
+                            " if you do not know what we are talking about."
+                            " First core index is 0, second 1 and so on.")
+
+        form.addSection(label='Input')
+
+        form.addParam('inputImageSet', PointerParam, label="Input Image set",
+                      pointerClass='SetOfParticles',
+                      help='The set of particles to predict shift')
+
+        form.addParam('Xdim', IntParam, label="Size of the images for training", default=128)
+
+        form.addParam('inputModel', PointerParam, label="Model trained",
+                      pointerClass='XmippProtDeepCenter',
+                      help='The model to predict angles')
+
+        form.addSection(label='Consensus')
+
+        form.addParam('numModels', IntParam,
+                      label="Number of models trained", default=5)
+
+        form.addParam('tolerance', IntParam,
+                      label="Tolerance in pixels", default=3,
+                      help="Max difference between predictions and their mean value.")
+
+        form.addParam('maxModels', IntParam,
+                      label="Maximum number of models dropped per particle", default=0,
+                      help="If more models are dropped, the particle is discarded.")
+
+        form.addParallelSection(threads=1, mpi=1)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertAllSteps(self):
+        self.predictImgsFn = self._getExtraPath('predict_input_imgs.xmd')
+
+        if self.useQueueForSteps() or self.useQueue():
+            myStr = os.environ["CUDA_VISIBLE_DEVICES"]
+        else:
+            myStr = self.gpuList.get()
+            os.environ["CUDA_VISIBLE_DEVICES"] = self.gpuList.get()
+        numGPU = myStr.split(',')
+
+        self._insertFunctionStep("convertStep")
+        self._insertFunctionStep("predict", numGPU[0])
+        self._insertFunctionStep('createOutputStep')
+
+    # --------------------------- STEPS functions ---------------------------------------------------
+    def convertStep(self):
+        writeSetOfParticles(self.inputImageSet.get(), self.predictImgsFn)
+        self.runJob("xmipp_image_resize",
+                    "-i %s -o %s --save_metadata_stack %s --fourier %d" %
+                    (self.predictImgsFn,
+                        self._getExtraPath('trainingResized.stk'),
+                        self._getExtraPath('trainingResized.xmd'),
+                        self.Xdim), numberOfMpi=self.numberOfThreads.get()*self.numberOfMpi.get())
+
+    def predict(self, gpuId):
+        self.modelAng = self.inputModel.get()
+        args = "%s %s %s %s %s %d %d %d" % (
+            self._getExtraPath("trainingResized.xmd"), gpuId, self._getPath(), self.predictImgsFn,
+            self.modelAng._getExtraPath("modelCenter"), self.numModels.get(), self.tolerance.get(),
+            self.maxModels.get())
+
+        self.runJob("xmipp_deep_center_predict", args, numberOfMpi=1, env=self.getCondaEnv())
+
+        remove(self._getExtraPath("trainingResized.xmd"))
+        remove(self._getExtraPath("trainingResized.stk"))
+
+    def createOutputStep(self):
+        imgFname = self._getPath('predict_results.xmd')
+        outputSet = self._createSetOfParticles()
+
+        readSetOfParticles(imgFname, outputSet)
+        outputSet.copyInfo(self.inputImageSet.get())
+        outputSet.setAlignmentProj()
+
+        self._defineOutputs(outputParticles=outputSet)
+        self._store(outputSet)
+        self._defineSourceRelation(self.inputImageSet.get(), outputSet)
+
+    # --------------------------- INFO functions --------------------------------
+    def _methods(self):
+        methods = []
+        if hasattr(self, 'outputParticles'):
+            methods.append("We learned a model to center particles from %i input images (%s)." \
+                % (self.inputSet.get().getSize(), self.getObjectTag('inputSet')))
+        return methods
diff --git a/xmipp3/protocols/protocol_deep_global_assignment.py b/xmipp3/protocols/protocol_deep_global_assignment.py
new file mode 100644
index 000000000..eeda8ea02
--- /dev/null
+++ b/xmipp3/protocols/protocol_deep_global_assignment.py
@@ -0,0 +1,176 @@
+# **************************************************************************
+# *
+# * Authors:     COS Sorzano and Adrian Sansinena
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow import VERSION_3_0
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, StringParam, FloatParam, EnumParam,
+                                        IntParam, BooleanParam, GPU_LIST)
+from pyworkflow.protocol.constants import LEVEL_ADVANCED
+from pyworkflow.utils.path import moveFile, cleanPattern
+from pwem.protocols import ProtAlign2D
+from pwem.emlib.metadata import iterRows, getFirstRow
+import pwem.emlib.metadata as md
+from xmipp3.convert import createItemMatrix, setXmippAttributes, readSetOfParticles, writeSetOfParticles
+from pwem import ALIGN_PROJ
+from pwem import emlib
+from pwem.emlib.image import ImageHandler
+import os
+import sys
+import numpy as np
+import math
+from shutil import copy
+from os import remove
+from os.path import exists, join
+import xmipp3
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+
+class XmippProtDeepGlobalAssignment(ProtAlign2D, xmipp3.XmippProtocol):
+    """Learns a model to assign angles to particles using deep learning. Particles must be previously centered with respect
+       to a volume, and they must have 3D alignment information. """
+    _label = 'deep global assignment'
+    _lastUpdateVersion = VERSION_3_0
+    _devStatus = BETA
+
+    _conda_env = 'xmipp_DLTK_v1.0'
+    _cond_modelPretrainTrue = 'modelPretrain==True'
+    _cond_modelPretrainFalse = 'modelPretrain==False'
+
+    def __init__(self, **args):
+        ProtAlign2D.__init__(self, **args)
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        form.addHidden(GPU_LIST, StringParam, default='0',
+                       expertLevel=LEVEL_ADVANCED,
+                       label="Choose GPU IDs",
+                       help="GPU may have several cores. Set it to zero"
+                            " if you do not know what we are talking about."
+                            " First core index is 0, second 1 and so on.")
+        form.addSection(label='Input')
+        form.addParam('inputTrainSet', PointerParam, label="Input training set",
+                      pointerClass='SetOfParticles',
+                      pointerCondition='hasAlignment2D or hasAlignmentProj',
+                      help='The set of particles previously aligned to be used as training set')
+
+        form.addParam('Xdim', IntParam, label="Size of the images for training", default=128)
+
+        form.addParam('modelPretrain', BooleanParam, default=False,
+                      label='Choose if you want to use a pretrained model',
+                      help='Set "yes" if you want to use a previously trained model. '
+                           'If you choose "no" new models will be trained.')
+
+        form.addParam('pretrainedModels', PointerParam,
+                      pointerClass='XmippProtDeepGlobalAssignment',
+                      condition=self._cond_modelPretrainTrue,
+                      label='Pretrained model',
+                      help='Select the pretrained model. ')
+
+        form.addSection(label='Training parameters')
+        form.addParam('numAngModels', IntParam,
+                      label="Number of models for angular assignment", default=5,
+                      help="Choose number of models you want to train. More than 1 is recommended only if next step "
+                           "is inference.")
+
+        form.addParam('numEpochs_ang', IntParam,
+                      label="Number of epochs",
+                      default=25, expertLevel=LEVEL_ADVANCED,
+                      help="Number of epochs for training.")
+
+        form.addParam('batchSize', IntParam,
+                      label="Batch size for training",
+                      default=32, expertLevel=LEVEL_ADVANCED,
+                      help="Batch size for training.")
+
+        form.addParam('learningRate', FloatParam,
+                      label="Learning rate",
+                      default=0.001, expertLevel=LEVEL_ADVANCED,
+                      help="Learning rate for training.")
+
+        form.addParam('sigma', FloatParam,
+                      label="Image shifting",
+                      default=2, expertLevel=LEVEL_ADVANCED,
+                      help="Maximum number of pixels that particles can be shifted in each direction from the center.")
+
+        form.addParam('patience', IntParam,
+                      label="Patience",
+                      default=20, expertLevel=LEVEL_ADVANCED,
+                      help="Training will be stopped if the number of epochs without improvement is greater than "
+                           "patience.")
+
+        form.addParallelSection(threads=1, mpi=1)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertAllSteps(self):
+        self.trainImgsFn = self._getExtraPath('train_input_imgs.xmd')
+        if self.useQueueForSteps() or self.useQueue():
+            myStr = os.environ["CUDA_VISIBLE_DEVICES"]
+        else:
+            myStr = self.gpuList.get()
+            os.environ["CUDA_VISIBLE_DEVICES"] = self.gpuList.get()
+        numGPU = myStr.split(',')
+
+        self._insertFunctionStep("convertStep")
+        self._insertFunctionStep("train", numGPU[0])
+
+    # --------------------------- STEPS functions ---------------------------------------------------
+    def convertStep(self):
+        writeSetOfParticles(self.inputTrainSet.get(), self.trainImgsFn)
+        self.runJob("xmipp_image_resize",
+                    "-i %s -o %s --save_metadata_stack %s --fourier %d" %
+                    (self.trainImgsFn,
+                     self._getExtraPath('trainingResized.stk'),
+                     self._getExtraPath('trainingResized.xmd'),
+                     self.Xdim), numberOfMpi=self.numberOfThreads.get() * self.numberOfMpi.get())
+
+    def train(self, gpuId):
+
+        sig = self.sigma.get()
+
+        self.pretrained = 'no'
+        self.pathToModel = 'none'
+        if self.modelPretrain:
+            self.model = self.pretrainedModels.get()
+            self.pretrained = 'yes'
+            self.pathToModel = self.model._getExtraPath("modelAngular0.h5")
+
+        args = "%s %s %f %d %d %s %d %f %d %s %s" % (
+            self._getExtraPath("trainingResized.xmd"), self._getExtraPath("modelAngular"), sig,
+            self.numEpochs_ang, self.batchSize.get(), gpuId, self.numAngModels.get(), self.learningRate.get(),
+            self.patience.get(), self.pretrained, self.pathToModel)
+        print(args)
+        self.runJob("xmipp_deep_global_assignment", args, numberOfMpi=1, env=self.getCondaEnv())
+
+        remove(self._getExtraPath("trainingResized.xmd"))
+        remove(self._getExtraPath("trainingResized.stk"))
+
+    # --------------------------- INFO functions --------------------------------
+    def _methods(self):
+        methods = []
+        if hasattr(self, 'outputParticles'):
+            methods.append("We learned a model to center particles from %i input images (%s)." \
+                           % (self.inputSet.get().getSize(), self.getObjectTag('inputSet')))
+        return methods
diff --git a/xmipp3/protocols/protocol_deep_global_assignment_predict.py b/xmipp3/protocols/protocol_deep_global_assignment_predict.py
new file mode 100644
index 000000000..d1c163800
--- /dev/null
+++ b/xmipp3/protocols/protocol_deep_global_assignment_predict.py
@@ -0,0 +1,152 @@
+# **************************************************************************
+# *
+# * Authors:     COS Sorzano, Erney Ramirez and Adrian Sansinena
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow import VERSION_3_0
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, StringParam, FloatParam,
+                                        IntParam, BooleanParam, GPU_LIST, EnumParam)
+from pyworkflow.protocol.constants import LEVEL_ADVANCED
+from pyworkflow.utils.path import moveFile, cleanPattern
+from pwem.protocols import ProtAlign2D
+from pwem.emlib.metadata import iterRows, getFirstRow
+import pwem.emlib.metadata as md
+from xmipp3.convert import createItemMatrix, setXmippAttributes, readSetOfParticles, writeSetOfParticles
+from pwem import ALIGN_PROJ
+from pwem import emlib
+from pwem.emlib.image import ImageHandler
+import os
+import sys
+import numpy as np
+import math
+from shutil import copy
+from os import remove
+from os.path import exists, join
+import xmipp3
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+
+class XmippProtDeepGlobalAssignmentPredict(ProtAlign2D, xmipp3.XmippProtocol):
+    """Predict the center particles using deep learning."""
+    _label = 'deep global assignment predict'
+    _lastUpdateVersion = VERSION_3_0
+    _devStatus = BETA
+
+    _conda_env = 'xmipp_DLTK_v1.0'
+
+    def __init__(self, **args):
+        ProtAlign2D.__init__(self, **args)
+
+    # --------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+
+        form.addHidden(GPU_LIST, StringParam, default='0',
+                       expertLevel=LEVEL_ADVANCED,
+                       label="Choose GPU IDs",
+                       help="GPU may have several cores. Set it to zero"
+                            " if you do not know what we are talking about."
+                            " First core index is 0, second 1 and so on.")
+        form.addSection(label='Input')
+
+        form.addParam('inputImageSet', PointerParam, label="Input Image set",
+                      pointerClass='SetOfParticles',
+                      help='The set of particles to predict shift')
+
+        form.addParam('Xdim', IntParam, label="Size of the images for training", default=128)
+
+        form.addParam('inputModel', PointerParam, label="Model trained",
+                      pointerClass='XmippProtDeepGlobalAssignment',
+                      help='The model to predict angles')
+
+        form.addSection(label='Consensus')
+
+        form.addParam('numAngModels', IntParam,
+                      label="Number of models trained", default=5)
+
+        form.addParam('tolerance', IntParam,
+                      label="Angular tolerance (º)", default=15,
+                      help="Max angular difference between predictions and their mean value.")
+
+        form.addParam('maxModels', IntParam,
+                      label="Maximum number of models dropped per particle", default=0,
+                      help="If more models are dropped, the particle is discarded.")
+
+        form.addParallelSection(threads=1, mpi=1)
+
+    # --------------------------- INSERT steps functions --------------------------------------------
+    def _insertAllSteps(self):
+        self.predictImgsFn = self._getExtraPath('predict_input_imgs.xmd')
+
+        if self.useQueueForSteps() or self.useQueue():
+            myStr = os.environ["CUDA_VISIBLE_DEVICES"]
+        else:
+            myStr = self.gpuList.get()
+            os.environ["CUDA_VISIBLE_DEVICES"] = self.gpuList.get()
+        numGPU = myStr.split(',')
+
+        self._insertFunctionStep("convertStep")
+        self._insertFunctionStep("predict", numGPU[0])
+        self._insertFunctionStep('createOutputStep')
+
+    # --------------------------- STEPS functions ---------------------------------------------------
+    def convertStep(self):
+        writeSetOfParticles(self.inputImageSet.get(), self.predictImgsFn)
+        self.runJob("xmipp_image_resize",
+                    "-i %s -o %s --save_metadata_stack %s --fourier %d" %
+                    (self.predictImgsFn,
+                     self._getExtraPath('trainingResized.stk'),
+                     self._getExtraPath('trainingResized.xmd'),
+                     self.Xdim), numberOfMpi=self.numberOfThreads.get() * self.numberOfMpi.get())
+
+    def predict(self, gpuId):
+        self.modelAng = self.inputModel.get()
+        args = "%s %s %s %s %s %d %d %d" % (
+            self._getExtraPath("trainingResized.xmd"), gpuId, self._getPath(), self.predictImgsFn,
+            self.modelAng._getExtraPath("modelAngular"), self.numAngModels.get(), self.tolerance.get(), self.maxModels.get())
+
+        self.runJob("xmipp_deep_global_assignment_predict", args, numberOfMpi=1, env=self.getCondaEnv())
+
+        remove(self._getExtraPath("trainingResized.xmd"))
+        remove(self._getExtraPath("trainingResized.stk"))
+
+    def createOutputStep(self):
+        imgFname = self._getPath('predict_results.xmd')
+        outputSet = self._createSetOfParticles()
+
+        readSetOfParticles(imgFname, outputSet)
+        outputSet.copyInfo(self.inputImageSet.get())
+        outputSet.setAlignmentProj()
+
+        self._defineOutputs(outputParticles=outputSet)
+        self._store(outputSet)
+        self._defineSourceRelation(self.inputImageSet.get(), outputSet)
+
+    # --------------------------- INFO functions --------------------------------
+    def _methods(self):
+        methods = []
+        if hasattr(self, 'outputParticles'):
+            methods.append("We learned a model to center particles from %i input images (%s)." \
+                           % (self.inputSet.get().getSize(), self.getObjectTag('inputSet')))
+        return methods
diff --git a/xmipp3/protocols/protocol_deep_hand.py b/xmipp3/protocols/protocol_deep_hand.py
index b784a779a..8acc9514a 100644
--- a/xmipp3/protocols/protocol_deep_hand.py
+++ b/xmipp3/protocols/protocol_deep_hand.py
@@ -39,7 +39,7 @@ class XmippProtDeepHand(EMProtocol, XmippProtocol):
     """
 
     _label ="deep hand"
-    _conda_env = "xmipp_deepHand"
+    _conda_env = "xmipp_pyTorch"
 
     def __init__(self, *args, **kwargs):
         EMProtocol.__init__(self, *args, **kwargs)
diff --git a/xmipp3/protocols/protocol_deep_micrograph_screen.py b/xmipp3/protocols/protocol_deep_micrograph_screen.py
index 601b710ba..5e4c00838 100644
--- a/xmipp3/protocols/protocol_deep_micrograph_screen.py
+++ b/xmipp3/protocols/protocol_deep_micrograph_screen.py
@@ -215,6 +215,7 @@ def _computeMaskForMicrographList(self, micList):
           else:
               args += ' -g -1'
 
+          os.environ['TF_FORCE_GPU_ALLOW_GROWTH'] = 'true'
           self.runJob('xmipp_deep_micrograph_cleaner', args)
 
 
@@ -282,7 +283,6 @@ def _getScale(self):
       return scale
 
     def _updateOutputCoordSet(self, micList, streamMode):
-
         # Do no proceed if there is not micrograph ready
         if not micList:
             return []
@@ -296,15 +296,13 @@ def _updateOutputCoordSet(self, micList, streamMode):
         firstTime = outputCoords is None
 
         if firstTime:
-            if self.micsSource==SAME_AS_PICKING or self.useOtherScale.get()==1:
+            if self.useOtherScale.get() == 1:
               boxSize = self.getBoxSize()
-              micSetPtr = self.getInputMicrographs()
             else:
               boxSize = self.inputCoordinates.get().getBoxSize()
-              micSetPtr = self.inputCoordinates.get().getMicrographs()
-              
-            outputCoords = self._createSetOfCoordinates(micSetPtr,
-                                                        suffix=self.getAutoSuffix())
+
+            micSetPtr = self.getInputMicrographsPointer()
+            outputCoords = self._createSetOfCoordinates(micSetPtr, suffix=self.getAutoSuffix())
             outputCoords.copyInfo(self.inputCoordinates.get())
             outputCoords.setBoxSize(boxSize)
         else:
@@ -320,25 +318,27 @@ def _updateOutputCoordSet(self, micList, streamMode):
 
         return micList
 
-
     #--------------------------- INFO functions --------------------------------
-
     def _getStreamingBatchSize(self):
-      self.firstBatch=True
-      if self.streamingBatchSize.get()==-1:
+      self.firstBatch = True
+      if self.streamingBatchSize.get() == -1:
         if not hasattr(self, "actualBatchSize"):
           if self.isInStreaming():
-            self.actualBatchSize= 16
+            self.actualBatchSize = 16
+            batchSize = self.actualBatchSize
           else:
             if self.firstBatch:
-              self.firstBatch=False
-              batchSize= 4
+              self.firstBatch = False
+              batchSize = 4
             else:
               nPickMics = self._getNumPickedMics()
-              self.actualBatchSize= min(50, nPickMics)
-              batchSize= self.actualBatchSize
+              self.actualBatchSize = min(50, nPickMics)
+              batchSize = self.actualBatchSize
+        else:
+            batchSize = self.actualBatchSize
       else:
-        batchSize= self.streamingBatchSize.get()
+        batchSize = self.streamingBatchSize.get()
+
       return batchSize
 
     def _getNumPickedMics(self):
@@ -422,6 +422,16 @@ def getInputMicrographs(self):
         else:
             return self.inputMicrographs.get()
 
+    def getInputMicrographsPointer(self):
+        """ Return the micrographs pointer associated to the SetOfCoordinates or
+        Other micrographs. """
+        if not self._micsOther():
+            inMicsPointer = Pointer(self.getMapper().getParent(self.inputCoordinates.get().getMicrographs()),
+                                    extended='outputMicrographs')
+            return inMicsPointer
+        else:
+            return self.inputMicrographs
+
     def getCoords(self):
         return self.inputCoordinates.get()
 
@@ -485,12 +495,9 @@ def registerCoords(self, coordsDir):
         and it is used from the Java picking GUI to register
         a new SetOfCoordinates when the user click on +Particles button.
         """
-
-        inputset = self.getInputMicrographs()
-        
+        inputset = self.getInputMicrographsPointer()
         mySuffix = '_Manual_%s' % coordsDir.split('manualThresholding_')[1]
         outputName = 'outputCoordinates' + mySuffix
-
         outputset = self._createSetOfCoordinates(inputset, suffix=mySuffix)
         readSetOfCoordinates(coordsDir, outputset.getMicrographs(), outputset)
         # summary = self.getSummary(outputset)
@@ -501,6 +508,6 @@ def registerCoords(self, coordsDir):
         # Using a pointer to define the relations is more robust to scheduling
         # and id changes between the protocol run.db and the main project
         # database. The pointer defined below points to the outputset object
-        self._defineSourceRelation(self.getInputMicrographs(),
+        self._defineSourceRelation(inputset,
                                    Pointer(value=self, extended=outputName))
         self._store()
diff --git a/xmipp3/protocols/protocol_flexalign.py b/xmipp3/protocols/protocol_flexalign.py
index f32fc3405..253833c55 100644
--- a/xmipp3/protocols/protocol_flexalign.py
+++ b/xmipp3/protocols/protocol_flexalign.py
@@ -54,7 +54,7 @@ class XmippProtFlexAlign(ProtAlignMovies):
     """
     NO_ROTATION = 0
     NO_FLIP = 0
-    _devStatus = UPDATED
+    _devStatus = PROD
     _label = 'FlexAlign'
     _lastUpdateVersion = VERSION_1_1
 
diff --git a/xmipp3/protocols/protocol_local_ctf.py b/xmipp3/protocols/protocol_local_ctf.py
index 4780a7353..49298eb6e 100644
--- a/xmipp3/protocols/protocol_local_ctf.py
+++ b/xmipp3/protocols/protocol_local_ctf.py
@@ -65,7 +65,7 @@ def _defineParams(self, form):
         form.addParam('maxGrayShiftChange', FloatParam, label="Maximum gray shift change", default=1,
                       expertLevel=LEVEL_ADVANCED, help="The reprojection is modified as a*P+b, b is restricted to the "
                                                        "interval [-maxGrayShift,maxGrayShift]")
-        form.addParam('sameDefocus', BooleanParam, label="Force defocusV to be equal than defocusU", default=True,
+        form.addParam('sameDefocus', BooleanParam, label="Force defocusV to be equal than defocusU", default=False,
                       expertLevel=LEVEL_ADVANCED,
                       help="As the CTF usually suffers from astigmatism (it is not spherical but ellipsoidal), the "
                            "defocus vary if computed in X or Y direction, being defocus U value the defocus in X "
diff --git a/xmipp3/protocols/protocol_movie_dose_analysis.py b/xmipp3/protocols/protocol_movie_dose_analysis.py
new file mode 100644
index 000000000..3a1b3b1ff
--- /dev/null
+++ b/xmipp3/protocols/protocol_movie_dose_analysis.py
@@ -0,0 +1,410 @@
+# **************************************************************************
+# *
+# * Authors:     Carlos Oscar S. Sorzano (coss@cnb.csic.es)
+# *              Daniel Marchán Torres (da.marchan@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+import matplotlib.pyplot as plt
+import numpy as np
+import os
+from pyworkflow import VERSION_3_0, NEW
+from pyworkflow.object import Set
+from pyworkflow.protocol import STEPS_PARALLEL
+from pyworkflow.protocol.params import (PointerParam, IntParam, FloatParam, LEVEL_ADVANCED)
+from pyworkflow.utils.properties import Message
+import pyworkflow.protocol.constants as cons
+from pwem.emlib.image import ImageHandler
+from pwem.objects import SetOfMovies
+from pwem.protocols import ProtProcessMovies
+from xmipp3.convert import getScipionObj
+import statistics as stat
+from pyworkflow import BETA, UPDATED, NEW, PROD
+
+THRESHOLD = 2
+OUTPUT_ACCEPTED = 'outputMovies'
+OUTPUT_DISCARDED = 'outputMoviesDiscarded'
+
+class XmippProtMovieDoseAnalysis(ProtProcessMovies):
+    """ Protocol for the dose analysis """
+    # FIXME: WITH .mrcs IT DOES NOT FILL THE LABELS
+    _devStatus = NEW
+
+    _label = 'movie dose analysis'
+    _lastUpdateVersion = VERSION_3_0
+    _devStatus = NEW
+    _possibleOutputs = {
+        OUTPUT_ACCEPTED: SetOfMovies,
+        OUTPUT_DISCARDED: SetOfMovies
+    }
+
+    finished = False
+    stats = {}
+    estimatedIds = []
+    meanDoseList = []
+    medianDoseTemporal = []
+    medianDifferences = []
+    meanGlobal = 0
+    usingExperimental = False
+
+    def __init__(self, **args):
+        ProtProcessMovies.__init__(self, **args)
+
+    # -------------------------- DEFINE param functions ----------------------
+    def _defineParams(self, form):
+        form.addSection(label=Message.LABEL_INPUT)
+
+        form.addParam('inputMovies', PointerParam, pointerClass='SetOfMovies',
+                      label=Message.LABEL_INPUT_MOVS,
+                      help='Select one or several movies. A dose analysis '
+                           'be calculated for each one of them.')
+        form.addParam('percentage_threshold', FloatParam, default=5,
+                      label="Maximum percentage difference (%)",
+                      help='By default, a difference of 5% against the median dose is used to '
+                           'assume that the dose has an incorrect value.')
+        form.addParam('n_samples', IntParam, default=20,
+                      label="Samples to estimate the median dose", expertLevel=LEVEL_ADVANCED,
+                      help='By default, 20 movies are used to '
+                           'compute the global median.')
+        form.addParam('window', IntParam, default=50,
+                      label="Window step (movies)", expertLevel=LEVEL_ADVANCED,
+                      help='By default, every 50 movies (window=50) we '
+                           'compute the percentage of incorrect dose analysis to check if there '
+                           'is any anomally in the dose.')
+        form.addParam('percentage_window', FloatParam, default=30,
+                      label="Windows maximum faulty percentage (%)", expertLevel=LEVEL_ADVANCED,
+                      help='By default, if 30% of the movies are discarded'
+                           'it assume that the dose has an incorrect value that endures in time.')
+
+    # -------------------------- STEPS functions ------------------------------
+    def createOutputStep(self):
+        self._closeOutputSet()
+
+    def _insertAllSteps(self):
+        # Build the list of all processMovieStep ids by
+        # inserting each of the steps for each movie
+        self.insertedDict = {}
+        self.samplingRate = self.inputMovies.get().getSamplingRate()
+        # Initial steps
+        self.initializeParams()
+        # Gain and Dark conversion step
+        self.convertCIStep = []
+        convertStepId = self._insertFunctionStep('_convertInputStep',
+                                                 prerequisites=[])
+        self.convertCIStep.append(convertStepId)
+
+        self._insertFunctionStep('createOutputStep',
+                                 prerequisites=[], wait=True)
+
+    def initializeParams(self):
+        self.framesRange = self.inputMovies.get().getFramesRange()
+        self.dims = self.inputMovies.get().getFirstItem().getDim()
+        self.pixelSize =  self.inputMovies.get().getFirstItem().getSamplingRate()
+        dosePerFrame = self.inputMovies.get().getFirstItem().getAcquisition().getDosePerFrame()
+        if dosePerFrame != 0 and dosePerFrame != None:
+            self.dosePerFrame = dosePerFrame
+        else:
+            self.usingExperimental = True
+
+    def _processMovie(self, movie):
+        movieId = movie.getObjId()
+        self.estimatedIds.append(movieId)
+        stats = self.estimatePoissonCount(movie)
+        self.stats[movieId] = stats
+
+        fnMonitorSummary = self._getPath("summaryForMonitor.txt")
+        if not os.path.exists(fnMonitorSummary):
+            fhMonitorSummary = open(fnMonitorSummary, "w")
+        else:
+            fhMonitorSummary = open(fnMonitorSummary, "a")
+
+        fhMonitorSummary.write("movie_%06d_poisson_count: mean=%f stdev=%f [min=%f,max=%f]\n" %
+                               (movieId, stats['mean'], stats['std'], stats['min'], stats['max']))
+        fhMonitorSummary.close()
+
+
+    def estimatePoissonCount(self, movie):
+        mean_frames = []
+        n = movie.getNumberOfFrames()
+        frames = [1, n/2, n]
+
+        for frame in frames:
+            frame_image = ImageHandler().read("%d@%s" % (frame, movie.getFileName())).getData()
+            mean_dose_per_pixel = np.mean(frame_image)
+            mean_dose_per_angstrom2 = mean_dose_per_pixel/ self.samplingRate**2
+            mean_frames.append(mean_dose_per_angstrom2)
+
+        stats = computeStats(np.asarray(mean_frames))
+        self.meanDoseList.append(stats['mean'])
+
+        return stats
+
+    def _loadOutputSet(self, SetClass, baseName):
+        """
+        Load the output set if it exists or create a new one.
+        fixSampling: correct the output sampling rate if binning was used,
+        except for the case when the original movies are kept and shifts
+        refers to that one.
+        """
+        setFile = self._getPath(baseName)
+        if os.path.exists(setFile) and os.path.getsize(setFile) > 0:
+            outputSet = SetClass(filename=setFile)
+            outputSet.loadAllProperties()
+            outputSet.enableAppend()
+        else:
+            outputSet = SetClass(filename=setFile)
+            outputSet.setStreamState(outputSet.STREAM_OPEN)
+            inputMovies = self.inputMovies.get()
+            outputSet.copyInfo(inputMovies)
+
+        return outputSet
+
+    def _checkNewOutput(self):
+        if getattr(self, 'finished', False):
+            return
+
+        # If continue then we need to define again some parameters
+        if self.isContinued():
+            self.initializeParams()
+
+        if len(self.meanDoseList) >= self.n_samples.get() and not hasattr(self, 'mu'):
+            medianDoseExperimental = np.median(self.meanDoseList)
+            if hasattr(self, 'dosePerFrame'):
+                refDose = self.dosePerFrame
+                diff = abs((medianDoseExperimental/refDose)-1) * 100
+                if diff < THRESHOLD:
+                   self.mu = refDose
+                else:
+                    self.mu = medianDoseExperimental
+                    self.usingExperimental = True
+                    self.info("The given dose per frame %f does not match with the experimental one %f"
+                              " having a difference of %f percent. "
+                              "Therefore, using the experimental as global median."
+                              %(refDose, medianDoseExperimental, diff))
+            else:
+                self.mu = medianDoseExperimental
+
+        if hasattr(self, 'mu'):
+            # Load previously done items (from text file)
+            doneList = self._readDoneList()
+            # Check for newly done items
+            newDone = [m for m in self.listOfMovies
+                       if m.getObjId() not in doneList and self._isMovieDone(m)]
+            # Update the file with the newly done movies
+            # or exit from the function if no new done movies
+            self.debug('_checkNewOutput: ')
+            self.debug('   listOfMovies: %s, doneList: %s, newDone: %s'
+                       % (len(self.listOfMovies), len(doneList), len(newDone)))
+
+            allDone = len(doneList) + len(newDone)
+            # We have finished when there is no more input movies
+            # (stream closed) and the number of processed movies is
+            # equal to the number of inputs
+            self.finished = self.streamClosed and allDone == len(self.listOfMovies)
+            streamMode = Set.STREAM_CLOSED if self.finished \
+                else Set.STREAM_OPEN
+
+            if newDone:
+                self._writeDoneList(newDone)
+            elif not self.finished:
+                # If we are not finished and no new output have been produced
+                # it does not make sense to proceed and updated the outputs
+                # so we exit from the function here
+                return
+
+            self.debug('   finished: %s ' % self.finished)
+            self.debug('        self.streamClosed (%s) AND' % self.streamClosed)
+            self.debug('        allDone (%s) == len(self.listOfMovies (%s)'
+                       % (allDone, len(self.listOfMovies)))
+            self.debug('   streamMode: %s' % streamMode)
+            # Find the acceptance intervals
+            lower, upper = self.getLimitIntervals()
+            self.info(f"Acceptance interval: ({lower:.4f}, {upper:.4f})")
+            self.info(f"Global median: ({self.mu:.4f})")
+
+            acceptedMovies = []
+            discardedMovies = []
+
+            for movie in newDone:
+                newMovie = movie.clone()
+                newMovie.setFramesRange(self.framesRange)
+                movieId = newMovie.getObjId()
+                stats = self.stats[movieId]
+                mean = stats['mean']
+                std = stats['std']
+                minDose = stats['min']
+                maxDose = stats['max']
+                diff_median = ((mean/self.mu)-1)*100
+
+                setAttribute(newMovie, '_DIFF_TO_DOSE_PER_ANGSTROM2', abs(diff_median))
+                setAttribute(newMovie, '_MEAN_DOSE_PER_ANGSTROM2', mean)
+                setAttribute(newMovie, '_STD_DOSE_PER_ANGSTROM2', std)
+                setAttribute(newMovie, '_MIN_DOSE_PER_FRAME', minDose)
+                setAttribute(newMovie, '_MAX_DOSE_PER_FRAME', maxDose)
+
+                self.medianDifferences.append(diff_median)
+                self.medianDoseTemporal.append(mean)
+                self.info('Movie with id %d has a mean dose per frame of %f and a diff of %f percent'
+                          %(movie.getObjId(), mean, diff_median))
+
+                if lower <= mean <= upper:
+                    self.info('accepted')
+                    acceptedMovies.append(newMovie)
+                else:
+                    self.info('discarded')
+                    discardedMovies.append(newMovie)
+
+                if len(self.medianDifferences) % self.window.get() == 0:
+                    if self.usingExperimental:
+                        # Update the median global
+                        self.mu = np.median(self.meanDoseList)
+                        self.info('Updating median global to %f' %self.mu)
+
+                    windowList = self.medianDoseTemporal[-self.window.get():]
+                    percentage = (1 - (len([dose for dose in windowList
+                                      if lower < dose < upper]) / len(windowList)))*100
+                    self.info('The faulty percentage of this window is %f' %percentage)
+
+                    if percentage > self.percentage_window.get():
+                        with open(self._getExtraPath('WARNING.TXT'), 'a') as f:
+                            self.info('Percentage of wrong dose in a window surpass the threshold: {}% > {}%'
+                                      .format(percentage, self.percentage_window.get()))
+                            f.write('Percentage of wrong dose in a window surpass the threshold: {}% > {}% \n'
+                                    .format(percentage, self.percentage_window.get()))
+                            f.close()
+
+            if len(acceptedMovies)>0:
+                moviesSet = self._loadOutputSet(SetOfMovies, 'movies.sqlite')
+                for movie in acceptedMovies:
+                    moviesSet.append(movie)
+                self._updateOutputSet(OUTPUT_ACCEPTED, moviesSet, streamMode)
+            if len(discardedMovies)>0:
+                moviesSetDiscarded = self._loadOutputSet(SetOfMovies, 'movies_discarded.sqlite')
+                for movie in discardedMovies:
+                    moviesSetDiscarded.append(movie)
+                self._updateOutputSet(OUTPUT_DISCARDED, moviesSetDiscarded, streamMode)
+
+            plotDoseAnalysis(self.getDosePlot(), self.meanDoseList, self.mu, lower, upper)
+            plotDoseAnalysisDiff(self.getDoseDiffPlot(), self.medianDifferences)
+
+        if self.finished:  # Unlock createOutputStep if finished all jobs
+            outputStep = self._getFirstJoinStep()
+            if outputStep and outputStep.isWaiting():
+                outputStep.setStatus(cons.STATUS_NEW)
+
+    def _updateOutputSet(self, outputName, outputSet, state=Set.STREAM_OPEN):
+        outputSet.setStreamState(state)
+        if self.hasAttribute(outputName):
+            outputSet.write()  # Write to commit changes
+            outputAttr = getattr(self, outputName)
+            # Copy the properties to the object contained in the protocol
+            outputAttr.copy(outputSet, copyId=False)
+            # Persist changes
+            self._store(outputAttr)
+        else:
+            # Here the defineOutputs function will call the write() method
+            self._defineOutputs(**{outputName: outputSet})
+            self._store(outputSet)
+
+        # Close set databaset to avoid locking it
+        outputSet.close()
+
+
+# ------------------------- UTILS functions --------------------------------
+    def getLimitIntervals(self):
+        """ Funtion to obtain the acceptance interval limits."""
+        lower = self.mu - self.mu * (self.percentage_threshold.get()/100)
+        upper = self.mu + self.mu * (self.percentage_threshold.get()/100)
+
+        return lower, upper
+
+    def getDosePlot(self):
+        return self._getExtraPath('dose_analysis_plot.png')
+
+    def getDoseDiffPlot(self):
+        return self._getExtraPath('dose_analysis_diff_plot.png')
+
+    # --------------------------- INFO functions -------------------------------
+    def _validate(self):
+        errors = []
+        if errors:
+            errors.append("")
+        return errors
+
+    def _summary(self):
+        fnSummary = self._getPath("summary.txt")
+        if not os.path.exists(fnSummary):
+            summary = ["No summary information yet."]
+        else:
+            fhSummary = open(fnSummary, "r")
+            summary = []
+            for line in fhSummary.readlines():
+                summary.append(line.rstrip())
+            fhSummary.close()
+        return summary
+
+
+# --------------------- WORKERS --------------------------------------
+def setAttribute(obj, label, value):
+    if value is None:
+        return
+    setattr(obj, label, getScipionObj(value))
+
+def computeStats(mean_frames):
+    mean = np.mean(mean_frames)
+    std = np.std(mean_frames)
+    max_dose = np.max(mean_frames)
+    min_dose = np.min(mean_frames)
+
+    stats = {'mean': mean,
+             'std': std,
+             'max': max_dose,
+             'min': min_dose,
+             }
+    return stats
+
+def plotDoseAnalysis(filename, doseValues, medianGlobal, lower, upper):
+    x = np.arange(start=1, stop=len(doseValues)+1, step=1)
+    plt.figure()
+    plt.scatter(x, doseValues,s=10)
+    plt.axhline(y=medianGlobal, color='r', linestyle='-', label='Median dose')
+    plt.axhline(y=upper, color='b', linestyle='-.', label='Upper limit dose')
+    plt.axhline(y=lower, color='g', linestyle='-.', label='Lower limit dose')
+    plt.xlabel("Movies ID")
+    plt.ylabel("Dose (electrons impacts per angstrom**2 )")
+    plt.title('Dose vs time')
+    plt.legend()
+    plt.grid()
+    plt.savefig(filename)
+
+def plotDoseAnalysisDiff(filename, medianDifferences):
+    medianDiff = np.median(medianDifferences)
+    x = np.arange(start=1+1, stop=len(medianDifferences)+2, step=1)
+    plt.figure()
+    plt.scatter(x, medianDifferences, s=10)
+    plt.axhline(y=medianDiff, color='r', linestyle='-',  label='Median dose difference')
+    plt.xlabel("Movies ID")
+    plt.ylabel("Dose differences (%)")
+    plt.title('Dose differences with respect to the global median vs time')
+    plt.legend()
+    plt.grid()
+    plt.savefig(filename)
diff --git a/xmipp3/protocols/protocol_movie_gain.py b/xmipp3/protocols/protocol_movie_gain.py
index 1fe0a449b..409fa8971 100644
--- a/xmipp3/protocols/protocol_movie_gain.py
+++ b/xmipp3/protocols/protocol_movie_gain.py
@@ -41,10 +41,15 @@
 
 from pwem.objects import SetOfMovies, Movie, SetOfImages, Image
 from pwem.protocols import EMProtocol, ProtProcessMovies
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 from pwem import emlib
 import xmipp3.utils as xmutils
 
+OUTPUT_ESTIMATED_GAINS = 'estimatedGains'
+OUTPUT_ORIENTED_GAINS = 'orientedGain'
+OUTPUT_RESIDUAL_GAINS = 'residualGains'
+OUTPUT_MOVIES = 'outputMovies'
 
 class XmippProtMovieGain(ProtProcessMovies):
     """ Estimate the gain image of a camera, directly analyzing one of its movies.
@@ -55,10 +60,15 @@ class XmippProtMovieGain(ProtProcessMovies):
     The same criteria is used for assigning the gain to the output movies (external corrected > external > estimated)
     """
     _label = 'movie gain'
+    _devStatus = UPDATED
     _lastUpdateVersion = VERSION_1_1
-
+    _stepsCheckSecs = 60
     estimatedDatabase = 'estGains.sqlite'
     residualDatabase = 'resGains.sqlite'
+    _possibleOutputs = {OUTPUT_ESTIMATED_GAINS: SetOfImages,
+                        OUTPUT_ORIENTED_GAINS: SetOfImages,
+                        OUTPUT_RESIDUAL_GAINS: SetOfImages,
+                        OUTPUT_MOVIES: SetOfMovies}
 
     def __init__(self, **args):
         EMProtocol.__init__(self, **args)
@@ -68,10 +78,9 @@ def __init__(self, **args):
     def _defineParams(self, form):
         form.addSection(label=Message.LABEL_INPUT)
 
-        form.addParam('inputMovies', PointerParam, pointerClass='SetOfMovies, '
-                                                                'Movie',
+        form.addParam('inputMovies', PointerParam, pointerClass='SetOfMovies',
                       label=Message.LABEL_INPUT_MOVS,
-                      help='Select one or several movies. A gain image will '
+                      help='Select several movies. A gain image will '
                            'be calculated for each one of them.')
         form.addParam('estimateGain', BooleanParam, default=True,
                       label="Estimate movies gain",
@@ -101,9 +110,9 @@ def _defineParams(self, form):
                            'frame will be used.')
         form.addParam('movieStep', IntParam, default=250,
                       label="Movie step", expertLevel=LEVEL_ADVANCED,
-                      help='By default, every movie (movieStep=1) is used to '
+                      help='By default, every 250 movies (movieStep=250) is used to '
                            'compute the movie gain. If you set '
-                           'this parameter to 2, 3, ..., then only every 2nd, '
+                           'this parameter to 2, 3, ..., then every 2nd, '
                            '3rd, ... movie will be used.')
 
         # It should be in parallel (>2) in order to be able of attaching
@@ -114,11 +123,11 @@ def _defineParams(self, form):
     def createOutputStep(self):
         if self.estimateGain.get():
             estGainsSet = self._loadOutputSet(SetOfImages, self.estimatedDatabase)
-            self._updateOutputSet('estimatedGains', estGainsSet, Set.STREAM_CLOSED)
+            self._updateOutputSet(OUTPUT_ESTIMATED_GAINS, estGainsSet, Set.STREAM_CLOSED)
 
         if self.estimateResidualGain.get():
             resGainsSet = self._loadOutputSet(SetOfImages, self.residualDatabase)
-            self._updateOutputSet('residualGains', resGainsSet, Set.STREAM_CLOSED)
+            self._updateOutputSet(OUTPUT_RESIDUAL_GAINS, resGainsSet, Set.STREAM_CLOSED)
 
     def _insertNewMoviesSteps(self, insertedDict, inputMovies):
         """ Insert steps to process new movies (from streaming)
@@ -127,36 +136,29 @@ def _insertNewMoviesSteps(self, insertedDict, inputMovies):
             inputMovies: input movies set to be check
         """
         deps = []
-        if isinstance(self.inputMovies.get(), Movie):
-            movie = self.inputMovies.get()
+        if len(insertedDict) == 0 and self.estimateOrientation.get():
+            # Adding a first step to orientate the input gain
+            firstMovie = inputMovies.getFirstItem()
+            movieDict = firstMovie.getObjDict(includeBasic=True)
+            orientStepId = self._insertFunctionStep('estimateOrientationStep',
+                                                    movieDict,
+                                                    prerequisites=self.convertCIStep)
+            # adding orientStep as dependency for all other steps
+            self.convertCIStep.append(orientStepId)
+
+        if len(insertedDict) == 0 and self.normalizeGain.get():
+            # Adding a step to normalize the gain (only one)
+            normStepId = self._insertFunctionStep('normalizeGainStep',
+                                                  prerequisites=self.convertCIStep)
+            # adding normStep as dependency for all other steps
+            self.convertCIStep.append(normStepId)
+        self.estimatedIds, self.estimatedResIds = [], []
+        # For each movie insert the step to process it
+        for movie in inputMovies:
             if movie.getObjId() not in insertedDict:
                 stepId = self._insertMovieStep(movie)
                 deps.append(stepId)
                 insertedDict[movie.getObjId()] = stepId
-        else:
-            if len(insertedDict) == 0 and self.estimateOrientation.get():
-                # Adding a first step to orientate the input gain
-                firstMovie = self.inputMovies.get().getFirstItem()
-                movieDict = firstMovie.getObjDict(includeBasic=True)
-                orientStepId = self._insertFunctionStep('estimateOrientationStep',
-                                                        movieDict,
-                                                        prerequisites=self.convertCIStep)
-                # adding orientStep as dependency for all other steps
-                self.convertCIStep.append(orientStepId)
-
-            if len(insertedDict) == 0 and self.normalizeGain.get():
-                # Adding a step to normalize the gain (only one)
-                normStepId = self._insertFunctionStep('normalizeGainStep',
-                                                      prerequisites=self.convertCIStep)
-                # adding normStep as dependency for all other steps
-                self.convertCIStep.append(normStepId)
-            self.estimatedIds, self.estimatedResIds = [], []
-            # For each movie insert the step to process it
-            for movie in self.inputMovies.get():
-                if movie.getObjId() not in insertedDict:
-                    stepId = self._insertMovieStep(movie)
-                    deps.append(stepId)
-                    insertedDict[movie.getObjId()] = stepId
         return deps
 
     def estimateGainFun(self, movie, noSigma=False, residual=False):
@@ -188,7 +190,7 @@ def estimateOrientationStep(self, movieDict):
 
         orientedSet = self._loadOutputSet(SetOfImages, 'orientedGain.sqlite')
         orientedSet = self.updateGainsOutput(movie, orientedSet, self.getOrientedGainPath())
-        self._updateOutputSet('orientedGain', orientedSet, Set.STREAM_CLOSED)
+        self._updateOutputSet(OUTPUT_ORIENTED_GAINS, orientedSet, Set.STREAM_CLOSED)
 
     def normalizeGainStep(self):
         gainFn = self.getFinalGainPath()
@@ -214,7 +216,7 @@ def _processMovie(self, movie):
                 self.estimateGainFun(movie)
 
         if self.estimateResidualGain.get() and not movieId in self.estimatedResIds:
-            print('\nEstimating residual gain')
+            self.info('\nEstimating residual gain')
             self.estimatedResIds.append(movieId)
             self.estimateGainFun(movie, residual=True)
 
@@ -268,98 +270,75 @@ def _loadOutputSet(self, SetClass, baseName, fixGain=False):
             outputSet = SetClass(filename=setFile)
             outputSet.setStreamState(outputSet.STREAM_OPEN)
 
-            if isinstance(self.inputMovies.get(), SetOfMovies) or isinstance(self.inputMovies.get(), Movie):
-                inputMovies = self.inputMovies.get()
-                outputSet.copyInfo(inputMovies)
+            inputMovies = self.inputMovies.get()
+            outputSet.copyInfo(inputMovies)
 
-                if fixGain:
-                    outputSet.setGain(self.getFinalGainPath(tifFlipped=True))
+            if fixGain:
+                outputSet.setGain(self.getFinalGainPath(tifFlipped=True))
 
         return outputSet
 
     def _checkNewInput(self):
-        if isinstance(self.inputMovies.get(), SetOfMovies):
-            ProtProcessMovies._checkNewInput(self)
+        ProtProcessMovies._checkNewInput(self)
 
     def _checkNewOutput(self):
         if getattr(self, 'finished', False):
             return
-        if isinstance(self.inputMovies.get(), Movie):
-            movie = self.inputMovies.get()
-            movieId = movie.getObjId()
-            streamMode = Set.STREAM_CLOSED
-            saveMovie = self.getAttributeValue('doSaveMovie', False)
-            moviesSet = self._loadOutputSet(SetOfMovies, 'movies.sqlite', fixGain=True)
-            moviesSet.append(movie)
-            self._updateOutputSet('outputMovies', moviesSet, streamMode)
 
+        # Load previously done items (from text file)
+        doneList = self._readDoneList()
+        # Check for newly done items
+        newDone = [m.clone() for m in self.listOfMovies
+                   if int(m.getObjId()) not in doneList and
+                   self._isMovieDone(m)]
+
+        allDone = len(doneList) + len(newDone)
+        # We have finished when there is not more input movies
+        # (stream closed) and the number of processed movies is
+        # equal to the number of inputs
+        self.finished = self.streamClosed and \
+                        allDone == len(self.listOfMovies)
+        streamMode = Set.STREAM_CLOSED if self.finished \
+            else Set.STREAM_OPEN
+
+        if newDone:
+            self._writeDoneList(newDone)
+        elif not self.finished:
+            # If we are not finished and no new output have been produced
+            # it does not make sense to proceed and updated the outputs
+            # so we exit from the function here
+            return
+
+        if any([self.doGainProcess(i.getObjId()) for i in newDone]):
+            # update outputGains if any residualGain is processed in newDone
             if self.estimateGain.get():
                 estGainsSet = self._loadOutputSet(SetOfImages, self.estimatedDatabase)
-                estGainsSet = self.updateGainsOutput(movie, estGainsSet, self.getEstimatedGainPath(movieId))
-                self._updateOutputSet('estimatedGains', estGainsSet, streamMode)
             if self.estimateResidualGain.get():
                 resGainsSet = self._loadOutputSet(SetOfImages, self.residualDatabase)
-                resGainsSet = self.updateGainsOutput(movie, resGainsSet, self.getResidualGainPath(movieId))
-                self._updateOutputSet('residualGains', resGainsSet, streamMode)
 
-            outputStep = self._getFirstJoinStep()
-            outputStep.setStatus(cons.STATUS_NEW)
-            self.finished = True
-        else:
-            # Load previously done items (from text file)
-            doneList = self._readDoneList()
-            # Check for newly done items
-            newDone = [m.clone() for m in self.listOfMovies
-                       if int(m.getObjId()) not in doneList and
-                       self._isMovieDone(m)]
-
-            allDone = len(doneList) + len(newDone)
-            # We have finished when there is not more input movies
-            # (stream closed) and the number of processed movies is
-            # equal to the number of inputs
-            self.finished = self.streamClosed and \
-                            allDone == len(self.listOfMovies)
-            streamMode = Set.STREAM_CLOSED if self.finished \
-                else Set.STREAM_OPEN
-
-            if newDone:
-                self._writeDoneList(newDone)
-            elif not self.finished:
-                # If we are not finished and no new output have been produced
-                # it does not make sense to proceed and updated the outputs
-                # so we exit from the function here
-                return
-
-            if any([self.doGainProcess(i.getObjId()) for i in newDone]):
-                # update outputGains if any residualGain is processed in newDone
+            for movie in newDone:
+                movieId = movie.getObjId()
+                if not self.doGainProcess(movieId):
+                    continue
                 if self.estimateGain.get():
-                    estGainsSet = self._loadOutputSet(SetOfImages, self.estimatedDatabase)
+                    estGainsSet = self.updateGainsOutput(movie, estGainsSet, self.getEstimatedGainPath(movieId))
                 if self.estimateResidualGain.get():
-                    resGainsSet = self._loadOutputSet(SetOfImages, self.residualDatabase)
+                    resGainsSet = self.updateGainsOutput(movie, resGainsSet, self.getResidualGainPath(movieId))
 
-                for movie in newDone:
-                    movieId = movie.getObjId()
-                    if not self.doGainProcess(movieId):
-                        continue
-                    if self.estimateGain.get():
-                        estGainsSet = self.updateGainsOutput(movie, estGainsSet, self.getEstimatedGainPath(movieId))
-                    if self.estimateResidualGain.get():
-                        resGainsSet = self.updateGainsOutput(movie, resGainsSet, self.getResidualGainPath(movieId))
-
-                if self.estimateGain.get():
-                    self._updateOutputSet('estimatedGains', estGainsSet, streamMode)
-                if self.estimateResidualGain.get():
-                    self._updateOutputSet('residualGains', resGainsSet, streamMode)
+            if self.estimateGain.get():
+                self._updateOutputSet(OUTPUT_ESTIMATED_GAINS, estGainsSet, streamMode)
+            if self.estimateResidualGain.get():
+                self._updateOutputSet(OUTPUT_RESIDUAL_GAINS, resGainsSet, streamMode)
 
-            moviesSet = self._loadOutputSet(SetOfMovies, 'movies.sqlite', fixGain=True)
-            for movie in newDone:
-                moviesSet.append(movie)
-            self._updateOutputSet('outputMovies', moviesSet, streamMode)
+        moviesSet = self._loadOutputSet(SetOfMovies, 'movies.sqlite', fixGain=True)
+        for movie in newDone:
+            moviesSet.append(movie)
+        self._updateOutputSet(OUTPUT_MOVIES, moviesSet, streamMode)
 
-            if self.finished:  # Unlock createOutputStep if finished all jobs
-                outputStep = self._getFirstJoinStep()
-                if outputStep and outputStep.isWaiting():
-                    outputStep.setStatus(cons.STATUS_NEW)
+        if self.finished:  # Unlock createOutputStep if finished all jobs
+            outputStep = self._getFirstJoinStep()
+            if outputStep and outputStep.isWaiting():
+                outputStep.setStatus(cons.STATUS_NEW)
 
     def updateGainsOutput(self, movie, imgSet, imageFile):
         movieId = movie.getObjId()
@@ -395,7 +374,7 @@ def match_orientation(self, exp_gain, est_gain):
             with respect to the estimated
             Input: 2 Xmipp Images
         '''
-        print('\nEstimating best orientation')
+        self.info('\nEstimating best orientation')
         sys.stdout.flush()
         best_cor = 0
         #Building conjugate of FT of estimated gain for correlations
@@ -430,20 +409,26 @@ def match_orientation(self, exp_gain, est_gain):
                     best_cor = minVal
                     best_transf = (angle,imir)
                     best_R = R
-                    T = np.asarray([[1, 0, np.asscalar(corLoc[1])], [0, 1, np.asscalar(corLoc[0])], [0, 0, 1]])
+                    # T = np.asarray([[1, 0, np.asscalar(corLoc[1])], [0, 1, np.asscalar(corLoc[0])], [0, 0, 1]])
+                    T = np.asarray([[1, 0, corLoc[1].item()],
+                                    [0, 1, corLoc[0].item()],
+                                    [0, 0, 1]])
                 if abs(maxVal) > abs(best_cor):
                     corLoc = translation_correction(maxLoc, est_gain_array.shape)
                     best_cor = maxVal
                     best_transf = (angle, imir)
                     best_R = R
-                    T = np.asarray([[1, 0, np.asscalar(corLoc[1])], [0, 1, np.asscalar(corLoc[0])], [0, 0, 1]])
+                    # T = np.asarray([[1, 0, np.asscalar(corLoc[1])], [0, 1, np.asscalar(corLoc[0])], [0, 0, 1]])
+                    T = np.asarray([[1, 0, corLoc[1].item()], 
+                                    [0, 1, corLoc[0].item()],
+                                    [0, 0, 1]])
 
         # Multiply by inverse of translation matrix
         best_M = np.matmul(np.linalg.inv(T), best_R)
         best_gain_array = xmutils.applyTransform(np.asarray(exp_gain.getData(), dtype=np.float64), best_M, est_gain_array.shape)
 
-        print('Best correlation: ', best_cor)
-        print('Rotation angle: {}\nHorizontal mirror: {}'.format(best_transf[0],best_transf[1]==1))
+        self.info('Best correlation: %f' %best_cor)
+        self.info('Rotation angle: {}\nHorizontal mirror: {}'.format(best_transf[0],best_transf[1]==1))
 
         inv_best_gain_array = invert_array(best_gain_array)
         if best_cor > 0:
diff --git a/xmipp3/protocols/protocol_particle_pick_automatic.py b/xmipp3/protocols/protocol_particle_pick_automatic.py
index 8ea3cc397..adffc6757 100644
--- a/xmipp3/protocols/protocol_particle_pick_automatic.py
+++ b/xmipp3/protocols/protocol_particle_pick_automatic.py
@@ -230,10 +230,7 @@ def getInputMicrographsPointer(self):
         # Get micrographs to pick
         if self.micsToPick == MICS_SAMEASPICKING:
             inputPicking = self.xmippParticlePicking.get()
-            if inputPicking is None:
-                return None
-            else:
-                return inputPicking.inputMicrographs
+            return inputPicking.inputMicrographs if inputPicking else None
         else:
             return self.inputMicrographs
         
@@ -243,7 +240,4 @@ def getInputMicrographs(self):
         a new set of micrographs with the same properties than a previous
         trained ones. )
         """ 
-        if self.getInputMicrographsPointer() is not None:
-            return self.getInputMicrographsPointer().get()
-        else:
-            return None
+        return self.getInputMicrographsPointer().get() if self.getInputMicrographsPointer() else None
diff --git a/xmipp3/protocols/protocol_particle_pick_consensus.py b/xmipp3/protocols/protocol_particle_pick_consensus.py
index 342beffd9..d913bb308 100644
--- a/xmipp3/protocols/protocol_particle_pick_consensus.py
+++ b/xmipp3/protocols/protocol_particle_pick_consensus.py
@@ -29,7 +29,7 @@
 """
 
 import os
-
+import enum
 from math import sqrt
 import numpy as np
 
@@ -44,6 +44,13 @@
 PICK_MODE_LARGER = 0
 PICK_MODE_EQUAL = 1
 
+
+class ProtPickingConsensusOutput(enum.Enum):
+    """ Possible outputs for particle picking protocols
+    """
+    consensusCoordinates = SetOfCoordinates
+
+
 class XmippProtConsensusPicking(ProtParticlePicking):
     """
     Protocol to estimate the agreement between different particle picking
@@ -65,7 +72,8 @@ class XmippProtConsensusPicking(ProtParticlePicking):
     """
 
     _label = 'picking consensus'
-    outputName = 'consensusCoordinates'
+    _possibleOutputs = ProtPickingConsensusOutput
+    outputName = ProtPickingConsensusOutput.consensusCoordinates.name
     FN_PREFIX = 'consensusCoords_'
 
     def __init__(self, **args):
diff --git a/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py b/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
index 34a732365..6981a91fb 100644
--- a/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
+++ b/xmipp3/protocols/protocol_preprocess/protocol_create_mask3d.py
@@ -208,6 +208,8 @@ def _insertAllSteps(self):
     def createMaskFromVolumeStep(self):
         volume = self.inputVolume.get()
         fnVol = getImageLocation(volume)
+        if fnVol.endswith(".mrc"):
+            fnVol += ":mrc"
         Ts = volume.getSamplingRate()
         
         if self.volumeOperation == OPERATION_THRESHOLD:
@@ -392,7 +394,11 @@ def _methods(self):
             messages.append("And, we smoothed it (sigma=%f voxels)."
                             % self.sigmaConvolution.get())
         if self.hasAttribute('outputMask'):
-            messages.append('We refer to the output mask as %s.'
-                            % self.outputMask.getNameId())
+            messages.append('We refer to the output mask as %s.'  % self.outputMask.getNameId())
         return messages
     
+    def _validate(self):
+        errors = []
+        if self.source == SOURCE_VOLUME and not self.inputVolume.get():
+            errors.append("You need to select an input volume")
+            return errors
diff --git a/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py b/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
index 2eb92f3d1..8b6fdcb40 100644
--- a/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
+++ b/xmipp3/protocols/protocol_preprocess/protocol_crop_resize.py
@@ -26,20 +26,24 @@
 # *  e-mail address 'scipion@cnb.csic.es'
 # *
 # **************************************************************************
+import os
 
 import pyworkflow.protocol.constants as const
+from pyworkflow.object import String
 from pwem.convert.headers import setMRCSamplingRate
 from pyworkflow.protocol.params import (BooleanParam, EnumParam, FloatParam,
                                         IntParam)
-from pwem.objects import Volume
+from pwem.objects import Volume, SetOfParticles, Mask
 
 from .protocol_process import XmippProcessParticles, XmippProcessVolumes
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 
 class XmippResizeHelper:
     """ Common features to change dimensions of either SetOfParticles,
     Volume or SetOfVolumes objects.
     """
+    _devStatus = UPDATED
 
     RESIZE_SAMPLINGRATE = 0
     RESIZE_DIMENSIONS = 1
@@ -48,9 +52,9 @@ class XmippResizeHelper:
 
     WINDOW_OP_CROP = 0
     WINDOW_OP_WINDOW = 1
-
-    #--------------------------- DEFINE param functions --------------------------------------------
-    @classmethod
+    
+    #--------------------------- DEFINE param functions --------------------------------------------       
+    @classmethod   
     def _defineProcessParams(cls, protocol, form):
         # Resize operation
         form.addParam('doResize', BooleanParam, default=False,
@@ -242,13 +246,28 @@ def _getSampling(imgSet):
 
 class XmippProtCropResizeParticles(XmippProcessParticles):
     """ Crop or resize a set of particles """
+    # Protocol constants
+    OUTPUT_PARTICLES_NAME = 'outputParticles'
+    OUTPUT_MASK_NAME = 'outputMask'
+
     _label = 'crop/resize particles'
     _inputLabel = 'particles'
+    _possibleOutputs = {OUTPUT_PARTICLES_NAME: SetOfParticles, OUTPUT_MASK_NAME: Mask}
     
     def __init__(self, **kwargs):
         XmippProcessParticles.__init__(self, **kwargs)
     
     #--------------------------- DEFINE param functions --------------------------------------------
+    def _defineParams(self, form):
+        # Creating super form
+        super()._defineParams(form)
+       
+        # Obtaining input particles param to accept also a mask
+        inputParticles = form.getParam('inputParticles')
+        inputParticles.pointerClass = String(str(inputParticles.pointerClass) + ',Mask')
+        inputParticles.label = String(str(inputParticles.label) + '/Mask')
+        inputParticles.help = String('Input particles or 2D Mask to be cropped/resized.')
+
     def _defineProcessParams(self, form):
         XmippResizeHelper._defineProcessParams(self, form)
         form.addParallelSection(threads=0, mpi=8)
@@ -265,12 +284,40 @@ def resizeStep(self, isFirstStep, args):
     
     def windowStep(self, isFirstStep, args):
         XmippResizeHelper.windowStep(self, self._ioArgs(isFirstStep)+args)
+    
+    def convertInputStep(self):
+        """ convert if necessary"""
+        if self.isMask():
+            # If input is a Mask, modify filter params
+            self.inputFn = self.inputParticles.get().getFileName()
+            inputName = os.path.splitext(os.path.basename(self.inputFn))[0]
+
+            # Set output mask path
+            self.outputStk = self._getExtraPath(os.path.basename(inputName + '.mrc'))
+            self.outputMd = self._getTmpPath('tmp.xmd')
+        else:
+            # If input is not Mask, keep default behaviour
+            super().convertInputStep()
+    
+    def createOutputStep(self):
+        if self.isMask():
+            # If input is a Mask, create output Mask
+            outputMask = Mask(self.outputStk)
+            outputMask.copyInfo(self.inputParticles.get())
+            self._preprocessOutput(outputMask)
+            self._defineOutputs(**{self.OUTPUT_MASK_NAME: outputMask})
+            self._defineTransformRelation(self.inputParticles.get(), outputMask)
+        else:
+            # If input is not Mask, keep default behaviour
+            super().createOutputStep()
         
     def _preprocessOutput(self, output):
-        """ We need to update the sampling rate of the 
+        """
+        We need to update the sampling rate of the 
         particles if the Resize option was used.
         """
-        self.inputHasAlign = self.inputParticles.get().hasAlignment()
+        if not self.isMask():
+            self.inputHasAlign = self.inputParticles.get().hasAlignment()
         
         if self.doResize:
             output.setSamplingRate(self.samplingRate)
@@ -338,9 +385,22 @@ def _methods(self):
         return [str]
 
     def _validate(self):
-        return XmippResizeHelper._validate(self)
+        """ This function validates the input parameters so only allowed operations take place. """
+        # Getting default errors
+        errors = XmippResizeHelper._validate(self)
+
+        # Checking if at least one of the operations has been selected
+        if not self.doResize and not self.doWindow.get():
+            errors.append('At least one of the possible operations needs to be selected.')
+        
+        # Returning errors
+        return errors
     
     #--------------------------- UTILS functions ---------------------------------------------------
+    def isMask(self):
+        """ This function returns True if the input object is a Mask. False otherwise. """
+        return isinstance(self.inputParticles.get(), Mask)
+      
     def _ioArgs(self, isFirstStep):
         if isFirstStep:
             return "-i %s -o %s --save_metadata_stack %s --keep_input_columns " % (self.inputFn, self.outputStk, self.outputMd)
diff --git a/xmipp3/protocols/protocol_preprocess/protocol_process.py b/xmipp3/protocols/protocol_preprocess/protocol_process.py
index 5419a5e5a..4f83b1da6 100644
--- a/xmipp3/protocols/protocol_preprocess/protocol_process.py
+++ b/xmipp3/protocols/protocol_preprocess/protocol_process.py
@@ -28,6 +28,7 @@
 from pwem.protocols import ProtProcessParticles, ProtPreprocessVolumes
 from pwem.objects import Volume
 import pwem.emlib.metadata as md
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 from xmipp3.convert import (writeSetOfParticles, xmippToLocation,
                             writeSetOfVolumes, getImageLocation)
@@ -37,6 +38,8 @@ class XmippProcessParticles(ProtProcessParticles):
     """ Class to create a base template for Xmipp protocols 
     that process SetOfParticles
     """
+    _devStatus = UPDATED
+
     def __init__(self, **kwargs):
         ProtProcessParticles.__init__(self, **kwargs)
         self._args = "-i %(inputFn)s "
@@ -87,7 +90,7 @@ def createOutputStep(self):
         inputSet = self.inputParticles.get()
         # outputSet could be SetOfParticles, SetOfAverages or any future sub-class of SetOfParticles
         className = inputSet.getClassName()
-        outputSet = self._createSetFromName(className)
+        outputSet = inputSet.createCopy(self._getPath())
         outputSet.copyInfo(inputSet)
 
         self._preprocessOutput(outputSet)
diff --git a/xmipp3/protocols/protocol_resolution_fso.py b/xmipp3/protocols/protocol_resolution_fso.py
index ec6115724..ff0620025 100644
--- a/xmipp3/protocols/protocol_resolution_fso.py
+++ b/xmipp3/protocols/protocol_resolution_fso.py
@@ -35,6 +35,7 @@
 from pyworkflow import BETA, UPDATED, NEW, PROD
 from pwem.objects import Volume
 from pwem.protocols import ProtAnalysis3D
+from pyworkflow import BETA, UPDATED, NEW, PROD
 
 OUTPUT_3DFSC = '3dFSC.mrc'
 OUTPUT_DIRECTIONAL_FILTER = 'filteredMap.mrc'
@@ -48,7 +49,7 @@ class XmippProtFSO(ProtAnalysis3D):
     """
     _label = 'resolution fso'
     _lastUpdateVersion = VERSION_2_0
-    _devStatus = NEW
+    _devStatus = PROD
 
     def __init__(self, **args):
         ProtAnalysis3D.__init__(self, **args)
diff --git a/xmipp3/protocols/protocol_simulate_ctf.py b/xmipp3/protocols/protocol_simulate_ctf.py
index 5cd37897b..7ea1fb694 100644
--- a/xmipp3/protocols/protocol_simulate_ctf.py
+++ b/xmipp3/protocols/protocol_simulate_ctf.py
@@ -67,6 +67,20 @@ def _defineParams(self, form):
         form.addParam('DefocusF', params.FloatParam, default=25000,
                       label="Upper defocus (A)",
                       help="Negative value is overfocus")
+        form.addParam('astig', params.BooleanParam, default=False,
+                      label="Simulate astigmatic CTF?",
+                      help="If yes, defocusU and defocusV will have different values with a difference determined by"
+                           " the user, and there will be a value for angle")
+        form.addParam('angle0', params.FloatParam, default=40, condition='astig',
+                      label="Lower defocus angle (degrees)",
+                      help="Between 0 and 90")
+        form.addParam('angleF', params.FloatParam, default=50, condition='astig',
+                      label="Upper defocus angle (degrees)",
+                      help="Between 0 and 90")
+        form.addParam('Defocus0diff', params.FloatParam, default=-500, condition='astig',
+                      label="Lower defocus difference between defocusU and defocusV (A)")
+        form.addParam('DefocusFdiff', params.FloatParam, default=500, condition='astig',
+                      label="Upper defocus difference between defocusU and defocusV(A)")
 
     # --------------------------- INSERT steps functions ------------------------
     def _insertAllSteps(self):
@@ -97,15 +111,24 @@ def simulateStep(self):
             location = particle.getLocation()
             fnIn = str(location[0]) + "@" + location[1]
             fnOut = str(n) + "@" + fnStk
-            defocus = random.uniform(self.Defocus0.get(), self.DefocusF.get())
+            defocusU = random.uniform(self.Defocus0.get(), self.DefocusF.get())
             args = "-i %s -o %s" % (fnIn, fnOut)
-            args += " --fourier ctfdef %f %f %f %f --sampling %f -v 0" % (self.voltage, self.cs, self.Q0, defocus, Ts)
+            if self.astig:
+                defocusV = defocusU + random.uniform(self.Defocus0diff.get(), self.DefocusFdiff.get())
+                defocusAngle = random.uniform(self.angle0.get(), self.angleF.get())
+                args += " --fourier ctfdefastig %f %f %f %f %f %f --sampling %f -v 0" % \
+                        (self.voltage, self.cs, self.Q0, defocusU, defocusV, defocusAngle, Ts)
+            else:
+                defocusV = defocusU
+                defocusAngle = 0
+                args += " --fourier ctfdef %f %f %f %f  --sampling %f -v 0" % \
+                        (self.voltage, self.cs, self.Q0, defocusU, Ts)
             self.runJob("xmipp_transform_filter", args)
 
             newCTF = CTFModel()
-            newCTF.setDefocusU(defocus)
-            newCTF.setDefocusV(defocus)
-            newCTF.setDefocusAngle(0.0)
+            newCTF.setDefocusU(defocusU)
+            newCTF.setDefocusV(defocusV)
+            newCTF.setDefocusAngle(defocusAngle)
             newParticle = particle.clone()
             newParticle.setLocation((n, fnStk))
             acquisition = newParticle.getAcquisition()
diff --git a/xmipp3/protocols/protocol_subtract_projection.py b/xmipp3/protocols/protocol_subtract_projection.py
index efe683b58..ad03a948f 100644
--- a/xmipp3/protocols/protocol_subtract_projection.py
+++ b/xmipp3/protocols/protocol_subtract_projection.py
@@ -34,6 +34,7 @@
 from xmipp3.convert import writeSetOfParticles, readSetOfParticles
 from pyworkflow import BETA, UPDATED, NEW, PROD
 
+OUTPUT = "output_particles.xmd"
 
 class XmippProtSubtractProjectionBase(EMProtocol):
     """ Helper class that contains some Protocol utilities methods
@@ -79,7 +80,7 @@ def createOutputStep(self):
         inputSet = self.inputParticles.get()
         outputSet = self._createSetOfParticles()
         outputSet.copyInfo(inputSet)
-        readSetOfParticles(self._getExtraPath("output_particles.xmd"), outputSet,
+        readSetOfParticles(self._getExtraPath(OUTPUT), outputSet,
                            extraLabels=[emlib.MDL_SUBTRACTION_R2, emlib.MDL_SUBTRACTION_BETA0,
                                         emlib.MDL_SUBTRACTION_BETA1])
         self._defineOutputs(outputParticles=outputSet)
@@ -101,6 +102,8 @@ def _defineParams(self, form):
                            ' whole images.')
         form.addParam('subtract', EnumParam, default=0, choices=["Keep", "Subtract"], display=EnumParam.DISPLAY_HLIST,
                       label="Mask contains the part to ")
+        form.addParallelSection(threads=0, mpi=4)
+
     # --------------------------- INSERT steps functions --------------------------------------------
     def _insertSubSteps(self):
         self._insertFunctionStep('convertStep')
@@ -116,13 +119,17 @@ def subtractionStep(self):
         if fnVol.endswith('.mrc'):
             fnVol += ':mrc'
         args = '-i %s --ref %s -o %s --sampling %f --max_resolution %f --padding %f ' \
-               '--sigma %d --limit_freq %d --cirmaskrad %d --save %s' % \
-               (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath("output_particles"),
+               '--sigma %d --limit_freq %d --cirmaskrad %d --save %s --oroot %s' % \
+               (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath(OUTPUT),
                 vol.getSamplingRate(), self.resol.get(), self.pad.get(), self.sigma.get(),
-                int(self.limit_freq.get()), self.cirmaskrad.get(), self._getExtraPath())
+                int(self.limit_freq.get()), self.cirmaskrad.get(), self._getExtraPath(),
+                self._getExtraPath("subtracted_part"))
         mask = self.mask.get()
+        fnMask = mask.getFileName()
+        if fnMask.endswith('.mrc'):
+            fnMask += ':mrc'
         if mask is not None:
-            args += ' --mask %s' % mask.getFileName()
+            args += ' --mask %s' % fnMask
         if self.nonNegative.get():
             args += ' --nonNegative'
         if self.subtract.get():
@@ -178,6 +185,7 @@ class XmippProtBoostParticles(XmippProtSubtractProjectionBase):
     # --------------------------- DEFINE param functions --------------------------------------------
     def _defineParams(self, form):
         XmippProtSubtractProjectionBase._defineParams(form)
+        form.addParallelSection(threads=0, mpi=4)
 
     # --------------------------- INSERT steps functions --------------------------------------------
     def _insertSubSteps(self):
@@ -194,10 +202,10 @@ def boostingStep(self):
         if fnVol.endswith('.mrc'):
             fnVol += ':mrc'
         args = '-i %s --ref %s -o %s --sampling %f --max_resolution %f --padding %f --sigma %d --limit_freq %d ' \
-               '--cirmaskrad %d --boost --save %s'\
-               % (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath("output_particles"),
+               '--cirmaskrad %d --boost --save %s --oroot %s'\
+               % (self._getExtraPath(self.INPUT_PARTICLES), fnVol, self._getExtraPath(OUTPUT),
                   vol.getSamplingRate(), self.resol.get(), self.pad.get(), self.sigma.get(), int(self.limit_freq.get()),
-                  self.cirmaskrad.get(), self._getExtraPath())
+                  self.cirmaskrad.get(), self._getExtraPath(), self._getExtraPath("subtracted_part"))
 
         if self.nonNegative.get():
             args += ' --nonNegative'
diff --git a/xmipp3/protocols/protocol_tilt_analysis.py b/xmipp3/protocols/protocol_tilt_analysis.py
index e6827da35..2005a3a20 100644
--- a/xmipp3/protocols/protocol_tilt_analysis.py
+++ b/xmipp3/protocols/protocol_tilt_analysis.py
@@ -242,14 +242,6 @@ def _checkNewOutput(self):
             if outputStep and outputStep.isWaiting():
                 outputStep.setStatus(cons.STATUS_NEW)
 
-    def _closeOutputSet(self):
-        if self.hasAttribute('outputMicrographs'):
-            self.outputMicrographs.close()
-            self.outputMicrographs.setStreamState(Set.STREAM_CLOSED)
-        if self.hasAttribute('discardedMicrographs'):
-            self.discardedMicrographs.close()
-            self.discardedMicrographs.setStreamState(Set.STREAM_CLOSED)
-
     def _insertNewMicrographSteps(self, insertedDict, inputMics):
         """ Insert steps to process new micrographs (from streaming)
         Params:
diff --git a/xmipp3/viewers/__init__.py b/xmipp3/viewers/__init__.py
index 6d4694540..a8dbb1fb5 100644
--- a/xmipp3/viewers/__init__.py
+++ b/xmipp3/viewers/__init__.py
@@ -27,6 +27,7 @@
 from .viewer import XmippViewer
 from .plotter import XmippPlotter
 
+from .viewer_angular_resolution_alignment import XmippProtAngResAlignViewer
 from .viewer_cl2d import XmippCL2DViewer
 from .viewer_ctf_consensus import XmippCTFConsensusViewer
 from .viewer_deep_consensus import XmippDeepConsensusViewer
@@ -48,13 +49,13 @@
 from .viewer_volume_strain import XmippVolumeStrainViewer
 from .viewer_reconstruct_highres import XmippReconstructHighResViewer
 from .viewer_solid_angles import SolidAnglesViewer
-from .viewer_consensus_classes3D import XmippConsensusClasses3DViewer
-from .viewer_classes3D import XmippClasses3DViewer
+from .viewer_consensus_classes import XmippConsensusClassesViewer
 from .viewer_extract_asymmetric_unit import viewerXmippProtExtractUnit
 from .viewer_deepEMHancer import viewerXmippProtDeepVolPostProc
 from .viewer_local_sharpening import viewerXmippProtLocSharp
 from .viewer_volume_subtraction import XmippProtVolSubtractionViewer
 from .viewer_volume_consensus import XmippVolumeConsensusViewer
+from .viewer_dose_analysis import XmippMovieDoseAnalysisViewer
 
 #from .viewer_combine_pdb import XmippProtCombinePdbViewer
 
@@ -67,4 +68,4 @@
 
 # TODO: Import from continuousflex the modules needed to create clusters (soft dependency)
 from .viewer_analyze_local_ctf import XmippAnalyzeLocalCTFViewer
-from .viewer_subtract_projection import XmippSubtractProjectionViewer
\ No newline at end of file
+from .viewer_subtract_projection import XmippSubtractProjectionViewer
diff --git a/xmipp3/viewers/viewer.py b/xmipp3/viewers/viewer.py
index 7d1139702..2a9210103 100644
--- a/xmipp3/viewers/viewer.py
+++ b/xmipp3/viewers/viewer.py
@@ -41,7 +41,7 @@
 from xmipp3.protocols import XmippProtKerdensom
 from xmipp3.protocols import XmippParticlePickingAutomatic
 from xmipp3.protocols import XmippProtParticlePickingPairs
-from xmipp3.protocols import XmippProtRotSpectra
+#from xmipp3.protocols import XmippProtRotSpectra
 from xmipp3.protocols import XmippProtScreenParticles
 from xmipp3.protocols import XmippProtCTFMicrographs
 from xmipp3.protocols import XmippProtValidateNonTilt
@@ -50,7 +50,6 @@
 from xmipp3.protocols import XmippProtAssignmentTiltPair
 from xmipp3.protocols import XmippProtMovieGain
 from xmipp3.protocols import XmippProtDeepDenoising
-from xmipp3.protocols import XmippProtParticleBoxsize
 from .plotter import XmippPlotter
 from xmipp3.protocols import XmippProtTiltAnalysis
 from pwem.viewers.viewers_data import MicrographsView
@@ -70,7 +69,6 @@ class XmippViewer(DataViewer):
                 XmippProtExtractParticlesPairs,
                 XmippProtKerdensom,
                 XmippProtParticlePickingPairs,
-                XmippProtRotSpectra,
                 XmippProtScreenParticles,
                 XmippProtCTFMicrographs,
                 XmippProtValidateNonTilt,
@@ -79,7 +77,6 @@ class XmippViewer(DataViewer):
                 XmippProtAngularGraphConsistency,
                 XmippProtMovieGain,
                 XmippProtDeepDenoising,
-                XmippProtParticleBoxsize,
                 XmippProtTiltAnalysis
                 ]
 
@@ -187,15 +184,6 @@ def _visualize(self, obj, **kwargs):
             #                                     ratio2Value=4.5)
             # self._views.append(MovieGainMonitorPlotter(movieGainMonitor))
 
-        elif issubclass(cls, XmippProtRotSpectra):
-            self._visualize(obj.outputClasses,
-                            viewParams={'columns': obj.SomXdim.get(),
-                                        RENDER: ' spectraPlot._filename average._filename',
-                                        ZOOM: 30,
-                                        VISIBLE: 'enabled id _size average._filename '
-                                                 'spectraPlot._filename',
-                                        'labels': 'id _size',
-                                        SORT_BY: 'id'})
 
         elif issubclass(cls, XmippProtKerdensom):
             self._visualize(obj.outputClasses, viewParams={'columns': obj.SomXdim.get(),
@@ -289,32 +277,6 @@ def _visualize(self, obj, **kwargs):
             memory = showj.getJvmMaxMemory()
             launchSupervisedPickerGUI(micsfn, posDir, obj, mode='review',
                                       memory=memory, inTmpFolder=inTmpFolder)
-
-        elif issubclass(cls, XmippProtParticleBoxsize):
-            """ Launching a Coordinates viewer with only one coord in the center
-                with the estimated boxsize.
-            """
-            micSet = obj.inputMicrographs.get()
-
-            coordsFn = self._getExtraPath(micSet.getName()+'_coords_to_view.sqlite')
-            if not os.path.exists(coordsFn):
-                # Just creating the coords once
-                coordsSet = SetOfCoordinates(filename=coordsFn)
-                coordsSet.setBoxSize(obj.boxsize)
-                for mic in micSet:
-                    coord = Coordinate()
-                    coord.setMicrograph(mic)
-                    coord.setPosition(mic.getXDim()/2, mic.getYDim()/2)
-                    coordsSet.append(coord)
-                coordsSet.write()
-            else:
-                coordsSet = SetOfCoordinates(filename=coordsFn)
-                coordsSet.loadAllProperties()
-
-            coordsSet.setMicrographs(micSet)
-            coordsSet.setName(micSet.getName())
-            self._visualize(coordsSet)
-
          # We need this case to happens before the ProtParticlePicking one
         elif issubclass(cls, XmippProtAssignmentTiltPair):
             if obj.getOutputsSize() >= 1:
diff --git a/xmipp3/viewers/viewer_angular_resolution_alignment.py b/xmipp3/viewers/viewer_angular_resolution_alignment.py
new file mode 100755
index 000000000..8bf2632a5
--- /dev/null
+++ b/xmipp3/viewers/viewer_angular_resolution_alignment.py
@@ -0,0 +1,148 @@
+# **************************************************************************
+# *
+# * Authors:     J.L. Vilas (jlvilas@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+
+from pyworkflow.protocol.params import LabelParam, StringParam, EnumParam, IntParam, LEVEL_ADVANCED
+from pyworkflow.viewer import ProtocolViewer, DESKTOP_TKINTER
+
+from pwem.emlib.metadata import MetaData
+from pwem import emlib
+
+
+import numpy as np
+
+import matplotlib.pyplot as plt
+from matplotlib import cm
+from matplotlib.ticker import FuncFormatter
+
+from xmipp3.protocols.protocol_angular_resolution_alignment import \
+    XmippProtResolutionAlignment, RADIAL_RESOLUTION_FN
+from xmipp3.viewers.plotter import XmippPlotter
+import xmippLib
+
+
+class XmippProtAngResAlignViewer(ProtocolViewer):
+    """
+    Visualization tools for the validation of alignment based on resolution.
+    
+    """
+    _label = 'viewer angular resolution alignment'
+    _targets = [XmippProtResolutionAlignment]
+    _environments = [DESKTOP_TKINTER]
+
+    @staticmethod
+    def getColorMapChoices():
+        return plt.colormaps()
+
+    def _defineParams(self, form):
+        form.addSection(label='Visualization')
+        groupDirFSC = form.addGroup('Resolution Analysis')
+        groupDirFSC.addParam('doShowRadialResolution', LabelParam, label="Radial FSC")
+
+    def _getVisualizeDict(self):
+        return {'doShowRadialResolution': self._showRadialResolution}
+
+    def _showRadialResolution(self, params=None):
+        """
+        It shows the FSC curve in terms of the resolution
+        The horizontal axis is linear in this plot. Note
+        That this is not the normal representation of the FSC
+        """
+        fnmd = self.protocol._getExtraPath(RADIAL_RESOLUTION_FN)
+        title = 'Radial Average of directional resolution'
+        xTitle = 'Radius (px)'
+        yTitle = 'Resolution (A)'
+        mdLabelX = emlib.MDL_IDX
+        mdLabelY = emlib.MDL_RESOLUTION_FRC
+        self._plotCurveFSC(fnmd, title, xTitle, yTitle, mdLabelX, mdLabelY)
+
+    def _plotCurveFSC(self, fnmd, title, xTitle, yTitle, mdLabelX, mdLabelY):
+        """
+        This function is called by _showFSCCurve.
+        It shows the FSC curve in terms of the resolution
+        That this is not the normal representation of the FSC
+        """
+        md = xmippLib.MetaData(fnmd)
+        xplotter = XmippPlotter(figure=None)
+        xplotter.plot_title_fontsize = 11
+
+        a = xplotter.createSubPlot(title, xTitle, yTitle, 1, 1)
+        xplotter.plotMdFile(md, mdLabelX, mdLabelY, 'g')
+
+        xx, yy = self._prepareDataForPlot(md, mdLabelX, mdLabelY)
+        a.grid(True)
+
+        return plt.show()
+
+    def _formatFreq(self, value, pos):
+        """ Format function for Matplotlib formatter. """
+        inv = 999.
+        if value:
+            inv = 1 / value
+        return "1/%0.2f" % inv
+
+    def interpolRes(self, thr, x, y):
+        """
+        This function is called by _showAnisotropyCurve.
+        It provides the cut point of the curve defined
+        by the points (x,y) with a threshold thr.
+        The flag okToPlot shows if there is no intersection points
+        """
+        idx = np.arange(0, len(x))
+        aux = np.array(y) <= thr
+        idx_x = idx[aux]
+        okToPlot = True
+        resInterp = []
+        if not idx_x.any():
+            okToPlot = False
+        else:
+            if len(idx_x) > 1:
+                idx_2 = idx_x[0]
+                idx_1 = idx_2 - 1
+                if idx_1 < 0:
+                    idx_2 = idx_x[1]
+                    idx_1 = idx_2 - 1
+                y2 = x[idx_2]
+                y1 = x[idx_1]
+                x2 = y[idx_2]
+                x1 = y[idx_1]
+                slope = (y2 - y1) / (x2 - x1)
+                ny = y2 - slope * x2
+                resInterp = 1.0 / (slope * thr + ny)
+            else:
+                okToPlot = False
+
+        return resInterp, okToPlot
+
+
+    def _prepareDataForPlot(self, md, mdLabelX, mdLabelY):
+        """ plot metadata columns mdLabelX and mdLabelY
+            if nbins is in args then and histogram over y data is made
+        """
+        xx = md.getColumnValues(mdLabelX)
+        yy = md.getColumnValues(mdLabelY)
+        return xx, yy
+
diff --git a/xmipp3/viewers/viewer_classes3D.py b/xmipp3/viewers/viewer_classes3D.py
deleted file mode 100644
index bc58eba13..000000000
--- a/xmipp3/viewers/viewer_classes3D.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# **************************************************************************
-# *
-# * Authors:     J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-"""
-This module implement the wrappers aroung Xmipp CL2D protocol
-visualization program.
-"""
-from pyworkflow.gui import IconButton, Icon
-from pyworkflow.gui.form import FormWindow
-from pyworkflow.viewer import (ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO)
-from pyworkflow.protocol.params import StringParam, LabelParam, IntParam
-
-from xmipp3.protocols.protocol_consensus_classes3D import XmippProtConsensusClasses3D
-from pwem.objects import Class3D
-
-import pickle
-import matplotlib.pyplot as plt
-
-FILE_OBJECTIVE_FDATA = 'ObjectiveFData.pkl'
-FILE_CLUSTERINGS = 'clusterings.pkl'
-FILE_ELBOWCLUSTERS = 'elbowclusters.pkl'
-
-
-class XmippClasses3DViewer(ProtocolViewer):
-    """ Visualization of results from the consensus classes 3D protocol
-    """
-    _label = 'viewer classes 3D'
-    _targets = [XmippProtConsensusClasses3D]
-    _environments = [DESKTOP_TKINTER, WEB_DJANGO]
-
-    def __init__(self, **kwargs):
-        ProtocolViewer.__init__(self, **kwargs)
-        self.nclusters, self.objFValues = self.getObjectiveFData()
-        self._elbowIdx = self.getElbowIndex()
-        self._allClusterings = self.getClusterings()
-
-    def getObjectiveFData(self):
-        loadPath = self.protocol._getExtraPath(FILE_OBJECTIVE_FDATA)
-        with open(loadPath, 'rb') as f:
-            self._ObjFData = pickle.load(f)
-        return self._ObjFData
-
-    def getClusterings(self):
-        loadPath = self.protocol._getExtraPath(FILE_CLUSTERINGS)
-        with open(loadPath, 'rb') as f:
-            self._allClusterings = pickle.load(f)
-        return self._allClusterings
-
-    def getElbowIndex(self):
-        loadPath = self.protocol._getExtraPath(FILE_ELBOWCLUSTERS)
-        with open(loadPath, 'rb') as f:
-            self.elbowIndex = pickle.load(f)
-        return self.elbowIndex
-
-    def _defineParams(self, form):
-        form.addSection(label='Visualization')
-        form.addParam('displayObjectiveFunction', LabelParam,
-                      label='Visualize Objective Function',
-                      help='Open a GUI to visualize the objective function for each number of clusters ')
-        form.addParam('chooseNumberOfClusters', IntParam, default=-1,
-                      label='Number of Clusters',
-                      help='Choose the final number of clusters in the consensus clustering. Press the eye')
-
-        #TODO: set an apply button to change the number of clusters instead of using the eye
-        '''command = self._chooseNumberOfClusters()
-        btn = IconButton(self.getWindow(), "Apply", Icon.ACTION_CONTINUE, command=command)
-        btn.config(relief="flat", activebackground=None, compound='left',
-                   fg='black', overrelief="raised")
-        btn.bind('<Button-1>')
-        btn.grid(row=2, column=2, sticky='nw')'''
-
-
-    def _getVisualizeDict(self):
-        return {'chooseNumberOfClusters': self._chooseNumberOfClusters,
-                'displayObjectiveFunction': self._visualizeObjectiveFunction}
-
-    def _visualizeObjectiveFunction(self, e=None):
-        nclusters, ob_values = self.nclusters, self.objFValues
-        elbowIdx = self._elbowIdx
-
-        plt.plot(nclusters, ob_values)
-        plt.scatter([nclusters[elbowIdx]], [ob_values[elbowIdx]], color='green')
-        plt.xlabel('Number of clusters')
-        plt.ylabel('Objective values')
-        plt.title('Objective values for each number of clusters')
-        plt.show()
-
-    def _chooseNumberOfClusters(self, e=None):
-        views=[]
-        nclusters = self.nclusters
-        elbow_nclust = nclusters[self._elbowIdx]
-
-        nclust = self.chooseNumberOfClusters.get()
-        if nclust == -1:
-            nclust = elbow_nclust
-        elif nclust > max(nclusters):
-            nclust = max(nclusters)
-
-        scipion_clustering = self.buildSetOfClasses(nclust)
-        self.protocol._defineOutputs(outputClasses=scipion_clustering)
-        for item in self.protocol.inputMultiClasses:
-            self.protocol._defineSourceRelation(item, scipion_clustering)
-
-        #TODO: esto es un objeto SetOfClasses3D pero no sabemos como lanzar un viewer con el sin tener el protocolo
-        #views.append(ObjectView(self._project, self.protocol.strId()))
-        return views
-
-    def buildSetOfClasses(self, nclust):
-        '''From a list of clusterings, nclust index it and that clustering
-        is converted into a scipion setOfClasses'''
-        clustIdx = self.nclusters.index(nclust)
-        clustering = self._allClusterings[clustIdx]
-
-        inputParticles = self.protocol.inputMultiClasses[0].get().getImages()
-        outputClasses = self.protocol._createSetOfClasses3D(inputParticles)
-        for classItem in clustering:
-            numOfPart = classItem[0]
-            partIds = classItem[1]
-            setRepId = classItem[2]
-            clsRepId = classItem[3]
-
-            setRep = self.protocol.inputMultiClasses[setRepId].get()
-            clRep = setRep[clsRepId]
-
-            newClass = Class3D()
-            # newClass.copyInfo(clRep)
-            newClass.setAcquisition(clRep.getAcquisition())
-            newClass.setRepresentative(clRep.getRepresentative())
-
-            outputClasses.append(newClass)
-
-            enabledClass = outputClasses[newClass.getObjId()]
-            enabledClass.enableAppend()
-            for itemId in partIds:
-                enabledClass.append(inputParticles[itemId])
-            outputClasses.update(enabledClass)
-
-        return outputClasses
diff --git a/xmipp3/viewers/viewer_consensus_classes.py b/xmipp3/viewers/viewer_consensus_classes.py
new file mode 100644
index 000000000..ef14f235c
--- /dev/null
+++ b/xmipp3/viewers/viewer_consensus_classes.py
@@ -0,0 +1,145 @@
+# **************************************************************************
+# *
+# * Authors:     J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
+# *              Oier Lauzirika Zarrabeitia (oierlauzi@bizkaia.eu)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+"""
+This module implement the wrappers around
+visualization program.
+"""
+from pyworkflow.viewer import (ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO)
+from pyworkflow.protocol.params import Form, Line, LabelParam, IntParam
+from pyworkflow.protocol.params import GE
+
+from pwem.viewers.showj import *
+from pwem.viewers import TableView, ObjectView
+from pwem.objects import SetOfClasses
+
+from xmipp3.protocols.protocol_consensus_classes import XmippProtConsensusClasses
+
+import numpy as np
+import matplotlib.pyplot as plt
+import scipy.spatial
+import scipy.cluster
+
+class XmippConsensusClassesViewer(ProtocolViewer):
+    """ Visualization of results from the consensus classes 3D protocol
+    """
+    _label = 'viewer consensus classes'
+    _targets = [XmippProtConsensusClasses]
+    _environments = [DESKTOP_TKINTER, WEB_DJANGO]
+
+    def __init__(self, **kwargs):
+        ProtocolViewer.__init__(self, **kwargs)
+
+    def _defineParams(self, form: Form):
+        form.addSection(label='Classes')
+        form.addParam('visualizeClasses', IntParam,
+                      validators=[GE(1)], default=1,
+                      label='Classes' )
+
+        form.addSection(label='Graphs')
+        form.addParam('visualizeDendrogram', LabelParam,
+                       label='Dendrogram' )
+        form.addParam('visualizeCostFunction', LabelParam,
+                       label='Cost function' )
+
+
+    def _getVisualizeDict(self):
+        return {
+            'visualizeClasses': self._visualizeClasses,
+            'visualizeDendrogram': self._visualizeDendrogram,
+            'visualizeCostFunction': self._visualizeCostFunction,
+        }
+    
+    # --------------------------- UTILS functions ------------------------------
+    def _getLinkageMatrix(self) -> np.ndarray:
+        return self.protocol._readLinkageMatrix()
+    
+    def _getElbows(self) -> dict:
+        return self.protocol._readElbows()
+    
+    def _getMergedIntersections(self, size) -> SetOfClasses:
+        return self.protocol._obtainMergedIntersections(size)
+    
+    def _visualizeClasses(self, param=None):
+        count = self.visualizeClasses.get()
+        classes = self._getMergedIntersections(count)
+        return self._showSetOfClasses3D(classes)
+        
+    def _visualizeDendrogram(self, param=None):
+        linkage = self._getLinkageMatrix()
+        elbows = self._getElbows()
+        labels = np.arange(1, len(linkage)+2)
+        y = linkage[:,2]
+        
+        fig, ax = plt.subplots()
+        scipy.cluster.hierarchy.dendrogram(linkage, ax=ax, labels=labels)
+        
+        # Plot the elbows
+        for key, value in elbows.items():
+            index = len(y) - value
+            label = key + ': ' + str(value)
+            ax.axhline((y[index] + y[index+1])/2, label=label, color='black', linestyle='--')
+        
+        ax.legend()
+        ax.set_ylabel('cost')
+        ax.set_xlabel('classId')
+        ax.set_title('Dendrogram')
+        
+        return [fig]
+        
+    def _visualizeCostFunction(self, param=None):
+        linkage = self._getLinkageMatrix()
+        elbows = self._getElbows()
+        y = linkage[:,2]
+        x = np.arange(len(y), 0, -1)
+        
+        fig, ax = plt.subplots()
+        ax.plot(x, y)
+        
+        # Plot the elbows
+        for key, value in elbows.items():
+            index = len(x) - value
+            label = key + ': ' + str(value)
+            ax.scatter([x[index]], [y[index]], label=label)
+        
+        ax.legend()
+        ax.set_ylabel('cost')
+        ax.set_xlabel('class count')
+        ax.set_title('Cost function')
+        
+        return [fig]
+    
+    def _showSetOfClasses3D(self, classes):
+        labels = 'enabled id _size _representative._filename _xmipp_classIntersectionSizePValue _xmipp_classIntersectionRelativeSizePValue'
+        labelRender = '_representative._filename'
+        return [ObjectView( self._project, classes.strId(), classes.getFileName(),
+                            viewParams={ORDER: labels,
+                                        VISIBLE: labels,
+                                        RENDER: labelRender,
+                                        SORT_BY: '_size desc',
+                                        MODE: MODE_MD})]
+        
\ No newline at end of file
diff --git a/xmipp3/viewers/viewer_consensus_classes3D.py b/xmipp3/viewers/viewer_consensus_classes3D.py
deleted file mode 100644
index 56f5d49f8..000000000
--- a/xmipp3/viewers/viewer_consensus_classes3D.py
+++ /dev/null
@@ -1,291 +0,0 @@
-# **************************************************************************
-# *
-# * Authors:     J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
-# *              Oier Lauzirika Zarrabeitia (oierlauzi@bizkaia.eu)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 2 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# * You should have received a copy of the GNU General Public License
-# * along with this program; if not, write to the Free Software
-# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# * 02111-1307  USA
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-"""
-This module implement the wrappers aroung Xmipp CL2D protocol
-visualization program.
-"""
-from pyworkflow.viewer import (ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO)
-from pyworkflow.protocol.params import StringParam, LabelParam, IntParam
-from pyworkflow.protocol.params import GE, GT, LE, LT
-
-from pwem.viewers import TableView, ObjectView
-from pwem.viewers.showj import *
-
-from xmipp3.protocols.protocol_consensus_classes3D import XmippProtConsensusClasses3D
-
-from scipy.cluster import hierarchy
-
-import matplotlib.pyplot as plt
-import numpy as np
-
-
-class XmippConsensusClasses3DViewer(ProtocolViewer):
-    """ Visualization of results from the consensus classes 3D protocol
-    """
-    _label = 'viewer consensus classes 3D'
-    _targets = [XmippProtConsensusClasses3D]
-    _environments = [DESKTOP_TKINTER, WEB_DJANGO]
-
-    def __init__(self, **kwargs):
-        ProtocolViewer.__init__(self, **kwargs)
-
-    def _defineParams(self, form):
-        # Particles section
-        form.addSection(label='Classes')
-        form.addParam('displayClasses', IntParam, label='Custom number of classes',
-                      validators=[GE(1)], default=1,
-                      help='Open a GUI to visualize the class of the given iteration. Warning: It takes time to open')
-
-        form.addParam('displayInitialClasses', LabelParam, label='Initial classes')
-        form.addParam('displayManualClasses', LabelParam, label='Manual number of classes')
-        form.addParam('displayOriginClasses', LabelParam, label='Number of classes by proximity to origin')
-        form.addParam('displayAngleClasses', LabelParam, label='Number of classes by angle')
-        form.addParam('displayPllClasses', LabelParam, label='Number of classes by profile likelihood')
-
-        # Graph section
-        form.addSection(label='Graphs')
-        form.addParam('displayDendrogram', LabelParam, label='Dendrogram',
-                        help='Open a GUI to visualize the dendogram each number of clusters')
-        form.addParam('displayDendrogramLog', LabelParam, label='Dendrogram (log)',
-                        help='Open a GUI to visualize the dendogram each number'
-                        ' of clusters with a logarithmic "y" axis')
-
-        form.addParam('displayObjectiveFunction', LabelParam, label='Objective Function',
-                        help='Open a GUI to visualize the objective function for each number of clusters')
-        form.addParam('displayObjectiveFunctionLog', LabelParam, label='Objective Function (log)',
-                        help='Open a GUI to visualize the objective function'
-                        ' for each number of clusters with a logarithmic "y" axis')
-
-        # Reference classification section (only if exists)
-        if self._readReferenceClassificationSizes() is not None:
-            form.addSection(label='Reference classification consensus')
-            form.addParam('displayReferenceClassificationSizePercentiles', LabelParam, label='Reference classification consensus size percentiles',
-                            help='Open a GUI to visualize a table with the most'
-                            ' common percentiles of the sizes of a random classification consensus')
-
-    def _getVisualizeDict(self):
-        return {
-            'displayClasses': self._visualizeClasses,
-            'displayInitialClasses': self._visualizeInitialClasses,
-            'displayManualClasses': self._visualizeManualClasses,
-            'displayOriginClasses': self._visualizeOriginClasses,
-            'displayAngleClasses': self._visualizeAngleClasses,
-            'displayPllClasses': self._visualizePllClasses,
-            'displayDendrogram': self._visualizeDendrogram,
-            'displayDendrogramLog': self._visualizeDendrogramLog,
-            'displayObjectiveFunction': self._visualizeObjectiveFunction,
-            'displayObjectiveFunctionLog': self._visualizeObjectiveFunctionLog,
-            'displayReferenceClassificationSizePercentiles': self._visualizeReferenceClassificationSizes,
-        }
-
-    # --------------------------- UTILS functions ------------------------------
-    def _getInputParticles(self):
-        return self.protocol._getInputParticles()
-
-    def _getInputClassifications(self):
-        return self.protocol._getInputClassifications()
-
-    def _readIntersections(self):
-        return self.protocol._readIntersections()
-
-    def _readClustering(self, iter):
-        return self.protocol._readClustering(iter)
-
-    def _readClusteringCount(self):
-        return self.protocol._readClusteringCount()
-
-    def _readAllClusterings(self):
-        return self.protocol._readAllClusterings()
-
-    def _readObjectiveValues(self):
-        return self.protocol._readObjectiveValues()
-    
-    def _readElbows(self):
-        return self.protocol._readElbows()
-
-    def _readReferenceClassificationSizes(self):
-        return self.protocol._readReferenceClassificationSizes()
-
-    def _readReferenceClassificationRelativeSizes(self):
-        return self.protocol._readReferenceClassificationRelativeSizes()
-
-    def _visualizeClasses(self, e):
-        # Read data
-        count = self.displayClasses.get()
-        particles = self._getInputParticles()
-        clustering = self._readClustering(count)
-        randomConsensusSizes = self._readReferenceClassificationSizes()
-        randomConsensusRelativeSizes = self._readReferenceClassificationRelativeSizes()
-
-        # Create the output classes and show them
-        outputClasses = self.protocol._createOutputClasses3D(
-            particles,
-            clustering, 
-            'viewer_'+str(count),
-            randomConsensusSizes, 
-            randomConsensusRelativeSizes
-        )
-
-        return self._showSetOfClasses3D(outputClasses)
-    
-    def _visualizeInitialClasses(self, e):
-        return self._showSetOfClasses3D(self.protocol.outputClasses_initial)
-
-    def _visualizeManualClasses(self, e):
-        return self._showSetOfClasses3D(self.protocol.outputClasses_manual)
-
-    def _visualizeOriginClasses(self, e):
-        return self._showSetOfClasses3D(self.protocol.outputClasses_origin)
-
-    def _visualizeAngleClasses(self, e):
-        return self._showSetOfClasses3D(self.protocol.outputClasses_angle)
-
-    def _visualizePllClasses(self, e):
-        return self._showSetOfClasses3D(self.protocol.outputClasses_pll)
-
-    def _visualizeDendrogram(self, e):
-        clusterings = self._readAllClusterings()
-        objectiveFunction = self._readObjectiveValues()
-
-        fig, ax = plt.subplots()
-        self._plotDendrogram(fig, ax, list(reversed(clusterings)), list(reversed(objectiveFunction)))
-        return [fig]
-        
-    def _visualizeDendrogramLog(self, e):
-        clusterings = self._readAllClusterings()
-        objectiveFunction = self._readObjectiveValues()
-
-        fig, ax = plt.subplots()
-        self._plotDendrogram(fig, ax, list(reversed(clusterings)), list(reversed(objectiveFunction)))
-        ax.set_yscale('log')
-        return [fig]
-        
-    def _visualizeObjectiveFunction(self, e):
-        objectiveFunction = self._readObjectiveValues()
-        elbows = self._readElbows()
-
-        fig, ax = plt.subplots()
-        self._plotObjectiveFunctionAndElbows(fig, ax, objectiveFunction, elbows)
-        return [fig]
-    
-    def _visualizeObjectiveFunctionLog(self, e):
-        objectiveFunction = self._readObjectiveValues()
-        elbows = self._readElbows()
-
-        fig, ax = plt.subplots()
-        self._plotObjectiveFunctionAndElbows(fig, ax, objectiveFunction[:-1], elbows)
-        ax.set_yscale('log')
-        return [fig]
-
-    def _visualizeReferenceClassificationSizes(self, e):
-        randomConsensusSizes = self._readReferenceClassificationSizes()
-        randomConsensusRelativeSizes = self._readReferenceClassificationRelativeSizes()
-
-        # Calculate the percentiles
-        percentiles = [1, 5, 10, 25, 50, 75, 90, 95, 99]
-        randomConsensusSizePercentiles = np.percentile(randomConsensusSizes, percentiles)
-        randomConsensusRelativeSizePercentiles = np.percentile(randomConsensusRelativeSizes, percentiles)
-        
-        # Create a table with the data
-        data = list(zip(percentiles, randomConsensusSizePercentiles, randomConsensusRelativeSizePercentiles))
-        header = ['Percentile (%)', 'Size percentile', 'Relative size percentile']
-        title = 'Reference classification consensus size percentiles'
-        return [TableView(header, data, title=title)]
-
-    def _showSetOfClasses3D(self, classes):
-        labels = 'enabled id _size _representative._filename _xmipp_classIntersectionSizePValue _xmipp_classIntersectionRelativeSizePValue'
-        labelRender = '_representative._filename'
-        return [ObjectView( self._project, classes.strId(), classes.getFileName(),
-                            viewParams={ORDER: labels,
-                                        VISIBLE: labels,
-                                        RENDER: labelRender,
-                                        SORT_BY: '_size desc',
-                                        MODE: MODE_MD})]
-
-    def _plotDendrogram(self, fig, ax, clusterings, obValues):
-        """ Plot the dendrogram from the objective functions of the merge
-            between the groups of images """
-
-        # Initialize required values
-        linkageMatrix = np.zeros((len(clusterings)-1, 4))
-        clsIds = np.arange(len(clusterings))
-        nIntersections = len(clusterings[0])
-
-        # Loop over each iteration of clustering
-        for i in range(len(clusterings)-1):
-            # Find the sets that were merged
-            clsMerged = []
-            assert(len(clusterings[i]) == len(clsIds))
-            for cls, clsId in zip(clusterings[i], clsIds):
-                if cls not in clusterings[i+1]:
-                    clsMerged.append(clsId)
-            assert(len(clsMerged) == 2)
-            clsMerged = np.array(clsMerged)
-            
-            # Find original number of sets within new set
-            nCls = list(map(lambda x : linkageMatrix[x,3] if (x >= 0) else 1, clsMerged - nIntersections))
-
-            # Create linkage matrix
-            linkageMatrix[i, 0:2] = clsMerged  # ids of merged sets
-            linkageMatrix[i, 2] = obValues[i+1]   # objective function as distance
-            linkageMatrix[i, 3] = sum(nCls)  # total number of original sets
-
-            # Change set ids to reflect new set of clusters
-            for id in clsMerged:
-                clsIds = np.delete(clsIds, np.argwhere(clsIds == id))
-            clsIds = np.append(clsIds, len(clusterings)+i)
-
-        # Plot resulting dendrogram
-        hierarchy.dendrogram(linkageMatrix, ax=ax)
-        ax.set_title('Dendrogram')
-        ax.set_xlabel('sets ids')
-        ax.set_ylabel('objective function')
-        ax.set_ylim([np.min(linkageMatrix[:, 2]), np.max(linkageMatrix[:, 2])])
-
-    def _plotObjectiveFunctionAndElbows(self, fig, ax, obValues, elbows):
-        """ Plots the objective function and elbows """
-
-        # Shorthands for some variables
-        numClusters = np.arange(1, 1+len(obValues))
-
-        # Plot obValues vs numClusters
-        ax.plot(numClusters, obValues)
-
-        # Show elbows as scatter points
-        for key, value in elbows.items():
-            value -= 1 # Value uses 1-based indexing
-            label = key + ': ' + str(numClusters[value])
-            ax.scatter([numClusters[value]], [obValues[value]], label=label, color='green')
-
-        # Configure the figure
-        ax.legend()
-        ax.set_xlabel('Number of clusters')
-        ax.set_ylabel('Objective values')
-        ax.set_title('Objective values for each number of clusters')
-        ax.set_ylim([np.min(obValues), np.max(obValues)])
-        
diff --git a/xmipp3/viewers/viewer_dose_analysis.py b/xmipp3/viewers/viewer_dose_analysis.py
new file mode 100644
index 000000000..a2fc50cf3
--- /dev/null
+++ b/xmipp3/viewers/viewer_dose_analysis.py
@@ -0,0 +1,113 @@
+# **************************************************************************
+# *
+# * Authors:     Daniel Marchán Torres (da.marchan@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow.viewer import Viewer, DESKTOP_TKINTER, WEB_DJANGO
+from pyworkflow.protocol.params import LabelParam
+from pwem.viewers import showj, EmProtocolViewer, ObjectView
+from pwem.viewers.showj import MODE, MODE_MD, ORDER, VISIBLE, RENDER
+from xmipp3.protocols.protocol_movie_dose_analysis import XmippProtMovieDoseAnalysis
+import matplotlib.pyplot as plt
+import os
+
+
+
+class XmippMovieDoseAnalysisViewer(EmProtocolViewer):
+    """ This viewer is intended to visualize the selection made by
+        the Xmipp - Movie poisson count protocol.
+    """
+    _label = 'viewer Movie Dose Analysis'
+    _environments = [DESKTOP_TKINTER, WEB_DJANGO]
+    _targets = [XmippProtMovieDoseAnalysis]
+
+
+    def _defineParams(self, form):
+        form.addSection(label='Visualization')
+        form.addParam('visualizeMovies', LabelParam,
+                      label="Visualize accepted movies",
+                      help="Visualize movies with the respective dose scores.")
+        form.addParam('visualizeDiscardedMovies', LabelParam,
+                      label="Visualize discarded movies",
+                      help="Visualize discarded movies with the respective dose scores.")
+        form.addParam('visualizeDoseVsTime', LabelParam,
+                      label="Visualize Dose vs Time",
+                      help="Visualize plot dose vs time.")
+        form.addParam('visualizeDoseDiffVsTime', LabelParam,
+                      label="Visualize Dose difference vs Time",
+                      help="Visualize plot dose difference against median dose vs time.")
+
+    def _getVisualizeDict(self):
+        return {
+                 'visualizeMovies': self._visualizeMoviesF,
+                 'visualizeDiscardedMovies' : self._visualizeDiscardedMoviesF,
+                 'visualizeDoseVsTime': self._visualizeDoseTime,
+                 'visualizeDoseDiffVsTime': self._visualizeDoseDiffTime
+                }
+
+    def _visualizeMoviesF(self, e=None):
+        return self._visualizeMovies("outputMovies")
+
+    def _visualizeDiscardedMoviesF(self, e=None):
+        return self._visualizeMovies("outputMoviesDiscarded")
+
+    def _visualizeDoseTime(self, e=None):
+        if os.path.exists(self.protocol.getDosePlot()):
+            image = plt.imread(self.protocol.getDosePlot())
+            plt.figure()
+            fig = plt.imshow(image)
+            fig.axes.get_xaxis().set_visible(False)
+            fig.axes.get_yaxis().set_visible(False)
+            plt.show()
+
+    def _visualizeDoseDiffTime(self, e=None):
+        if os.path.exists(self.protocol.getDoseDiffPlot()):
+            image = plt.imread(self.protocol.getDoseDiffPlot())
+            plt.figure()
+            fig = plt.imshow(image)
+            fig.axes.get_xaxis().set_visible(False)
+            fig.axes.get_yaxis().set_visible(False)
+            plt.show()
+
+    def _visualizeMovies(self, objName):
+        views = []
+
+        labels = 'id _filename _samplingRate _acquisition._dosePerFrame ' \
+                 '_acquisition._doseInitial _MEAN_DOSE_PER_ANGSTROM2 _STD_DOSE_PER_ANGSTROM2 ' \
+                 '_DIFF_TO_DOSE_PER_ANGSTROM2 '
+
+        if self.protocol.hasAttribute(objName):
+            setMovies = getattr(self.protocol, objName)
+            views.append(ObjectView(
+                self._project, setMovies.getObjId(), setMovies.getFileName(),
+                viewParams={MODE: MODE_MD, ORDER: labels, VISIBLE: labels})) # FIXME: DO NOT RENDER THE FILENAME
+        else:
+            self.infoMessage('%s does not have %s%s'
+                             % (self.protocol.getObjLabel(), objName,
+                                getStringIfActive(self.protocol)),
+                             title='Info message').show()
+        return views
+
+def getStringIfActive(prot):
+    return ', yet.' if prot.isActive() else '.'
\ No newline at end of file
diff --git a/xmipp3/viewers/viewer_metaprotocol_golden_highres.py b/xmipp3/viewers/viewer_metaprotocol_golden_highres.py
index 29642e2e6..264404d08 100644
--- a/xmipp3/viewers/viewer_metaprotocol_golden_highres.py
+++ b/xmipp3/viewers/viewer_metaprotocol_golden_highres.py
@@ -31,11 +31,10 @@
     IntParam, FloatParam
 from pyworkflow.protocol.constants import LEVEL_ADVANCED
 from pyworkflow.viewer import DESKTOP_TKINTER, WEB_DJANGO, ProtocolViewer
-from pwem.viewers import ObjectView, DataView, ChimeraClientView
+from pwem.viewers import ObjectView, DataView
 import pwem.viewers.showj as showj
+from pwem import emlib
 
-from xmippLib import (MDL_SAMPLINGRATE, MDL_ANGLE_ROT, MDL_ANGLE_TILT,
-                      MDL_RESOLUTION_FREQ, MDL_RESOLUTION_FRC, MetaData)
 from xmipp3.protocols.protocol_metaprotocol_golden_highres import \
     XmippMetaProtGoldenHighRes
 from .plotter import XmippPlotter
@@ -123,9 +122,9 @@ def _showFSCLoc(self, paramName=None):
 
 
     def _plotFSC(self, ab, fnFSC):
-        md = MetaData(fnFSC)
-        resolution_inv = [md.getValue(MDL_RESOLUTION_FREQ, f) for f in md]
-        frc = [md.getValue(MDL_RESOLUTION_FRC, f) for f in md]
+        md = emlib.MetaData(fnFSC)
+        resolution_inv = [md.getValue(emlib.MDL_RESOLUTION_FREQ, f) for f in md]
+        frc = [md.getValue(emlib.MDL_RESOLUTION_FRC, f) for f in md]
         # self.maxFrc = max(frc)
         self.minInv.append(min(resolution_inv))
         self.maxInv.append(max(resolution_inv))
diff --git a/xmipp3/viewers/viewer_projmatch.py b/xmipp3/viewers/viewer_projmatch.py
index 4505deb10..57caee6f4 100644
--- a/xmipp3/viewers/viewer_projmatch.py
+++ b/xmipp3/viewers/viewer_projmatch.py
@@ -32,7 +32,7 @@
 
 from pyworkflow.protocol.executor import StepExecutor
 from pyworkflow.viewer import ProtocolViewer, DESKTOP_TKINTER, WEB_DJANGO
-from pwem.viewers import (DataView, EmPlotter, showj, ChimeraClientView,
+from pwem.viewers import (DataView, EmPlotter, showj,
                           ChimeraView, ObjectView, ChimeraAngDist)
 import pwem.objects as emobj
 from pyworkflow.utils import createUniqueFileName, cleanPattern, cleanPath
@@ -41,9 +41,10 @@
                                         StringParam, EnumParam,
                                         NumericRangeParam, BooleanParam)
 import pyworkflow.utils as pwutils
+
 from xmipp3.convert import *
 from xmipp3.viewers.plotter import XmippPlotter
-from xmipp3.protocols import XmippProtProjMatch
+from ..protocols.protocol_projmatch import XmippProtProjMatch
 
 
 ITER_LAST = 0
@@ -323,8 +324,7 @@ def _showVolumesChimera(self, volumes):
             view = ChimeraView(cmdFile)
         else:
             
-            #view = CommandView('xmipp_chimera_client --input "%s" --mode projector 256 &' % volumes[0])
-            view = ChimeraClientView(volumes[0], showProjection=True)
+            view = ChimeraView(volumes[0])
         
         return [view]
     
diff --git a/xmipp3/viewers/viewer_resolution_fso.py b/xmipp3/viewers/viewer_resolution_fso.py
index 9dfd530fd..e9b7786a2 100644
--- a/xmipp3/viewers/viewer_resolution_fso.py
+++ b/xmipp3/viewers/viewer_resolution_fso.py
@@ -323,12 +323,25 @@ def _plotCurveAnisotropy(self, fnmd, title, xTitle, yTitle, mdLabelX, mdLabelY1,
         res_01, okToPlot_01 = self.interpolRes(0.1, xx, yy)
         res_05, okToPlot_05 = self.interpolRes(0.5, xx, yy)
         res_09, okToPlot_09 = self.interpolRes(0.9, xx, yy)
-        
+
+        textstr = ''
+
         if okToPlot_09:
-           if self.protocol.halfVolumesFile:
-              sampling = self.protocol.inputHalves.get().getSamplingRate()
-           else:
-              sampling = self.protocol.half1.get().getSamplingRate()
+           textstr += str(0.9) + ' --> ' + str("{:.2f}".format(res_09)) + 'A\n'
+        if okToPlot_05:
+           textstr += str(0.5) + ' --> ' + str("{:.2f}".format(res_05)) + 'A\n'
+        if okToPlot_01:
+           textstr += str(0.1) + ' --> ' + str("{:.2f}".format(res_01)) + 'A'
+
+        props = dict(boxstyle='round', facecolor='white')
+        a.axes.text(0.0, 0.0, textstr, fontsize=12, ha="left", va="bottom", bbox=props)
+        
+        if self.protocol.halfVolumesFile:
+           sampling = self.protocol.inputHalves.get().getSamplingRate()
+        else:
+           sampling = self.protocol.half1.get().getSamplingRate()
+
+        if okToPlot_09 and okToPlot_01:
            t = round((2*sampling/(res_01))*len(yyBingham)) + 3
            if t<len(yyBingham):
               for component in range(t, len(yyBingham)-1):
@@ -337,13 +350,10 @@ def _plotCurveAnisotropy(self, fnmd, title, xTitle, yTitle, mdLabelX, mdLabelY1,
         else:
            a.axes.plot(xx, yyBingham, 'r--')
 
-        if (okToPlot_01 and okToPlot_05 and okToPlot_09):
-            textstr = str(0.9) + ' --> ' + str("{:.2f}".format(res_09)) + 'A\n' + str(0.5) + ' --> ' + str(
-                "{:.2f}".format(res_05)) + 'A\n' + str(0.1) + ' --> ' + str("{:.2f}".format(res_01)) + 'A'
-            a.axes.axvspan(1.0 / res_09, 1.0 / res_01, alpha=0.3, color='green')
+        if not okToPlot_01:
+           res_01 = 2*sampling
 
-            props = dict(boxstyle='round', facecolor='white')
-            a.axes.text(0.0, 0.0, textstr, fontsize=12, ha="left", va="bottom", bbox=props)
+        a.axes.axvspan(1.0 / res_09, 1.0 / res_01, alpha=0.3, color='green')
 
         return plt.show()
 
diff --git a/xmipp3/viewers/viewer_resolution_monogenic_signal.py b/xmipp3/viewers/viewer_resolution_monogenic_signal.py
index 55fabb233..dc8009cd8 100644
--- a/xmipp3/viewers/viewer_resolution_monogenic_signal.py
+++ b/xmipp3/viewers/viewer_resolution_monogenic_signal.py
@@ -207,9 +207,12 @@ def _plotHistogram(self, param=None):
         _plotter = EmPlotter()
         _plotter.createSubPlot("Resolutions Histogram",
                                "Resolution (A)", "# of Counts")
-        barwidth = (x_axis[-1] - x_axis[0]) / len(x_axis)
-
-        _plotter.plotDataBar(x_axis[:-2], y_axis[:-2], barwidth)
+        if len(x_axis) == 1:
+            barwidth = 0.1
+            _plotter.plotDataBar(x_axis, y_axis, barwidth)
+        else:
+            barwidth = (x_axis[-1] - x_axis[0]) / len(x_axis)
+            _plotter.plotDataBar(x_axis[:-2], y_axis[:-2], barwidth)
 
         return [_plotter]
 
diff --git a/xmipp3/viewers/viewer_structure_map.py b/xmipp3/viewers/viewer_structure_map.py
index b393be0b0..a1af5054d 100644
--- a/xmipp3/viewers/viewer_structure_map.py
+++ b/xmipp3/viewers/viewer_structure_map.py
@@ -99,7 +99,10 @@ def _visualize(self, e=None):
 
         self.coordinates = (np.loadtxt(file) for file in fnOutput)
         self.coordinates = np.vstack(self.coordinates)
-        labels = [str(idp) for idp in range(1, self.coordinates.shape[0] + 1)]
+        labels = [v.getObjLabel() for v in self.protocol.inputVolumes.get()]
+        if '' in labels:
+                labels = [str(idp) for idp in
+                          range(1, self.coordinates.shape[0] + 1)]
         if os.path.isfile(self._getExtraPath('weigths.txt')):
             weights = np.loadtxt(self._getExtraPath('weigths.txt'))
         else:
diff --git a/xmipp3/wizards.py b/xmipp3/wizards.py
index 09ccee0d9..cea61d331 100644
--- a/xmipp3/wizards.py
+++ b/xmipp3/wizards.py
@@ -35,13 +35,13 @@
 from .protocols.protocol_cl2d import IMAGES_PER_CLASS
 
 from .protocols import (
-    XmippProtCTFMicrographs, XmippProtProjMatch, XmippProtPreprocessParticles,
+    XmippProtCTFMicrographs, XmippProtPreprocessParticles, XmippProtProjMatch,
     XmippProtPreprocessMicrographs, XmippProtPreprocessVolumes,
     XmippProtExtractParticles, XmippProtExtractParticlesPairs, XmippProtPickingRemoveDuplicates,
     XmippProtFilterParticles, XmippProtFilterVolumes, XmippProtMaskParticles,
     XmippProtMaskVolumes, XmippProtAlignVolume, XmippProtCL2D,
     XmippProtHelicalParameters, XmippProtConsensusPicking, XmippProtMonoRes,
-    XmippProtRotSpectra, XmippProtReconstructHighRes, XmippProtExtractUnit,
+    XmippProtReconstructHighRes, XmippProtExtractUnit,
     XmippProtShiftParticles, XmippProtVolumeDeformZernike3D, XmippProtStructureMapZernike3D,
     XmippProtSubtractProjection, XmippProtBoostParticles)
 
@@ -157,7 +157,10 @@ class XmippParticleConsensusRadiusWizard(Wizard):
 
     def _getRadius(self, protocol):
         if protocol.inputCoordinates.hasValue():
-            boxSize=protocol.inputCoordinates[0].get().getBoxSize()
+            if protocol.inputCoordinates.get() == None:  # Protocol=XmippProtConsensusPicking => inputCoordinates=list
+                boxSize = protocol.inputCoordinates[0].get().getBoxSize()
+            else:  # Protocol=XmippProtPickingRemoveDuplicates => inputCoordinates=setOfCoordinates
+                boxSize = protocol.inputCoordinates.get().getBoxSize()
             radius = int(boxSize*0.1)
             if radius<10:
                 radius=10
@@ -220,8 +223,7 @@ def show(self, form):
 
 
 class XmippParticleMaskRadiiWizard(ParticlesMaskRadiiWizard):
-    _targets = [(XmippProtMaskParticles, ['innerRadius', 'outerRadius']),
-                (XmippProtRotSpectra, ['spectraInnerRadius', 'spectraOuterRadius'])]
+    _targets = [(XmippProtMaskParticles, ['innerRadius', 'outerRadius'])]
 
     def _getParameters(self, protocol):
 
@@ -313,7 +315,6 @@ def _getParameters(self, protocol):
         protParams['value']= protocol.cylinderInnerRadius.get()
         return protParams
 
-
 class XmippVolumeMaskRadiusProjMWizard(XmippVolumeMaskRadiusWizard):
     _targets = [(XmippProtProjMatch, ['maskRadius'])]
 
@@ -328,6 +329,8 @@ def _getParameters(self, protocol):
         return protParams
 
 
+
+
 class XmippVolumeRadiiWizard(VolumeMaskRadiiWizard):
     _targets = [(XmippProtMaskVolumes, ['innerRadius', 'outerRadius']),
                (XmippProtExtractUnit, ['innerRadius', 'outerRadius'])]
@@ -352,6 +355,7 @@ def show(self, form):
         _label = params['label']
         VolumeMaskRadiiWizard.show(self, form, _value, _label, UNIT_PIXEL)
 
+
 class XmippVolumeRadiiProjMWizard(XmippVolumeRadiiWizard):
     _targets = [(XmippProtProjMatch, ['innerRadius', 'outerRadius'])]
 


Test patch:
diff --git a/xmipp3/tests/test_protocol_deep_center.py b/xmipp3/tests/test_protocol_deep_center.py
new file mode 100644
index 000000000..ec74959fa
--- /dev/null
+++ b/xmipp3/tests/test_protocol_deep_center.py
@@ -0,0 +1,109 @@
+# **************************************************************************
+# *
+# * Authors:    Adrian Sansinena Rodriguez  (adrian.sansinena@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow.tests import BaseTest, DataSet, setupTestProject
+
+from pwem.protocols import (ProtImportParticles, ProtSubSet,
+                            exists)
+
+from pwem import emlib
+from xmipp3.protocols import XmippProtDeepCenter
+
+
+class TestDeepCenter(BaseTest):
+    @classmethod
+    def runImportParticles(cls):
+        """ Import Particles.
+        """
+        args = {'importFrom': ProtImportParticles.IMPORT_FROM_SCIPION,
+                'sqliteFile': cls.particles,
+                'amplitudConstrast': 0.1,
+                'sphericalAberration': 2.0,
+                'voltage': 200,
+                'samplingRate': 0.99,
+                'haveDataBeenPhaseFlipped': True
+                }
+
+        # Id's should be set increasing from 1 if ### is not in the
+        # pattern
+        protImport = cls.newProtocol(ProtImportParticles, **args)
+        protImport.setObjLabel('import particles')
+        cls.launchProtocol(protImport)
+        return protImport
+
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+
+        # Data
+        cls.dataset = DataSet.getDataSet('10010')
+        cls.particles = cls.dataset.getFile('particles')
+
+        cls.protImportParts = cls.runImportParticles()
+
+    def test(self):
+        subset = self.newProtocol(ProtSubSet,
+                                  inputFullSet=self.protImportParts.outputParticles,
+                                  chooseAtRandom=True,
+                                  nElements=400)
+        self.launchProtocol(subset)
+
+        deepCenter = self.newProtocol(XmippProtDeepCenter,
+                                   inputTrainSet=subset.outputParticles,
+                                   Xdim=128,
+                                   modelPretrain=False,
+                                   numCenModels=5,
+                                   numEpochs_cen=1,
+                                   batchSize=32,
+                                   learningRate=0.001,
+                                   sigma=8,
+                                   patience=5)
+        self.launchProtocol(deepCenter)
+
+        fnModel0 = deepCenter._getExtraPath("modelCenter0.h5")
+        fnModel2 = deepCenter._getExtraPath("modelCenter2.h5")
+
+        if not exists(fnModel0):
+            self.assertTrue(False, fnModel0 + " does not exist")
+        if not exists(fnModel2):
+            self.assertTrue(False, fnModel2 + " does not exist")
+
+        deepCenter2 = self.newProtocol(XmippProtDeepCenter,
+                                      inputTrainSet=subset.outputParticles,
+                                      Xdim=128,
+                                      modelPretrain=True,
+                                      pretrainedModels=deepCenter,
+                                      numCenModels=5,
+                                      numEpochs_cen=10,
+                                      batchSize=32,
+                                      learningRate=0.001,
+                                      sigma=8,
+                                      patience=5)
+        self.launchProtocol(deepCenter2)
+
+        fnModel0 = deepCenter2._getExtraPath("modelCenter0.h5")
+        if not exists(fnModel0):
+            self.assertTrue(False, fnModel0 + " does not exist")
\ No newline at end of file
diff --git a/xmipp3/tests/test_protocol_deep_center_predict.py b/xmipp3/tests/test_protocol_deep_center_predict.py
new file mode 100644
index 000000000..a08b2bed2
--- /dev/null
+++ b/xmipp3/tests/test_protocol_deep_center_predict.py
@@ -0,0 +1,95 @@
+# **************************************************************************
+# *
+# * Authors:    Adrian Sansinena Rodriguez  (adrian.sansinena@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow.tests import BaseTest, DataSet, setupTestProject
+
+from pwem.protocols import (ProtImportParticles, ProtSubSet,
+                            exists)
+
+from pwem import emlib
+from xmipp3.protocols import XmippProtDeepCenterPredict, XmippProtDeepCenter
+
+
+class TestDeepCenterPredict(BaseTest):
+    @classmethod
+    def runImportParticles(cls):
+        """ Import Particles.
+        """
+        args = {'importFrom': ProtImportParticles.IMPORT_FROM_SCIPION,
+                'sqliteFile': cls.particles,
+                'amplitudConstrast': 0.1,
+                'sphericalAberration': 2.0,
+                'voltage': 200,
+                'samplingRate': 0.99,
+                'haveDataBeenPhaseFlipped': True
+                }
+
+        protImport = cls.newProtocol(ProtImportParticles, **args)
+        protImport.setObjLabel('import particles')
+        cls.launchProtocol(protImport)
+        return protImport
+
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+
+        # Data
+        cls.dataset = DataSet.getDataSet('10010')
+        cls.particles = cls.dataset.getFile('particles')
+
+        cls.protImportParts = cls.runImportParticles()
+
+    def test(self):
+        subset = self.newProtocol(ProtSubSet,
+                                  inputFullSet=self.protImportParts.outputParticles,
+                                  chooseAtRandom=True,
+                                  nElements=400)
+        self.launchProtocol(subset)
+
+        deepCenter = self.newProtocol(XmippProtDeepCenter,
+                                   inputTrainSet=subset.outputParticles,
+                                   Xdim=128,
+                                   modelPretrain=False,
+                                   numCenModels=5,
+                                   numEpochs_cen=1,
+                                   batchSize=32,
+                                   learningRate=0.001,
+                                   sigma=8,
+                                   patience=5)
+        self.launchProtocol(deepCenter)
+
+        deepCenterPredict = self.newProtocol(XmippProtDeepCenterPredict,
+                                      inputImageSet=subset.outputParticles,
+                                      Xdim=128,
+                                      inputModel=deepCenter,
+                                      numModels=5,
+                                      tolerance=2,
+                                      maxModels=1)
+        self.launchProtocol(deepCenterPredict)
+
+        self.assertIsNotNone(deepCenterPredict.outputParticles,
+                             "There was a problem with Deep Center Predict")
+
diff --git a/xmipp3/tests/test_protocol_deep_global_assignment.py b/xmipp3/tests/test_protocol_deep_global_assignment.py
new file mode 100644
index 000000000..84a89a4b9
--- /dev/null
+++ b/xmipp3/tests/test_protocol_deep_global_assignment.py
@@ -0,0 +1,109 @@
+# **************************************************************************
+# *
+# * Authors:    Adrian Sansinena Rodriguez  (adrian.sansinena@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow.tests import BaseTest, DataSet, setupTestProject
+
+from pwem.protocols import (ProtImportParticles, ProtSubSet,
+                            exists)
+
+from pwem import emlib
+from xmipp3.protocols import XmippProtDeepGlobalAssignment
+
+
+class TestDeepGlobalAssignment(BaseTest):
+    @classmethod
+    def runImportParticles(cls):
+        """ Import Particles.
+        """
+        args = {'importFrom': ProtImportParticles.IMPORT_FROM_SCIPION,
+                'sqliteFile': cls.particles,
+                'amplitudConstrast': 0.1,
+                'sphericalAberration': 2.0,
+                'voltage': 200,
+                'samplingRate': 0.99,
+                'haveDataBeenPhaseFlipped': True
+                }
+
+        # Id's should be set increasing from 1 if ### is not in the
+        # pattern
+        protImport = cls.newProtocol(ProtImportParticles, **args)
+        protImport.setObjLabel('import particles')
+        cls.launchProtocol(protImport)
+        return protImport
+
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+
+        # Data
+        cls.dataset = DataSet.getDataSet('10010')
+        cls.particles = cls.dataset.getFile('particles')
+
+        cls.protImportParts = cls.runImportParticles()
+
+    def test(self):
+        subset = self.newProtocol(ProtSubSet,
+                                  inputFullSet=self.protImportParts.outputParticles,
+                                  chooseAtRandom=True,
+                                  nElements=400)
+        self.launchProtocol(subset)
+
+        deepGA = self.newProtocol(XmippProtDeepGlobalAssignment,
+                                   inputTrainSet=subset.outputParticles,
+                                   Xdim=128,
+                                   modelPretrain=False,
+                                   numAngModels=5,
+                                   numEpochs_ang=1,
+                                   batchSize=32,
+                                   learningRate=0.001,
+                                   sigma=8,
+                                   patience=5)
+        self.launchProtocol(deepGA)
+
+        fnModel0 = deepGA._getExtraPath("modelAngular0.h5")
+        fnModel2 = deepGA._getExtraPath("modelAngular2.h5")
+
+        if not exists(fnModel0):
+            self.assertTrue(False, fnModel0 + " does not exist")
+        if not exists(fnModel2):
+            self.assertTrue(False, fnModel2 + " does not exist")
+
+        deepGA2 = self.newProtocol(XmippProtDeepGlobalAssignment,
+                                      inputTrainSet=subset.outputParticles,
+                                      Xdim=128,
+                                      modelPretrain=True,
+                                      pretrainedModels=deepGA,
+                                      numAngModels=5,
+                                      numEpochs_ang=10,
+                                      batchSize=32,
+                                      learningRate=0.001,
+                                      sigma=8,
+                                      patience=5)
+        self.launchProtocol(deepGA2)
+
+        fnModel0 = deepGA2._getExtraPath("modelAngular0.h5")
+        if not exists(fnModel0):
+            self.assertTrue(False, fnModel0 + " does not exist")
\ No newline at end of file
diff --git a/xmipp3/tests/test_protocol_deep_global_assignment_predict.py b/xmipp3/tests/test_protocol_deep_global_assignment_predict.py
new file mode 100644
index 000000000..2a41fd181
--- /dev/null
+++ b/xmipp3/tests/test_protocol_deep_global_assignment_predict.py
@@ -0,0 +1,94 @@
+# **************************************************************************
+# *
+# * Authors:    Adrian Sansinena Rodriguez  (adrian.sansinena@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+
+from pyworkflow.tests import BaseTest, DataSet, setupTestProject
+
+from pwem.protocols import (ProtImportParticles, ProtSubSet,
+                            exists)
+
+from pwem import emlib
+from xmipp3.protocols import XmippProtDeepGlobalAssignmentPredict, XmippProtDeepGlobalAssignment
+
+
+class TestDeepGlobalAssignmentPredict(BaseTest):
+    @classmethod
+    def runImportParticles(cls):
+        """ Import Particles.
+        """
+        args = {'importFrom': ProtImportParticles.IMPORT_FROM_SCIPION,
+                'sqliteFile': cls.particles,
+                'amplitudConstrast': 0.1,
+                'sphericalAberration': 2.0,
+                'voltage': 200,
+                'samplingRate': 0.99,
+                'haveDataBeenPhaseFlipped': True
+                }
+
+        protImport = cls.newProtocol(ProtImportParticles, **args)
+        protImport.setObjLabel('import particles')
+        cls.launchProtocol(protImport)
+        return protImport
+
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+
+        # Data
+        cls.dataset = DataSet.getDataSet('10010')
+        cls.particles = cls.dataset.getFile('particles')
+
+        cls.protImportParts = cls.runImportParticles()
+
+    def test(self):
+        subset = self.newProtocol(ProtSubSet,
+                                  inputFullSet=self.protImportParts.outputParticles,
+                                  chooseAtRandom=True,
+                                  nElements=400)
+        self.launchProtocol(subset)
+
+        deepGA = self.newProtocol(XmippProtDeepGlobalAssignment,
+                                  inputTrainSet=subset.outputParticles,
+                                  Xdim=128,
+                                  modelPretrain=False,
+                                  numAngModels=5,
+                                  numEpochs_ang=1,
+                                  batchSize=32,
+                                  learningRate=0.001,
+                                  sigma=8,
+                                  patience=5)
+        self.launchProtocol(deepGA)
+
+        deepGAPredict = self.newProtocol(XmippProtDeepGlobalAssignmentPredict,
+                                      inputImageSet=subset.outputParticles,
+                                      Xdim=128,
+                                      inputModel=deepGA,
+                                      numAngModels=5,
+                                      tolerance=45,
+                                      maxModels=3)
+        self.launchProtocol(deepGAPredict)
+
+        self.assertIsNotNone(deepGAPredict.outputParticles,
+                             "There was a problem with Deep Center Predict")
\ No newline at end of file
diff --git a/xmipp3/tests/test_protocol_simulate_ctf.py b/xmipp3/tests/test_protocol_simulate_ctf.py
index 9f09874d3..eb7c385ed 100644
--- a/xmipp3/tests/test_protocol_simulate_ctf.py
+++ b/xmipp3/tests/test_protocol_simulate_ctf.py
@@ -32,7 +32,7 @@
 from pwem.protocols import ProtImportVolumes
 from xmipp3.protocols import XmippProtSimulateCTF, XmippProtCreateGallery
 
-
+OUTPARTERROR = "There was a problem with particles output"
 class TestXmippProtSimulateCTF(BaseTest):
     """ Testing CTF simulation
     """
@@ -72,6 +72,17 @@ def runSimulateCTF(self, projections, defocus0=5000, defocusF=25000):
         self.launchProtocol(protSimulateCTF)
         return protSimulateCTF.outputParticles
 
+    def runSimulateCTFAstig(self, projections, defocus0=5000, defocusF=25000):
+        protSimulateCTFAstig = self.newProtocol(XmippProtSimulateCTF,
+                                           inputParticles=projections,
+                                           Defocus0=defocus0,
+                                           DefocusF=defocusF,
+                                           astig=True,
+                                           objLabel='Projections with CTF Astig - [%.2f, %.2f]' % (defocus0, defocusF))
+
+        self.launchProtocol(protSimulateCTFAstig)
+        return protSimulateCTFAstig.outputParticles
+
     def testSimulateCTF(self):
         importedVolume = self.runImportVolume(self.volume, 1)
         projectionsGallery = self.runCreateGallery(importedVolume)
@@ -79,9 +90,9 @@ def testSimulateCTF(self):
         # Check default defoci values
         projectionsCTFDefault = self.runSimulateCTF(projectionsGallery)
         self.assertTrue(projectionsCTFDefault,
-                        "There was a problem with particles output")
+                        OUTPARTERROR)
         self.assertEqual(projectionsCTFDefault.getSize(),1647,
-                        "There was a problem with particles output")
+                        OUTPARTERROR)
         self.assertEqual(projectionsCTFDefault.getXDim(),60,
                         "Unexpected particle size in output particles")
         self.assertEqual(projectionsCTFDefault.getSamplingRate(),1,
@@ -94,3 +105,36 @@ def testSimulateCTF(self):
                                     '[%f, %f]' % (5000, 25000))
             self.assertLessEqual(defocusU,25000, 'DefocusU and DefocusV values are outside the specified range:'
                                     '[%f, %f]' % (5000, 25000))
+
+
+    def testSimulateCTFAstig(self):
+        importedVolume = self.runImportVolume(self.volume, 1)
+        projectionsGallery = self.runCreateGallery(importedVolume)
+
+        # Check default defoci values
+        projectionsCTFDefaultAstig = self.runSimulateCTFAstig(projectionsGallery)
+        self.assertTrue(projectionsCTFDefaultAstig,
+                        OUTPARTERROR)
+        self.assertEqual(projectionsCTFDefaultAstig.getSize(),1647,
+                        OUTPARTERROR)
+        self.assertEqual(projectionsCTFDefaultAstig.getXDim(),60,
+                        "Unexpected particle size in output particles")
+        self.assertEqual(projectionsCTFDefaultAstig.getSamplingRate(),1,
+                        "Unexpected sampling rate in output particles")
+        for projection in projectionsCTFDefaultAstig.iterItems():
+            defocusU = projection.getCTF().getDefocusU()
+            defocusV = projection.getCTF().getDefocusV()
+            angle = projection.getCTF().getDefocusAngle()
+            self.assertNotEqual(defocusU,defocusV,'DefocusU and DeofcusV are equal but they should be')
+            self.assertGreaterEqual(defocusU,5000, 'DefocusU values are outside the specified range:'
+                                    '[%f, %f]' % (5000, 25000))
+            self.assertLessEqual(defocusU,25000, 'DefocusU values are outside the specified range:'
+                                    '[%f, %f]' % (5000, 25000))
+            self.assertGreaterEqual(defocusV,(5000-500), 'DefocusV values are outside the specified range:'
+                                    '[%f, %f]' % (5000-500, 25000+500))
+            self.assertLessEqual(defocusV,(25000+500), 'DefocusV values are outside the specified range:'
+                                    '[%f, %f]' % (5000-500, 25000+500))
+            self.assertGreaterEqual(angle,40, 'Defocus angle values are outside the specified range:'
+                                    '[%f, %f]' % (40, 50))
+            self.assertLessEqual(angle,50, 'Defocus angle values are outside the specified range:'
+                                    '[%f, %f]' % (40, 50))
diff --git a/xmipp3/tests/test_protocols_angular_resolution_alignment.py b/xmipp3/tests/test_protocols_angular_resolution_alignment.py
new file mode 100644
index 000000000..8d0360ba6
--- /dev/null
+++ b/xmipp3/tests/test_protocols_angular_resolution_alignment.py
@@ -0,0 +1,88 @@
+# **************************************************************************
+# *
+# * Authors:    Jose Luis Vilas Prieto (jlvilas@cnb.csic.es)
+# *
+# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'scipion@cnb.csic.es'
+# *
+# **************************************************************************
+from pwem.protocols import ProtImportVolumes, exists
+from pyworkflow.tests import BaseTest, DataSet, setupTestProject
+
+from xmipp3.protocols import XmippProtResolutionAlignment, XmippProtCreateMask3D
+
+
+class TestResAlignmentBase(BaseTest):
+    @classmethod
+    def setData(cls, dataProject='resmap'):
+        cls.dataset = DataSet.getDataSet(dataProject)
+        cls.map3D = cls.dataset.getFile('betagal')
+        cls.half1 = cls.dataset.getFile('betagal_half1')
+        cls.half2 = cls.dataset.getFile('betagal_half2')
+        cls.mask = cls.dataset.getFile('betagal_mask')
+
+    @classmethod
+    def runImportVolumes(cls, pattern, samplingRate):
+        """ Run an Import volumes protocol. """
+        cls.protImport = cls.newProtocol(ProtImportVolumes,
+                                         filesPath=pattern,
+                                         samplingRate=samplingRate
+                                         )
+        cls.launchProtocol(cls.protImport)
+        return cls.protImport
+
+    @classmethod
+    def runCreateMask(cls, pattern, thr):
+        """ Create a volume mask. """
+        cls.msk = cls.newProtocol(XmippProtCreateMask3D,
+                                  inputVolume=pattern,
+                                  volumeOperation=0,  # OPERATION_THRESHOLD,
+                                  threshold=thr,
+                                  doSmall=False,
+                                  smallSize=False,
+                                  doBig=False,
+                                  doSymmetrize=False,
+                                  doMorphological=False,
+                                  doInvert=False,
+                                  doSmooth=False,
+                                  sigmaConvolution=2
+                                  )
+        cls.launchProtocol(cls.msk)
+        return cls.msk
+
+
+class TestResAlignment(TestResAlignmentBase):
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        TestResAlignmentBase.setData()
+        cls.protImportVol = cls.runImportVolumes(cls.map3D, 3.54)
+        cls.protImportHalf1 = cls.runImportVolumes(cls.half1, 3.54)
+        cls.protImportHalf2 = cls.runImportVolumes(cls.half2, 3.54)
+        cls.protCreateMask = cls.runCreateMask(cls.protImportVol.outputVolume, 0.02)
+
+    def testResAlign1(self):
+        resAlign = self.newProtocol(XmippProtResolutionAlignment, halfVolumesFile=False, half1=self.protImportHalf1.outputVolume, half2=self.protImportHalf2.outputVolume,
+                               mask=self.protCreateMask.outputMask)
+        self.launchProtocol(resAlign)
+        self.assertTrue(exists(resAlign._getExtraPath("radial_FSC_resolution.xmd")), "resolution alignment with mask has failed")
+
+
+
diff --git a/xmipp3/tests/test_protocols_continuousflex.py b/xmipp3/tests/test_protocols_continuousflex.py
index 31c515248..68c8ada80 100644
--- a/xmipp3/tests/test_protocols_continuousflex.py
+++ b/xmipp3/tests/test_protocols_continuousflex.py
@@ -28,6 +28,28 @@
                                       FlexProtAlignmentNMA, FlexProtSubtomogramAveraging)
 from continuousflex.protocols.protocol_subtomograms_synthesize import MODE_RELATION_3CLUSTERS
 
+files_dictionary = {'pdb': 'pdb/AK.pdb', 'particles': 'particles/img.stk', 'vol': 'volumes/AK_LP10.vol',
+                    'precomputed_atomic': 'gold/images_WS_atoms.xmd',
+                    'precomputed_pseudoatomic': 'gold/images_WS_pseudoatoms.xmd',
+                    'small_stk': 'test_alignment_10images/particles/smallstack_img.stk',
+                    'subtomograms':'HEMNMA_3D/subtomograms/*.vol',
+                    'precomputed_HEMNMA3D_atoms':'HEMNMA_3D/gold/precomputed_atomic.xmd',
+                    'precomputed_HEMNMA3D_pseudo':'HEMNMA_3D/gold/precomputed_pseudo.xmd',
+
+                    'charmm_prm':'genesis/par_all36_prot.prm',
+                    'charmm_top':'genesis/top_all36_prot.rtf',
+                    '1ake_pdb':'genesis/1ake.pdb',
+                    '1ake_vol':'genesis/1ake.mrc',
+                    '4ake_pdb':'genesis/4ake.pdb',
+                    '4ake_aa_pdb':'genesis/4ake_aa.pdb',
+                    '4ake_aa_psf':'genesis/4ake_aa.psf',
+                    '4ake_ca_pdb':'genesis/4ake_ca.pdb',
+                    '4ake_ca_top':'genesis/4ake_ca.top',
+                    }
+DataSet(name='nma_V2.0', folder='nma_V2.0', files=files_dictionary,
+        url='https://raw.githubusercontent.com/continuousflex-org/testdata-continuousflex/main')
+
+
 
 class TestContinuousFlexBasics(TestWorkflow):
     """ Test protocol for HEMNMA, HEMNMA-3D, StructMap, TomoFlow and STA backend functions. """
diff --git a/xmipp3/tests/test_protocols_local_defocus.py b/xmipp3/tests/test_protocols_local_defocus.py
index 7643f868a..bd19bd5d8 100644
--- a/xmipp3/tests/test_protocols_local_defocus.py
+++ b/xmipp3/tests/test_protocols_local_defocus.py
@@ -52,7 +52,8 @@ def testXmippLocalDefocus(self):
 
         protEstimateLocalDefocus = self.newProtocol(XmippProtLocalCTF,
                                                     inputSet=protSimulateCTF.outputParticles,
-                                                    inputVolume=self.protCreatePhantom.outputVolume)
+                                                    inputVolume=self.protCreatePhantom.outputVolume,
+                                                    sameDefocus=True)
         self.launchProtocol(protEstimateLocalDefocus)
         self.assertIsNotNone(protEstimateLocalDefocus.outputParticles.getFiles(),
                              "There was a problem with local CTF estimation")
@@ -70,7 +71,8 @@ class TestXmippLocalDefocusEstimationConsensus(TestXmippLocalDefocusEstimation):
 
     def testXmippLocalDefocusConsensus(self):
         protSimulateCTF2 = self.newProtocol(XmippProtSimulateCTF,
-                                            inputParticles=self.protCreateGallery.outputReprojections)
+                                            inputParticles=self.protCreateGallery.outputReprojections,
+                                            astig=True)
         self.launchProtocol(protSimulateCTF2)
         self.assertIsNotNone(protSimulateCTF2.outputParticles, "There was a problem with second CTF simulation")
 
@@ -82,7 +84,8 @@ def testXmippLocalDefocusConsensus(self):
                              "There was a problem with second CTF estimation")
 
         protSimulateCTF3 = self.newProtocol(XmippProtSimulateCTF,
-                                            inputParticles=self.protCreateGallery.outputReprojections)
+                                            inputParticles=self.protCreateGallery.outputReprojections,
+                                            astig=True)
         self.launchProtocol(protSimulateCTF3)
         self.assertIsNotNone(protSimulateCTF3.outputParticles, "There was a problem with third CTF simulation")
 
diff --git a/xmipp3/tests/test_protocols_xmipp_2d.py b/xmipp3/tests/test_protocols_xmipp_2d.py
index 3222f16e1..9d4241d33 100644
--- a/xmipp3/tests/test_protocols_xmipp_2d.py
+++ b/xmipp3/tests/test_protocols_xmipp_2d.py
@@ -1141,33 +1141,6 @@ def _checkTranslationScaling(self, protAssign, protParticles, shiftsAppliedBefor
         [self.assertAlmostEqual(inT * scale, outT) for inT, outT in zip(inTranslation, outTranslation)]
 
 
-class TestXmippRotSpectra(TestXmippBase):
-    """This class check if the protocol to calculate the rotational spectra from particles in Xmipp works properly."""
-    @classmethod
-    def setUpClass(cls):
-        setupTestProject(cls)
-        TestXmippBase.setData('mda')
-        cls.protImport = cls.runImportParticles(cls.particlesFn, 3.5)
-        cls.align2D = cls.runCL2DAlign(cls.protImport.outputParticles)
-         
-    def test_rotSpectra(self):
-        print("Run Rotational Spectra")
-        xmippProtRotSpectra = self.newProtocol(XmippProtRotSpectra, SomXdim=2, SomYdim=2)
-        xmippProtRotSpectra.inputParticles.set(self.align2D.outputParticles)
-        self.launchProtocol(xmippProtRotSpectra)        
-        self.assertIsNotNone(xmippProtRotSpectra.outputClasses, "There was a problem with Rotational Spectra")
-
-    def test_rotSpectraMask(self):
-        print("Run Rotational Spectra with Mask")
-        protMask = self.runCreateMask(3.5, 100)
-        xmippProtRotSpectra = self.newProtocol(XmippProtRotSpectra, useMask=True, SomXdim=2, SomYdim=2)
-        xmippProtRotSpectra.inputParticles.set(self.align2D.outputParticles)
-        xmippProtRotSpectra.useMask.set(True)
-        xmippProtRotSpectra.Mask.set(protMask.outputMask)
-        self.launchProtocol(xmippProtRotSpectra)
-        self.assertIsNotNone(xmippProtRotSpectra.outputClasses, "There was a problem with Rotational Spectra")
-
-
 class TestXmippKerdensom(TestXmippBase):
     """This class check if the protocol to calculate the kerdensom from particles in Xmipp works properly."""
     @classmethod
diff --git a/xmipp3/tests/test_protocols_xmipp_3d.py b/xmipp3/tests/test_protocols_xmipp_3d.py
index 5f1cb6348..634357ae2 100644
--- a/xmipp3/tests/test_protocols_xmipp_3d.py
+++ b/xmipp3/tests/test_protocols_xmipp_3d.py
@@ -25,18 +25,19 @@
 # *
 # **************************************************************************
 
+# General imports
+import os.path, shutil
+
+# Scipion em imports
+from pwem.convert import Ccp4Header
 from pwem.protocols import (ProtImportVolumes, ProtImportMask,
                             ProtImportParticles, ProtImportAverages,
-                            ProtImportPdb, ProtSubSet)
-from xmipp3.protocols.protocol_align_volume_and_particles import AlignVolPartOutputs
-
-try:
-    from itertools import izip
-except ImportError:
-    izip = zip
-
+                            ProtImportPdb, ProtSubSet, ProtImportSetOfAtomStructs)
 from pyworkflow.utils import greenStr, magentaStr
 from pyworkflow.tests import *
+
+# Plugin imports
+from xmipp3.protocols.protocol_align_volume_and_particles import AlignVolPartOutputs
 from xmipp3.base import *
 from xmipp3.convert import *
 from xmipp3.constants import *
@@ -69,6 +70,8 @@
 MSG_WRONG_SHIFT = "There was a problem with output shift "
 MSG_WRONG_GALLERY = "There was a problem with the gallery creation"
 MSG_WRONG_ROTATION = "There was a problem with the rotation"
+MSG_WRONG_RECONSTRUCTION = "There was a problem with the reconstruction"
+MSG_WRONG_MERGER = "There was a problem with the merger of the "
 MSG_WRONG_IMPORT = "There was a problem with the import of "
 MSG_WRONG_PROTOCOL = "There was a problem with the protocol: "
 MSG_WRONG_MAP = "There was a problem with the map creation"
@@ -1032,7 +1035,6 @@ def test_rotsym(self):
         self.assertIsNotNone(protRotSym.outputVolume,
                              "There was a problem with Rotational Symmetry")
 
-
 class TestXmippProjMatching(TestXmippBase):
 
     @classmethod
@@ -1053,7 +1055,7 @@ def testXmippProjMatching(self):
                                  )
         self.launchProtocol(protImportParts)
         self.assertIsNotNone(protImportParts.getFiles(), "There was a problem with the import")
-        
+
         print("Get a Subset of particles")
         protSubset = self.newProtocol(ProtSubSet,
                                          objLabel='100 Particles',
@@ -1061,7 +1063,7 @@ def testXmippProjMatching(self):
                                          nElements=100)
         protSubset.inputFullSet.set(protImportParts.outputParticles)
         self.launchProtocol(protSubset)
-        
+
         print("Import Volume")
         protImportVol = self.newProtocol(ProtImportVolumes,
                                          objLabel='Volume',
@@ -1069,7 +1071,7 @@ def testXmippProjMatching(self):
                                          samplingRate=7.08)
         self.launchProtocol(protImportVol)
         self.assertIsNotNone(protImportVol.getFiles(), "There was a problem with the import")
-        
+
         print("Run Projection Matching")
         protProjMatch = self.newProtocol(XmippProtProjMatch,
                                          ctfGroupMaxDiff=0.00001,
@@ -1082,7 +1084,6 @@ def testXmippProjMatching(self):
         self.launchProtocol(protProjMatch)
         self.assertIsNotNone(protProjMatch.outputVolume, "There was a problem with Projection Matching")
 
-
 class TestPdbImport(TestXmippBase):
     
     @classmethod
@@ -1109,52 +1110,88 @@ def testImportPdbFromFn(self):
 
 
 class TestXmippPdbConvert(TestXmippBase):
-    
     @classmethod
     def setUpClass(cls):
+        """ This function prepares the project before instantiating and running any protocol. """
         setupTestProject(cls)
         cls.dataset = DataSet.getDataSet('nma')
         cls.pdb = cls.dataset.getFile('pdb')
     
-    def testXmippPdbConvertFromDb(self):
-        print("Run convert a pdb from database")
-        protConvert = self.newProtocol(XmippProtConvertPdb, pdbId="3j3i", sampling=4, setSize=True,
-                                       size_z=100, size_y=100, size_x=100)
-        self.launchProtocol(protConvert)
-        self.assertIsNotNone(protConvert.outputVolume.getFileName(), "There was a problem with the conversion")
-        self.assertAlmostEqual(protConvert.outputVolume.getSamplingRate(), protConvert.sampling.get(), places=1,
-                               msg=(MSG_WRONG_SAMPLING, "volume"))
-        self.assertAlmostEqual(protConvert.outputVolume.getDim()[0], protConvert.size_z.get(), places=1,
-                               msg=(MSG_WRONG_SIZE, "volume"))
-        
-    def testXmippPdbConvertFromObj(self):
+    def test1XmippSinglePdb(self):
+        """ This function runs a test with a single pdb as input. """
         print("Run convert a pdb from import")
+
+        # Creating and launching import protocol to obtain input pdb
         protImport = self.newProtocol(ProtImportPdb, 
                                       inputPdbData=ProtImportPdb.IMPORT_FROM_FILES, 
                                       pdbFile=self.pdb)
         self.launchProtocol(protImport)
         self.assertIsNotNone(protImport.outputPdb.getFileName(), "There was a problem with the import")
         
+        # Creating and launching convert to pdb protocol
         protConvert = self.newProtocol(XmippProtConvertPdb, 
-                                       inputPdbData=XmippProtConvertPdb.IMPORT_OBJ, 
-                                       sampling=3, setSize=True, size_z=20, size_y=20, size_x=20)
+                                       numberOfMpi=1, sampling=3, setSize=True, size_z=20, size_y=20, size_x=20)
         protConvert.pdbObj.set(protImport.outputPdb)
         self.launchProtocol(protConvert)
-        self.assertIsNotNone(protConvert.outputVolume.getFileName(), "There was a problem with the conversion")
-        self.assertAlmostEqual(protConvert.outputVolume.getSamplingRate(), protConvert.sampling.get(), places=1,
+
+        # Obtaining output and analyzing results
+        volume = getattr(protConvert, protConvert.OUTPUT_NAME1, None)
+        volumeFn = volume.getFileName()
+        self.assertTrue(volumeFn.endswith(".mrc"), "Output volume form converting a pdb is not an mrc")
+        ccp4header = Ccp4Header(volumeFn, readHeader=True)
+        headerSr = ccp4header.getSampling()[0]
+        self.assertAlmostEquals(volume.getSamplingRate(), headerSr, "%s header for sampling rate is wrong." % volumeFn)
+
+        self.assertAlmostEqual(volume.getSamplingRate(), protConvert.sampling.get(), places=1,
                                msg=(MSG_WRONG_SAMPLING, "volume"))
-        self.assertAlmostEqual(protConvert.outputVolume.getDim()[0], protConvert.size_z.get(), places=1,
+        self.assertAlmostEqual(volume.getDim()[0], protConvert.size_z.get(), places=1,
                                msg=(MSG_WRONG_SIZE, "volume"))
 
-    def testXmippPdbConvertFromFn(self):
-        print("Run convert a pdb from file")
-        protConvert = self.newProtocol(XmippProtConvertPdb,inputPdbData=2, pdbFile=self.pdb, sampling=2, setSize=False)
+    def test2XmippPdbSet(self):
+        """ This function runs a test with a set of pdbs as input. """
+        print("Run convert a set of pdbs")
+
+        # Defining data path
+        dataPath = os.path.dirname(self.pdb)
+
+        # Obtaining list of pdb files inside folder
+        files = os.listdir(dataPath)
+        pdbFiles = [pdbFile for pdbFile in files if pdbFile.endswith('.pdb')]
+
+        # If there is only one .pdb file, duplicate it so we can have a set with at least two atom structures
+        tmpFiles = []
+        if len(pdbFiles) < 2:
+            newFile = pdbFiles[0].replace('.pdb', '_tmp.pdb')
+            shutil.copyfile(os.path.join(dataPath, pdbFiles[0]), os.path.join(dataPath, newFile))
+            tmpFiles.append(newFile)
+
+        # Creating and launching import protocol to obtain input pdbs
+        protImport = self.newProtocol(ProtImportSetOfAtomStructs,
+                                      inputPdbData=ProtImportSetOfAtomStructs.IMPORT_FROM_FILES,
+                                      filesPath=dataPath)
+        self.launchProtocol(protImport)
+        self.assertIsNotNone(protImport.outputAtomStructs.getFirstItem().getFileName(), "There was a problem with the import")
+
+        # Creating and launching convert to pdb protocol
+        protConvert = self.newProtocol(XmippProtConvertPdb, numberOfMpi=1, numberOfThreads=2, sampling=3, size=20)
+        protConvert.pdbObj.set(protImport.outputAtomStructs)
         self.launchProtocol(protConvert)
-        self.assertIsNotNone(protConvert.outputVolume.getFileName(), "There was a problem with the conversion")
-        self.assertAlmostEqual(protConvert.outputVolume.getSamplingRate(), protConvert.sampling.get(), places=1,
-                               msg=(MSG_WRONG_SAMPLING, "volume"))
-        self.assertAlmostEqual(protConvert.outputVolume.getDim()[0], 48, places=1, msg=(MSG_WRONG_SIZE, "volume"))
 
+        # Deleting tmp files if there were any
+        for tmpFile in tmpFiles:
+            os.remove(os.path.join(dataPath, tmpFile))
+
+        # Obtaining output and analyzing results
+        volumes = getattr(protConvert, protConvert.OUTPUT_NAME2, None)
+        volumeFn = volumes.getFirstItem().getFileName()
+        self.assertTrue(volumeFn.endswith(".mrc"), "Output volume form converting a pdb is not an mrc")
+        ccp4header = Ccp4Header(volumeFn, readHeader=True)
+        headerSr = ccp4header.getSampling()[0]
+        self.assertAlmostEquals(volumes.getSamplingRate(), headerSr, "%s header for sampling rate is wrong." % volumeFn)
+        self.assertAlmostEqual(volumes.getSamplingRate(), protConvert.sampling.get(), places=1,
+                               msg=(MSG_WRONG_SAMPLING, "volumes"))
+        self.assertAlmostEqual(volumes.getDim()[0], protConvert.size.get(), places=1,
+                               msg=(MSG_WRONG_SIZE, "volumes"))
 
 class TestXmippValidateNonTilt(TestXmippBase):
     @classmethod
@@ -1514,20 +1551,11 @@ def testXmippProjSub(self):
         self.assertIsNotNone(protCreateMaskKeep.getFiles(),
                              "There was a problem with the 3D mask of the 1 item phantom")
 
-        # Subtraction of particles - reference volume with and without mask
-        protSubtractProj = self.newProtocol(XmippProtSubtractProjection,
-                                            inputParticles=protCreateGallery.outputReprojections,
-                                            vol=protCreatePhantom1item.outputVolume)
-        self.launchProtocol(protSubtractProj)
-        self.assertIsNotNone(protSubtractProj.outputParticles,
-                             "There was a problem with projection subtraction")
-        self.assertEqual(protSubtractProj.outputParticles.getSamplingRate(), 1.0, (MSG_WRONG_SAMPLING, "particles"))
-        self.assertEqual(protSubtractProj.outputParticles.getFirstItem().getDim(), (80, 80, 1),
-                         (MSG_WRONG_DIM, "particles"))
-        self.assertEqual(protSubtractProj.outputParticles.getSize(), 181, (MSG_WRONG_SIZE, "particles"))
+        # Subtraction of particles - reference volume with mask
         protSubtractProjMask = self.newProtocol(XmippProtSubtractProjection,
                                                 inputParticles=protCreateGallery.outputReprojections,
                                                 vol=protCreatePhantom2items.outputVolume,
+                                                subtract=1,
                                                 mask=protCreateMaskKeep.outputMask)
         self.launchProtocol(protSubtractProjMask)
         self.assertIsNotNone(protSubtractProjMask.outputParticles,
@@ -1545,7 +1573,9 @@ def testXmippProjSub(self):
                              "There was a problem with CTF simulation")
         protSubtractProjCTF = self.newProtocol(XmippProtSubtractProjection,
                                                inputParticles=protSimulateCTF.outputParticles,
-                                               vol=protCreatePhantom1item.outputVolume)
+                                               vol=protCreatePhantom1item.outputVolume,
+                                               subtract=1,
+                                               mask=protCreateMaskKeep.outputMask)
         self.launchProtocol(protSubtractProjCTF)
         self.assertIsNotNone(protSubtractProjCTF.outputParticles,
                              "There was a problem with projection subtraction CTF")
@@ -1561,7 +1591,9 @@ def testXmippProjSub(self):
                              "There was a problem with add noise protocol")
         protSubtractProjNoise = self.newProtocol(XmippProtSubtractProjection,
                                                  inputParticles=protAddNoise.outputParticles,
-                                                 vol=protCreatePhantom1item.outputVolume)
+                                                 vol=protCreatePhantom1item.outputVolume,
+                                                 subtract=1,
+                                                 mask=protCreateMaskKeep.outputMask)
         self.launchProtocol(protSubtractProjNoise)
         self.assertIsNotNone(protSubtractProjNoise.outputParticles,
                              "There was a problem with projection subtraction with noise")
@@ -1577,7 +1609,9 @@ def testXmippProjSub(self):
                              "There was a problem with add noise to ctf particles protocol")
         protSubtractProjNoiseCTF = self.newProtocol(XmippProtSubtractProjection,
                                                     inputParticles=protAddNoiseCTF.outputParticles,
-                                                    vol=protCreatePhantom1item.outputVolume)
+                                                    vol=protCreatePhantom1item.outputVolume,
+                                                    subtract=1,
+                                                    mask=protCreateMaskKeep.outputMask)
         self.launchProtocol(protSubtractProjNoiseCTF)
         self.assertIsNotNone(protSubtractProjNoiseCTF.outputParticles,
                              "There was a problem with projection subtraction with noise and CTF")
@@ -1617,7 +1651,8 @@ def testXmippProjSub(self):
         protSubtractProjOver = self.newProtocol(XmippProtSubtractProjection,
                                                 inputParticles=protCreateGalleryOver.outputReprojections,
                                                 vol=protCreatePhantom2Over.outputVolume,
-                                                mask=protCreateMaskKeepOver.outputMask)
+                                                mask=protCreateMaskKeepOver.outputMask,
+                                                subtract=1)
         self.launchProtocol(protSubtractProjOver)
         self.assertIsNotNone(protSubtractProjOver.outputParticles,
                              "There was a problem with projection subtraction with overlap")
@@ -1627,7 +1662,9 @@ def testXmippProjSub(self):
         self.assertEqual(protSubtractProjOver.outputParticles.getSize(), 1647, (MSG_WRONG_SIZE, "particles"))
         protSubtractProjOverNoMask = self.newProtocol(XmippProtSubtractProjection,
                                                       inputParticles=protCreateGalleryOver.outputReprojections,
-                                                      vol=protCreatePhantom1Over.outputVolume)
+                                                      vol=protCreatePhantom1Over.outputVolume,
+                                                      mask=protCreateMaskKeepOver.outputMask,
+                                                      subtract=1)
         self.launchProtocol(protSubtractProjOverNoMask)
         self.assertIsNotNone(protSubtractProjOverNoMask.outputParticles,
                              "There was a problem with projection subtraction with overlap")
@@ -1648,7 +1685,9 @@ def testXmippProjSub(self):
                              "There was a problem with add noise to ctf overlap particles protocol")
         protSubtractProjNoiseCTFOver = self.newProtocol(XmippProtSubtractProjection,
                                                         inputParticles=protAddNoiseCTFOver.outputParticles,
-                                                        vol=protCreatePhantom1Over.outputVolume)
+                                                        vol=protCreatePhantom1Over.outputVolume,
+                                                        mask=protCreateMaskKeepOver.outputMask,
+                                                        subtract=1)
         self.launchProtocol(protSubtractProjNoiseCTFOver)
         self.assertIsNotNone(protSubtractProjNoiseCTFOver.outputParticles,
                              "There was a problem with projection subtraction with noise and CTF overlap")
@@ -2531,6 +2570,48 @@ def testXmippAlignVolumeAndParticles(self):
                          protCreatePhantomReference.outputVolume.getSamplingRate(),
                          (MSG_WRONG_SAMPLING, "rotated phantom"))
 
+class TestXmippReconstructSignificant(TestXmippBase):
+    """This class checks if the protocol reconstruct significant in Xmipp works properly."""
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+
+    def testXmippReconstructSignificant(self):
+        # Create input data: phantom with one cylinder
+        protCreatePhantom1 = self.newProtocol(XmippProtPhantom,
+                                               desc='80 80 80 0\ncyl + 5 0 0 0 5 5 10 0 0 0',
+                                               sampling=1.0)
+        self.launchProtocol(protCreatePhantom1)
+        self.assertIsNotNone(protCreatePhantom1.getFiles(),
+                             "There was a problem with the phantom creation")
+        # Create particles from the first phantom
+        protCreateGallery = self.newProtocol(XmippProtCreateGallery,
+                                             inputVolume=protCreatePhantom1.outputVolume,
+                                             rotStep=15.0,
+                                             tiltStep=90.0)
+        self.launchProtocol(protCreateGallery)
+        self.assertIsNotNone(protCreateGallery.getFiles(),
+                             MSG_WRONG_GALLERY)
+        # Reconstruct significant (default values)
+        protReconstructSignificant = self.newProtocol(XmippProtReconstructSignificant,
+                                                      inputSet=protCreateGallery.outputReprojections)
+        self.launchProtocol(protReconstructSignificant)
+        self.assertIsNotNone(protReconstructSignificant.getFiles(),
+                             MSG_WRONG_RECONSTRUCTION)
+        self.assertIsNotNone(protReconstructSignificant.outputVolume,
+                             (MSG_WRONG_RECONSTRUCTION, " of the final volume"))
+        # Reconstruct significant (default values) with a reference volume
+        protReconstructSignificant2 = self.newProtocol(XmippProtReconstructSignificant,
+                                                       inputSet=protCreateGallery.outputReprojections,
+                                                       thereisRefVolume=True,
+                                                       refVolume=protCreatePhantom1.outputVolume)
+        self.launchProtocol(protReconstructSignificant2)
+        self.assertIsNotNone(protReconstructSignificant2.getFiles(),
+                             MSG_WRONG_RECONSTRUCTION)
+        self.assertIsNotNone(protReconstructSignificant2.outputVolume,
+                             (MSG_WRONG_RECONSTRUCTION, " of the final volume"))
+
+
 class TestXmippDeepHand(TestXmippBase):
     """This class checks if the protocol deep hand in Xmipp works properly."""
     @classmethod
@@ -2569,7 +2650,7 @@ def testXmippDeepHand(self):
         self.assertEqual(protDeepHand.outputVol.getDim(), protImportVol.outputVolume.getDim(),
                          (MSG_WRONG_DIM, "volume"))
         # Check if the hand value is right
-        self.assertAlmostEquals(protDeepHand.outputHand.get(), 0.380511, 6,"There was a problem with the hand value")
+        self.assertAlmostEquals(protDeepHand.outputHand.get(), 0.3805, 4,"There was a problem with the hand value")
         # Check if the flip is right
         self.assertTrue(protDeepHand.outputHand.get()<protDeepHand.thresholdHand.get(), "There was a problem with the flip")
 
@@ -2634,7 +2715,6 @@ def testXmippResolutionBfactor(self):
                                                  fscResolution=8.35)
         self.launchProtocol(protbfactorResolution)
 
-
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         className = sys.argv[1]
diff --git a/xmipp3/tests/test_protocols_xmipp_mics.py b/xmipp3/tests/test_protocols_xmipp_mics.py
index 5db01d0ff..d69fd0058 100644
--- a/xmipp3/tests/test_protocols_xmipp_mics.py
+++ b/xmipp3/tests/test_protocols_xmipp_mics.py
@@ -290,30 +290,6 @@ def testCTF(self):
         sampling = ctfModel.getMicrograph().getSamplingRate()
         self.assertAlmostEquals(sampling, 2.474, delta=0.001)
 
-class TestXmippBoxsize(TestXmippBase):
-    """This class check if the protocol to determine the BoxSize in Xmipp works properly."""
-    @classmethod
-    def setUpClass(cls):
-        setupTestProject(cls)
-        TestXmippBase.setData()
-        fileName = DataSet.getDataSet('relion_tutorial').getFile('allMics')
-        cls.protImport = cls.runImportMicrograph(fileName,
-                                                 samplingRate=3.54,
-                                                 voltage=300,
-                                                 sphericalAberration=2,
-                                                 scannedPixelSize=None,
-                                                 magnification=56000)
-    def test1(self):
-        #TODO: CHECK IF THE PREDICTIONS ON MIC MATCH THE PREDICTIONS ON DOWNSAMPLED MICS
-        # Estimate CTF on the downsampled micrographs
-        print("Estimating boxsize...")
-        protCTF = XmippProtParticleBoxsize()
-        protCTF.inputMicrographs.set(self.protImport.outputMicrographs)
-        self.proj.launchProtocol(protCTF, wait=True)
-        self.assertIsNotNone(protCTF.boxsize, "Boxsize has not been produced.")
-        self.assertAlmostEquals(protCTF.boxsize.get(), 50, delta=20,
-                                msg='Wrong estimated boxsize.')
-
 
 class TestXmippAutomaticPicking(TestXmippBase):
     """This class check if the protocol to pick the micrographs automatically in Xmipp works properly."""
diff --git a/xmipp3/tests/test_protocols_xmipp_movies.py b/xmipp3/tests/test_protocols_xmipp_movies.py
index 1f52c0eb3..5b8e81a0e 100644
--- a/xmipp3/tests/test_protocols_xmipp_movies.py
+++ b/xmipp3/tests/test_protocols_xmipp_movies.py
@@ -792,3 +792,81 @@ def testFilterAcceptBoth(self):
         self._checkMaxShiftFiltering(protDoMic, label, hasMic=True,
                                      hasDw=True, results=[True, True])
 
+
+class TestMovieDoseAnalysis(BaseTest):
+
+    @classmethod
+    def setData(cls):
+        setupTestProject(cls)
+        cls.ds = DataSet.getDataSet('relion30_tutorial')
+
+    @classmethod
+    def runImportMovies(cls):
+        protImport = cls.newProtocol(
+            ProtImportMovies,
+            filesPath=cls.ds.getFile('Movies/'),
+            filesPattern='*.tiff',
+            samplingRateMode=0,
+            samplingRate=0.885,
+            magnification=50000,
+            scannedPixelSize=7.0,
+            voltage=200,
+            sphericalAberration=1.4,
+            doseInitial=0.0,
+            dosePerFrame=1.277,
+            gainFile=cls.ds.getFile("Movies/gain.mrc")
+        )
+        protImport.setObjLabel('import 24 movies')
+        protImport.setObjComment('Relion 3 tutorial movies:\n\n'
+                                 'Microscope Jeol Cryo-ARM 200\n'
+                                 'Data courtesy of Takyuki Kato in the Namba '
+                                 'group\n(Osaka University, Japan)')
+        return cls.launchProtocol(protImport)
+
+
+    @classmethod
+    def setUpClass(cls):
+        setupTestProject(cls)
+        cls.setData()
+        cls.protImport = cls.runImportMovies()
+
+    # ------- Tests ---------------------------------------
+    def testDoseAnalysisAssert(self):
+        """ This must create two sets of movies.
+        """
+        label = 'Dose Analysis Assert'
+        protPoisson = self.newProtocol(XmippProtMovieDoseAnalysis,
+                                       objLabel=label,
+                                       n_samples=10,
+                                       movieStep=4
+                                       )
+        protPoisson.inputMovies.set(self.protImport.outputMovies)
+        self.launchProtocol(protPoisson)
+
+        self.assertIsNotNone(getattr(protPoisson, 'outputMoviesDiscarded', None),
+                          "outputMoviesDiscarded were not created. "
+                          "Bad filtering in test.")
+
+        self.assertIsNotNone(getattr(protPoisson, 'outputMovies', None),
+                             "outputMovies were not created. "
+                             "Bad filtering in test.")
+
+    def testDoseAnalysisFiltering(self):
+        """ This must discard movies by dose analysis.
+        """
+        label = 'Dose Analysis Filter'
+        protPoisson = self.newProtocol(XmippProtMovieDoseAnalysis,
+                                       objLabel=label,
+                                       n_samples=24,
+                                       movieStep=4
+                                       )
+        protPoisson.inputMovies.set(self.protImport.outputMovies)
+        self.launchProtocol(protPoisson)
+
+        sizeAccepted = protPoisson.outputMovies.getSize()
+        self.assertEqual(sizeAccepted, 19, 'Number of accepted movies must be 19 and its '
+                                           '%d' % sizeAccepted)
+
+        sizeDiscarded = protPoisson.outputMoviesDiscarded.getSize()
+        self.assertEqual(sizeDiscarded, 5, 'Number of accepted movies must be 5 and its '
+                                            '%d' % sizeDiscarded)
\ No newline at end of file