# **************************************************************************
# *
# * Authors: Javier Vargas and Adrian Quintana (jvargas@cnb.csic.es aquintana@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 numpy as np
import pyworkflow.protocol.params as params
from pyworkflow.protocol import STEPS_PARALLEL
from pwem.protocols import ProtAlignVolume
from pwem.objects import Volume, Transform, SetOfVolumes
from xmipp3.convert import getImageLocation
from pyworkflow import BETA, UPDATED, NEW, PROD
import xmippLib
ALIGN_MASK_CIRCULAR = 0
ALIGN_MASK_BINARY_FILE = 1
ALIGN_ALGORITHM_EXHAUSTIVE = 0
ALIGN_ALGORITHM_LOCAL = 1
ALIGN_ALGORITHM_EXHAUSTIVE_LOCAL = 2
ALIGN_ALGORITHM_FAST_FOURIER = 3
[docs]class XmippProtAlignVolume(ProtAlignVolume):
"""
Aligns a set of 3D volumes using cross-correlation or Fast Fourier Transform methods. The alignment allows direct
comparison or averaging of volumes by bringing them into a common spatial frame.
AI Generated:
Align Volume (XmippProtAlignVolume) — User Manual
Overview
The Align Volume protocol aligns one or more 3D volumes to a common reference using Xmipp
cross-correlation–based methods. Its main purpose is to place maps in the same spatial frame so they can be
compared, averaged, or further analyzed in a biologically meaningful way. In typical cryo-EM workflows, this
step becomes essential when volumes originate from different reconstructions, different processing strategies,
or different biochemical conditions.
For a biological user, the most common situations include comparing conformational states, standardizing maps
before visualization or modeling, or producing an averaged consensus volume from several reconstructions. The
protocol is designed to work both in exploratory analyses and in more demanding, publication-level workflows.
Inputs and General Workflow
The protocol requires a reference volume, which defines the coordinate system, and one or more input
volumes, which will be transformed to match the reference. A good practical rule is to choose as reference
the map with the highest quality or the one representing the biologically most relevant state. Ideally, all
volumes should share the same voxel size and similar box size. Large structural differences between the
reference and the inputs may lead to unstable or biologically meaningless alignments.
Optionally, the protocol can compute an average volume after alignment. This is useful when combining several
reconstructions of the same state to improve signal or reduce noise. However, biological caution is needed:
averaging heterogeneous conformations will blur flexible regions and may hide meaningful variability.
Masking: Focusing the Alignment
Masking is one of the most biologically important options in this protocol because it determines which
regions of the map drive the alignment. When masking is disabled, the algorithm uses the full volume, which
is acceptable for compact, globular complexes with limited flexibility. In many real cryo-EM cases, however,
applying a mask significantly improves robustness.
The protocol allows either a circular mask or a binary mask file. The circular mask is simple and convenient
for globular particles or quick exploratory runs. The user only needs to provide a radius in pixels. In
contrast, the binary mask is more powerful and is generally preferred for complex biological systems such as
multi-domain proteins, membrane proteins, or assemblies with flexible appendages. In these cases, focusing
the alignment on the stable core of the structure often yields much more reliable results.
From a biological perspective, the mask should include the structurally conserved region and exclude large
solvent areas or highly mobile domains. Poor masking is one of the most common causes of suboptimal alignment.
Choice of Alignment Algorithm
The protocol offers several alignment strategies that differ mainly in robustness and computational cost.
For most routine biological work, the Fast Fourier method is the recommended starting point. It provides
results comparable to exhaustive searches but at a much lower computational cost, making it well suited for
large datasets or facility pipelines such as those commonly handled within Scipion environments.
The Exhaustive search performs a full exploration of the parameter space. Although slower, it is more robust
when the relative orientation between volumes is largely unknown. This option becomes useful in difficult cases,
for example when comparing reconstructions obtained from very different pipelines or when strong misalignment
is suspected.
The Local method is designed for refinement around a known orientation. It is very fast but relies on good
initial estimates. Biological users typically employ it when volumes are already approximately aligned and only
fine adjustments are needed.
Finally, the Exhaustive + Local strategy combines both approaches. It first performs a global search and then r
efines locally, offering a good balance between robustness and precision. This option is particularly suitable
for challenging datasets where both reliability and accuracy are important.
Angular and Shift Search Ranges
Advanced users can control the angular and translational search space. In many biological workflows the default
values work well, but understanding their meaning can help in difficult cases.
Angular ranges determine how broadly the protocol explores possible orientations. When the relative orientation
between volumes is unknown, wide angular ranges are appropriate, although they increase computation time. When
volumes are already roughly aligned, narrowing the search and reducing the angular step improves precision and
speed.
Similarly, the shift ranges define how far the protocol searches for translations in X, Y, and Z. If maps are
well centered, small ranges are sufficient and more efficient. Larger ranges should only be used when
significant mis-centering is expected. Excessively large search spaces can dramatically increase runtime
without improving biological relevance.
The protocol also allows a scale search. This is mainly useful when comparing volumes that may have small
magnification differences or slightly mismatched voxel sizes, for example when combining data processed with
different software packages or acquired under different microscope calibrations. If voxel sizes are known to be
consistent, scale optimization is usually unnecessary.
Local Alignment Initialization
When the Local algorithm is selected, the user can provide initial angles and shifts. These parameters
guide the refinement and should reflect the best available prior knowledge. In iterative cryo-EM
workflows—such as when refining class averages or comparing closely related reconstructions—providing good
initial values significantly improves convergence and speed. An optional scale optimization can also be enabled
in this mode when small magnification differences are suspected.
Outputs and Their Interpretation
After execution, the protocol produces the aligned volume or set of volumes, each accompanied by the
corresponding rigid transformation. If multiple inputs are provided, the outputs preserve their identity but
are now expressed in the reference coordinate frame.
If averaging was requested, an additional average volume is produced. Biologically, this average enhances
common structural features and reduces noise, but it should be interpreted carefully. When structural
heterogeneity is present, averaging may obscure meaningful differences rather than reveal them.
Practical Recommendations
In routine biological practice, it is often best to begin with the Fast Fourier method using default parameters
and inspect the visual quality of the alignment. If results appear unstable—particularly in flexible
complexes—introducing an appropriate binary mask usually provides the largest improvement. For difficult cases
with large orientation uncertainty, switching to the Exhaustive + Local strategy is often effective.
When working with near-identical reconstructions, the Local method provides very fast refinement. Conversely,
when preparing volumes for quantitative comparison or publication figures, it is advisable to tighten angular
steps and carefully verify the results visually.
Web-Oriented Variant
The Align Volume Web protocol is a streamlined version intended for web visualization environments. It disables
masking and uses the Fast Fourier method by default to ensure rapid execution in remote or facility portals.
This variant is particularly suitable for quick comparisons and interactive visualization services but is less
flexible than the full protocol for demanding biological analyses.
Final Perspective
For most cryo-EM users, volume alignment is not merely a geometric operation but a biologically meaningful
step that can strongly influence downstream interpretation. Careful selection of the reference, thoughtful
masking of flexible regions, and an alignment strategy matched to the biological question are the key
elements for reliable results.
"""
_label = 'align volume'
nVols = 0
_devStatus = PROD
OUTPUT_NAME1 = "outputVolume"
OUTPUT_NAME2 = "outputVolumes"
_possibleOutputs = {OUTPUT_NAME1: Volume,
OUTPUT_NAME2: SetOfVolumes}
def __init__(self, **args):
ProtAlignVolume.__init__(self, **args)
self.stepsExecutionMode = STEPS_PARALLEL
#--------------------------- DEFINE param functions --------------------------------------------
def _defineParams(self, form):
form.addSection(label='Volume parameters')
form.addParam('inputReference', params.PointerParam, pointerClass='Volume',
label="Reference volume", important=True,
help='Reference volume to be used for the alignment.')
form.addParam('inputVolumes', params.MultiPointerParam, pointerClass='SetOfVolumes,Volume',
label="Input volume(s)", important=True,
help='Select one or more volumes (Volume or SetOfVolumes)\n'
'to be aligned againt the reference volume.')
form.addParam('computeAvg', params.BooleanParam, label='Create average', default=False,
help='Average all the volumes once aligned')
group1 = form.addGroup('Mask')
group1.addParam('applyMask', params.BooleanParam, default=False,
label='Apply mask?',
help='Apply a 3D Binary mask to the volumes')
group1.addParam('maskType', params.EnumParam, choices=['circular','binary file'], default=ALIGN_MASK_CIRCULAR,
label='Mask type', display=params.EnumParam.DISPLAY_COMBO, condition='applyMask',
help='Select the type of mask you want to apply')
group1.addParam('maskRadius', params.IntParam, default=-1, condition='applyMask and maskType==%d' % ALIGN_MASK_CIRCULAR,
label='Mask radius',
help='Insert the radius for the mask')
group1.addParam('maskFile', params.PointerParam, condition='applyMask and maskType==%d' % ALIGN_MASK_BINARY_FILE,
pointerClass='VolumeMask', label='Mask file',
help='Select the volume mask object')
form.addSection(label='Search strategy')
form.addParam('alignmentAlgorithm', params.EnumParam, default=ALIGN_ALGORITHM_FAST_FOURIER,
choices=['exhaustive',
'local',
'exhaustive + local',
'fast fourier'],
label='Alignment algorithm', display=params.EnumParam.DISPLAY_COMBO,
help='Exhaustive searches all possible combinations within a search space.'
'Local searches around a given position.'
'Be aware that the Fast Fourier algorithm requires a special compilation'
'of Xmipp (--cltomo flag). It performs the same job as the '
'exhaustive method but much faster.')
anglesCond = 'alignmentAlgorithm!=%d' % ALIGN_ALGORITHM_LOCAL
group = form.addGroup('Angles range', condition=anglesCond, expertLevel=params.LEVEL_ADVANCED)
line = group.addLine('Rotational angle (deg)')
line.addParam('minRotationalAngle', params.FloatParam, default=0, label='Min')
line.addParam('maxRotationalAngle', params.FloatParam, default=360, label='Max')
line.addParam('stepRotationalAngle', params.FloatParam, default=5, label='Step')
line = group.addLine('Tilt angle (deg)', expertLevel=params.LEVEL_ADVANCED)
line.addParam('minTiltAngle', params.FloatParam, default=0, label='Min')
line.addParam('maxTiltAngle', params.FloatParam, default=180, label='Max')
line.addParam('stepTiltAngle', params.FloatParam, default=5, label='Step')
line = group.addLine('Inplane angle (deg)', expertLevel=params.LEVEL_ADVANCED)
line.addParam('minInplaneAngle', params.FloatParam, default=0, label='Min')
line.addParam('maxInplaneAngle', params.FloatParam, default=360, label='Max')
line.addParam('stepInplaneAngle', params.FloatParam, default=5, label='Step')
group = form.addGroup('Shifts range', condition=anglesCond, expertLevel=params.LEVEL_ADVANCED)
line = group.addLine('Shift X (px)')
line.addParam('minimumShiftX', params.FloatParam, default=0, label='Min')
line.addParam('maximumShiftX', params.FloatParam, default=0, label='Max')
line.addParam('stepShiftX', params.FloatParam, default=1, label='Step')
line = group.addLine('Shift Y (px)', expertLevel=params.LEVEL_ADVANCED)
line.addParam('minimumShiftY', params.FloatParam, default=0, label='Min')
line.addParam('maximumShiftY', params.FloatParam, default=0, label='Max')
line.addParam('stepShiftY', params.FloatParam, default=1, label='Step')
line = group.addLine('Shift Z (px)', expertLevel=params.LEVEL_ADVANCED)
line.addParam('minimumShiftZ', params.FloatParam, default=0, label='Min')
line.addParam('maximumShiftZ', params.FloatParam, default=0, label='Max')
line.addParam('stepShiftZ', params.FloatParam, default=1, label='Step')
line = form.addLine('Scale ', expertLevel=params.LEVEL_ADVANCED, condition=anglesCond)
line.addParam('minimumScale', params.FloatParam, default=1, label='Min')
line.addParam('maximumScale', params.FloatParam, default=1, label='Max')
line.addParam('stepScale', params.FloatParam, default=0.005, label='Step')
group = form.addGroup('Initial values',
condition='alignmentAlgorithm==%d' % ALIGN_ALGORITHM_LOCAL,
expertLevel=params.LEVEL_ADVANCED)
line = group.addLine('Initial angles')
line.addParam('initialRotAngle', params.FloatParam, default=0, label='Rot')
line.addParam('initialTiltAngle', params.FloatParam, default=0, label='Tilt')
line.addParam('initialInplaneAngle', params.FloatParam, default=0, label='Psi')
line = group.addLine('Initial shifts ', expertLevel=params.LEVEL_ADVANCED)
line.addParam('initialShiftX', params.FloatParam, default=0, label='X')
line.addParam('initialShiftY', params.FloatParam, default=0, label='Y')
line.addParam('initialShiftZ', params.FloatParam, default=0, label='Z')
group.addParam('optimizeScale', params.BooleanParam, default=False, expertLevel=params.LEVEL_ADVANCED,
label='Optimize scale',
help='Choose YES if you want to optimize the scale of input volume/s based on the reference')
group.addParam('initialScale', params.FloatParam, default=1, expertLevel=params.LEVEL_ADVANCED, condition='optimizeScale',
label='Initial scale')
form.addParallelSection(threads=8, mpi=1)
#--------------------------- INSERT steps functions --------------------------------------------
def _insertAllSteps(self):
# Iterate through all input volumes and align them
# againt the reference volume
refFn = getImageLocation(self.inputReference.get())
maskArgs = self._getMaskArgs()
alignArgs = self._getAlignArgs()
alignSteps = []
idx=1
for vol in self._iterInputVolumes():
volFn = getImageLocation(vol)
volId = vol.getObjId()
stepId = self._insertFunctionStep('alignVolumeStep', refFn, volFn,
self._getExtraPath("vol%02d.mrc"%idx), maskArgs,
alignArgs, idx, prerequisites=[])
alignSteps.append(stepId)
idx+=1
self._insertFunctionStep('createOutputStep', prerequisites=alignSteps)
#--------------------------- STEPS functions --------------------------------------------
[docs] def alignVolumeStep(self, refFn, inVolFn, outVolFn, maskArgs, alignArgs, volId):
args = "--i1 %s --i2 %s --apply %s" % (refFn, inVolFn, outVolFn)
args += maskArgs
args += alignArgs
args += " --copyGeo %s" % (
self._getExtraPath('transformation-matrix_vol%06d.txt'%volId))
self.runJob("xmipp_volume_align", args)
if self.alignmentAlgorithm == ALIGN_ALGORITHM_EXHAUSTIVE_LOCAL:
args = "--i1 %s --i2 %s --apply --local" % (refFn, outVolFn)
args += " --copyGeo %s" % (
self._getExtraPath('transformation-matrix_vol%06d.txt'%volId))
self.runJob("xmipp_volume_align", args)
[docs] def createOutputStep(self):
Ts = self.inputReference.get().getSamplingRate()
vols = []
idx=1
Vavg=None
for vol in self._iterInputVolumes():
outVol = Volume()
fnOutVol = self._getExtraPath("vol%02d.mrc"%idx)
outVol.setLocation(fnOutVol)
outVol.setObjComment(vol.getObjComment())
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))
transform = Transform()
transform.setMatrix(transformationMat)
outVol.setTransform(transform)
vols.append(outVol)
# Set the sampling rate in the mrc header
self.runJob("xmipp_image_header", "-i %s --sampling_rate %f"%(fnOutVol, Ts))
if self.computeAvg:
Vi=xmippLib.Image(fnOutVol).getData()
if Vavg is None:
Vavg=Vi
else:
Vavg+=Vi
idx+=1
if len(vols) > 1:
volSet = self._createSetOfVolumes()
volSet.setSamplingRate(Ts)
for vol in vols:
volSet.append(vol)
outputArgs = {'outputVolumes': volSet}
else:
vols[0].setSamplingRate(Ts)
outputArgs = {'outputVolume': vols[0]}
if self.computeAvg:
Vavg/=len(vols)
Vsave=xmippLib.Image()
Vsave.setData(Vavg)
fnAvg=self._getExtraPath("averageVolume.mrc")
Vsave.write(fnAvg)
self.runJob("xmipp_image_header", f"-i {fnAvg} --sampling_rate {Ts}")
volAvg=Volume()
volAvg.setFileName(fnAvg)
volAvg.setSamplingRate(Ts)
outputArgs['outputAverage']=volAvg
self._defineOutputs(**outputArgs)
if len(vols) > 1:
for pointer in self.inputVolumes:
self._defineSourceRelation(pointer, outputArgs['outputVolumes'])
else:
for pointer in self.inputVolumes:
self._defineSourceRelation(pointer, outputArgs['outputVolume'])
if self.computeAvg:
for pointer in self.inputVolumes:
self._defineSourceRelation(pointer, outputArgs['outputAverage'])
# --------------------------- INFO functions --------------------------------------------
def _validate(self):
errors = []
for pointer in self.inputVolumes:
if pointer.pointsNone():
errors.append('Invalid input, pointer: %s' % pointer.getObjValue())
errors.append(' extended: %s' % pointer.getExtended())
return errors
def _summary(self):
summary = []
nVols = self._getNumberOfInputs()
if nVols > 0:
summary.append("Volumes to align: *%d* " % nVols)
else:
summary.append("No volumes selected.")
summary.append("Alignment method: %s" % self.getEnumText('alignmentAlgorithm'))
return summary
def _methods(self):
nVols = self._getNumberOfInputs()
if nVols > 0:
methods = 'We aligned %d volumes against a reference volume using ' % nVols
#TODO: Check a more descriptive way to add the reference and
# all aligned volumes to the methods (such as obj.getNameId())
# also to show the number of volumes from each set in the input.
# This approach implies to consistently include also the outputs
# ids to be tracked in all the workflow's methods.
if self.alignmentAlgorithm == ALIGN_ALGORITHM_FAST_FOURIER:
methods += ' the Fast Fourier alignment described in [Chen2013].'
elif self.alignmentAlgorithm == ALIGN_ALGORITHM_LOCAL:
methods += ' a local search of the alignment parameters.'
elif self.alignmentAlgorithm == ALIGN_ALGORITHM_EXHAUSTIVE:
methods += ' an exhaustive search.'
elif self.alignmentAlgorithm == ALIGN_ALGORITHM_EXHAUSTIVE_LOCAL:
methods += ' an exhaustive search followed by a local search.'
else:
methods = 'No methods available yet.'
return [methods]
def _citations(self):
if self.alignmentAlgorithm == ALIGN_ALGORITHM_FAST_FOURIER:
return ['Chen2013']
#--------------------------- UTILS functions --------------------------------------------
def _iterInputVolumes(self):
""" Iterate over all the input volumes. """
for pointer in self.inputVolumes:
item = pointer.get()
if item is None:
break
itemId = item.getObjId()
if isinstance(item, Volume):
item.outputName = self._getExtraPath('output_vol%06d.mrc' % itemId)
# If item is a Volume and label is empty
if not item.getObjLabel():
# Volume part of a set
if item.getObjParentId() is None:
item.setObjLabel("%s.%s" % (pointer.getObjValue(), pointer.getExtended()))
else:
item.setObjLabel('%s.%s' % (self.getMapper().getParent(item).getRunName(), item.getClassName()))
yield item
elif isinstance(item, SetOfVolumes):
for vol in item:
vol.outputName = self._getExtraPath('output_vol%06d_%03d.mrc' % (itemId, vol.getObjId()))
# If set item label is empty
if not vol.getObjLabel():
# if set label is not empty use it
if item.getObjLabel():
vol.setObjLabel("%s - %s%s" % (item.getObjLabel(), vol.getClassName(), vol.getObjId()))
else:
vol.setObjLabel("%s - %s%s" % (self.getMapper().getParent(item).getRunName(), vol.getClassName(), vol.getObjId()))
yield vol
def _getNumberOfInputs(self):
""" Return the total number of input volumes. """
nVols = 0
for _ in self._iterInputVolumes():
nVols += 1
return nVols
def _getMaskArgs(self):
maskArgs = ''
if self.applyMask:
if self.maskType == ALIGN_MASK_CIRCULAR:
maskArgs+=" --mask circular -%d" % self.maskRadius
else:
maskArgs+=" --mask binary_file %s" % self.maskFile.get().getFileName()
return maskArgs
def _getAlignArgs(self):
alignArgs = ' --dontWrap'
if self.alignmentAlgorithm == ALIGN_ALGORITHM_FAST_FOURIER:
alignArgs += " --frm"
elif self.alignmentAlgorithm == ALIGN_ALGORITHM_LOCAL:
alignArgs += " --local --rot %f %f 1 --tilt %f %f 1 --psi %f %f 1 -x %f %f 1 -y %f %f 1 -z %f %f 1" %\
(self.initialRotAngle, self.initialRotAngle,
self.initialTiltAngle, self.initialTiltAngle,
self.initialInplaneAngle, self.initialInplaneAngle,
self.initialShiftX, self.initialShiftX,
self.initialShiftY, self.initialShiftY,
self.initialShiftZ,self.initialShiftZ)
if self.optimizeScale:
alignArgs += " --scale %f %f 0.005" %(self.initialScale, self.initialScale)
else:
alignArgs += " --dontScale"
else: # Exhaustive or Exhaustive+Local
alignArgs += " --rot %f %f %f --tilt %f %f %f --psi %f %f %f -x %f %f %f -y %f %f %f -z %f %f %f --scale %f %f %f" %\
(self.minRotationalAngle, self.maxRotationalAngle, self.stepRotationalAngle,
self.minTiltAngle, self.maxTiltAngle, self.stepTiltAngle,
self.minInplaneAngle, self.maxInplaneAngle, self.stepInplaneAngle,
self.minimumShiftX, self.maximumShiftX, self.stepShiftX,
self.minimumShiftY, self.maximumShiftY, self.stepShiftY,
self.minimumShiftZ, self.maximumShiftZ, self.stepShiftZ,
self.minimumScale, self.maximumScale, self.stepScale)
return alignArgs
[docs]class XmippProtAlignVolumeForWeb(XmippProtAlignVolume):
""" Similar to XmippProtAlignVolume but optimized for web-based visualization. This protocol aligns volumes for easier comparison in web interfaces, facilitating remote analysis and presentation.
"""
_label = 'align volume web'
def _defineParams(self, form):
XmippProtAlignVolume._defineParams(self, form)
maskGroup = form.getParam('Mask')
maskGroup.config(condition='False')
# Set as default the fast fourier align method
# this requires that the Xmipp is compiled with
# the corresponding flag
form.getParam('alignmentAlgorithm').config(default=ALIGN_ALGORITHM_FAST_FOURIER)