#!/usr/bin/env python
# subtomosetup - make command files for reconstructing subvolumes around points
#
# Author: David Mastronarde
#
# $Id: subtomosetup,v f131bffb32f6 2025/09/24 04:21:34 mast $
#

progname = 'subtomosetup'
prefix = 'ERROR: ' + progname + ' - '
yzRatioCrit = 2.
xzShiftDecimals = 2
maxChunks = 99990
minChunks = 8
maxChunkPerProc = 10
minChunkPerProc = 5
warnXtiltCrit = 0.3
debug = 0

# Compose the next command file name and increment the number. making a sync if indicated
def makeNextComName(doSync = False):
   global comNum
   if doSync:
      comName = rootWithDir + fmtstr('-{:03d}', comNum) + '-sync.com'
   else:
      comName = rootWithDir + fmtstr('-{:03d}', comNum) + '.com'
   comNum += 1
   return comName


#### MAIN PROGRAM  ####
#
# load System Libraries
import os, sys, copy

#
# Setup runtime environment
if os.getenv('IMOD_DIR') != None:
   IMOD_DIR = os.environ['IMOD_DIR']
   if sys.platform == 'cygwin' and sys.version_info[0] > 2:
      IMOD_DIR = IMOD_DIR.replace('\\', '/')
      if IMOD_DIR[1] == ':' and IMOD_DIR[2] == '/':
         IMOD_DIR = '/cygdrive/' + IMOD_DIR[0].lower() + IMOD_DIR[2:]
   sys.path.insert(0, os.path.join(IMOD_DIR, 'pylib'))
   from imodpy import *
   addIMODbinIgnoreSIGHUP()
else:
   sys.stdout.write(prefix + " IMOD_DIR is not defined!\n")
   sys.exit(1)

#
# load IMOD Libraries
from pip import *
from pysed import *
from tomocoords import *

# Fallbacks from ../manpages/autodoc2man 3 1 subtomosetup
options = ["root:RootName:CH:", "center:CenterPositionFile:FN:",
           "volume:VolumeModeled:FN:", "raw:RawStackFile:FN:", "axis:AxisAngle:F:",
           "objects:ObjectsToUse:LI:", "size:SizeInXYZ:IT:",
           "dir:DirectoryForOutput:FN:", "chunk:DirectoryForChunkFiles:FN:",
           "skip:SkipSubVolNumbers:B:", "stackvols:MakeVolumeStacks:I:",
           "com:CommandFile:FN:", "binali:NewAlignedBinning:I:",
           "newstcom:NewstackComFile:FN:", "unaligned:UseUnalignedImages:B:",
           "reduce:FourierReduceByFactor:I:", "zlevels:NumberOfZLevels:I:",
           "extent:ExtentOfZLevelsInNm:I:", "invert:InvertZLevelOffsets:I:",
           "adjust:AdjustForAlignZShift:B:", "ctfcom:CorrectionComFile:FN:",
           "erase:EraseFiducials:B:", "goldcom:GoldEraserComFile:FN:",
           "filter:FilterIn2D:B:", "2dcom:2DFilterComFile:FN:", "gpu:WhenToUseGPU:I:",
           "pixel:RawPixelSize:F:", "xform:AlignTransformFile:FN:",
           "reorient:ReorientionType:I:", "proc:ProcessorNumber:I:",
           "runs:RunsPerChunk:I:", "help:usage:B:"]

(rootName, volName, centerFile, pointFile, modelFile, objList, comFile, \
       reorientIn, enteredOrient) = getCommonOptions(options, progname)

checkList = []
boundPixels = parallelBoundarySize()
outDir = PipGetString('DirectoryForOutput', '')
skipVolNumbers = PipGetBoolean('SkipSubVolNumbers', 0)
comDir = PipGetString('DirectoryForChunkFiles', 0)
(xSize, ySize, zSize) = PipGetThreeIntegers('SizeInXYZ', 0, 0, 0)
if xSize < 1 or ySize < 1 or zSize < 1:
   exitError('Positive sizes must be entered for the reconstructions')

makeVolStacks = PipGetInteger('MakeVolumeStacks', 0)
if not PipGetErrNo() and makeVolStacks < 5:
   exitError('The volume stack entry must be at least 5')
numZlevels = PipGetInteger('NumberOfZLevels', 0)
maxZextent = PipGetFloat('ExtentOfZLevelsInNm', 0.)
if numZlevels and maxZextent:
   exitError('You cannot enter both a number of Z levels and their extent')
doCTF = numZlevels > 0 or maxZextent > 0
invertZoffsets = PipGetInteger('InvertZLevelOffsets', -1)
adjustForZShift = PipGetBoolean('AdjustForAlignZShift', 0)

if doCTF:
   ctfComFile = getOrDeriveComFile('CorrectionComFile', 'ctfcorrection', comFile,
                                   'CTF correction')
   ctfLines = readTextFile(ctfComFile, 'CTF correction command file')
   checkList.append((ctfComFile, 'CTF correction command file'))

doErase = PipGetBoolean('EraseFiducials', 0)
if doErase:
   eraseComFile =  getOrDeriveComFile('GoldEraserComFile', 'golderaser', comFile,
                                      'erasing gold')
   checkList.append((eraseComFile, 'gold erasing command file'))

# Determine filtering
doFilter = PipGetBoolean('FilterIn2D', 0)
if doFilter:
   mtfComFile = getOrDeriveComFile('2DFilterComFile', 'mtffilter', comFile, 
                                   '2D filtering')
   mtfLines = readTextFile(mtfComFile, '2D filtering command file')
   
(comExt, ifDual, rawRoot, typeExt, rawExt) = findRootAxisAndExtensions()
axisLet = setAxisLetter(rootName, ifDual)

# Get raw stack name 
stackName = PipGetString('RawStackFile', '')
if not stackName:
   (rawExt, numCandid) = getRawExtensionFallback(rawExt, rootName)

   if not numCandid or numCandid > 1:
      exitError('The raw stack name must be entered, it cannot be deduced')
   
   stackName = rootName + '.' + rawExt

# Get the aligned stack name, or give up and assume it
if typeExt:
   aliName = datasetFilename('.ali', rootName, typeExt)
else:
   tiltLines = readTextFile(comFile)
   aliName = optionValue(tiltLines, 'inputproj', 0, 1)
   if not aliName:
      aliName = rootName + '.ali'

# Get the axis angle and whether X/Y are transposed
(axisAngle, transposeXY) = getAxisAngleAndTranspose(axisLet)

# See if using raw input
useRawInput = PipGetBoolean('UseUnalignedImages', 0)
splitCorrSize = ''
if useRawInput:
   (aliXformFile, rawBinning, rawPixSize) = getEssentialRawOptions(rawRoot)
   try:
      (nxCorr, nyCorr, nzCorr) = getmrcsize(stackName)
   except ImodpyError:
      exitFromImodError(progname)

   splitCorrSize = fmtstr('-size {},{},{}', nxCorr // rawBinning, nyCorr // rawBinning,
                           nzCorr)

# See if remaking aligned stack
newAliBinning = PipGetInteger('NewAlignedBinning', 0)
if newAliBinning > 0:
   if useRawInput:
      exitError('You cannot use unaligned images and specify a new aligned stack binning')
   newstComFile = getOrDeriveComFile('NewstackComFile', 'newst', comFile, 
                                     'making new aligned stack')
   newstLines = readTextFile(newstComFile)

   # Make modifications now to binning and to size if it is present
   sedcom = sedDelAndAdd('BinByFactor', newAliBinning, 'TransformFile')
   aliOutSize = optionValue(newstLines, 'SizeToOutputInXandY', INT_VALUE)
   oldAliBin = optionValue(newstLines, 'BinByFactor', INT_VALUE, numVal = 1)
   if not oldAliBin:
      oldAliBin = 1
   if aliOutSize:
      for ind in (0, 1):
         aliOutSize[ind] = (aliOutSize[ind] * oldAliBin + newAliBinning - 1) // \
            newAliBinning
      sedcom.append(sedModify('SizeToOutputInXandY', 
                    fmtstr('{},{}', aliOutSize[0], aliOutSize[1])))
   newstLines = pysed(sedcom, newstLines)

   runLines = []
   gotRun = False

   # Get the lines for running it
   for line in newstLines:
      if gotRun and line.startswith('$'):
         break
      if not gotRun and line.startswith('$') and 'newstack' in line:
         gotRun = True
      elif gotRun:
         runLines.append(line)

   if not gotRun:
      exitError('Could not find newstack line in command file')

   # Make a stack with one section in order to get the size and header correct
   sedcom = sedDelAndAdd('SectionsToRead ', 0, 'TransformFile')
   runLines = pysed(sedcom, runLines)
   try:
      newstOut = runcmd('newstack -StandardInput', runLines)
   except ImodpyError:
      exitFromImodError(progname)

# Check that all the files exist
checkList += [(stackName, 'raw stack'),
              (volName, 'modeled volume'), (comFile, 'command file'),
              (centerFile, 'center position file')]
if not useRawInput and newAliBinning <= 0:
   checkList.append((aliName, 'aligned stack'))
if doCTF and adjustForZShift:
   if axisLet == None:
      exitError('Cannot determine axis for finding align command file')
   alignCom = 'align' + axisLet + '.com'
   checkList.append((alignCom, 'align command file'))

for (name, descrip) in checkList:
   if not os.path.exists(name):
      exitError('The ' + descrip + ', ' + name + ', does not exist')

# Check and create output directory
if outDir:
   if os.path.exists(outDir):
      if not os.path.isdir(outDir):
         exitError('The specified name for output directory already exists and is ' +\
                      'not a directory')
   else:
      try:
         os.mkdir(outDir)
      except OSError:
         exitError('Making directory for output, ' + outDir)

      try:
         outDir = os.path.relpath(outDir)
      except Exception:
         pass
      if os.path.isabs(outDir):
         prnstr('WARNING: ' + progname + ' - The output directory cannot be converted' + \
                ' to a relative path and may not work on other machines')

if comDir:
   if os.path.exists(comDir):
      if not os.path.isdir(comDir):
         exitError('The specified name for command file directory already exists and ' +\
                   'is not a directory')
   else:
      try:
         os.mkdir(comDir)
      except OSError:
         exitError('Making directory for command files, ' + comDir)


# Get possible entries for runs per chunk and # of processors
numProc = PipGetInteger('ProcessorNumber', 0)
zeroProcEntered = not PipGetErrNo() and numProc == 0
numRunsPerChunk = PipGetInteger('RunsPerChunk', 10)
if not PipGetErrNo() and numProc > 0:
   exitError('You cannot enter both -proc and -runs')

fullRec = None
(pid, cleanList, pointList, aliBinning, reorient, tiltLines, \
    nxRaw, nyRaw, nzRaw, pixXraw, pixYraw, pixZraw, \
    nxAli, nyAli, nzAli, pixXali, pixYali, pixZali, origXali, origYali, \
    origZali, nxVol, nyVol, nzVol, pixXvol, pixYvol, pixZvol, origXvol, \
    origYvol, origZvol, nxFull, nyFull, nzFull, pixXfull, pixYfull, pixZfull, \
    origXfull, origYfull, origZfull) = \
    getPointsAndHeaders(modelFile, objList, pointFile, progname, stackName, aliName, \
                        volName, fullRec, enteredOrient, reorientIn, comFile, \
                        useRawInput)

# Fix nzAli to be raw size for new ali binning or raw input, for raw it is used
# only for getting the maxSlices
if newAliBinning > 0 or useRawInput:
   nzAli = nzRaw

if optionValue(tiltLines, 'ExpandedByFactor', FLOAT_VALUE, numVal = 1):
   exitError('Reconstructions with an expansion factor applied are not (yet) supported')

if doCTF:
   rawArg = ''
   if useRawInput:
      rawArg = stackName
   (nx, ny, nz, xaxisTilt, tiltUseGPU, ctfXtilt, ctfUseGPU, ctfPixSize, ctfInput,
    ctfOutput) = getCTFoptionsCheckIfCorrected(progname, tiltLines, ctfLines, rawArg, \
                                               doFilter)
   (aliRoot, aliExt) = os.path.splitext(ctfOutput)
   if not useRawInput:
      splitCorrSize = fmtstr('-size {},{},{}', nxAli, nyAli, nzAli)
   if ctfUseGPU == None or ctfUseGPU < 0:
      ctfUseGPU = tiltUseGPU

   zShiftAdjustment = 0.
   if adjustForZShift:
      aliLines = readTextFile(alignCom, 'align command file')
      alignZshift = optionValue(aliLines, 'AxisZShift', FLOAT_VALUE, numVal = 1)
      shiftArr = optionValue(tiltLines, 'shift', 2, 1)
      if shiftArr and len(shiftArr) > 1:
         startingZshift = shiftArr[1]

      zShiftAdjustment = alignZshift + startingZshift

else:
   ctfOutput = aliName
   ctfInput = aliName
   ctfPixSize = pixXraw
   tiltUseGPU = optionValue(tiltLines, 'UseGPU', 1, 1, numVal = 1)
   if tiltUseGPU == None:
      tiltUseGPU = -1

# Restore the previous aligned stack
if newAliBinning > 0:
   cleanupFiles([aliName])
   if os.path.exists(aliName + '~'):
      try:
         os.rename(aliName + '~', aliName)
      except Exception:
         pass

(aliRoot, aliExt) = os.path.splitext(ctfOutput)
overrideGPU = PipGetInteger('WhenToUseGPU', -1)
if not PipGetErrNo():
   if overrideGPU == 0:
      ctfUseGPU = tiltUseGPU = -1
   elif overrideGPU == 1:
      ctfUseGPU = tiltUseGPU = 0
   elif overrideGPU > 1:
      ctfUseGPU = 0
      tiltUseGPU = -1

(comRoot, ext) = os.path.splitext(comFile)
if not comExt:
   comExt = ext
comRoot += '-sub'
rootWithDir = comRoot
optComDir = ''
if comDir:
   rootWithDir = comDir + '/' + comRoot
   optComDir = '-dir "' + comDir + '"'
cleanChunkFiles(rootWithDir)
comNum = 1
tempStacks = ''

# Handle setting all the items for "aligned stack" when using raw input
# The ...ali values from the above calls are actually raw stack values
if useRawInput:
   checkForDistortion(axisLet, 1, progname)
   aliBinning = rawBinning
   if not rawPixSize:
      rawPixSize = getFallbackRawPixel(ctfPixSize, axisLet, comExt)
   headerPixSize = ctfPixSize * rawBinning
   if transposeXY:
      (nxAli, nyAli) = (nyAli, nxAli)
      origXali += pixXraw * (nxAli - nxRaw) / 2.
      origYali += pixYraw * (nyAli - nyRaw) / 2.
   nxAli = nxAli // rawBinning
   nyAli = nyAli // rawBinning
   pixXali *= rawBinning
   pixYali *= rawBinning
   ctfPixSize = rawPixSize * rawBinning

# Make command file for making new aligned stack
# adjust pixel size from com file for CTF correction by change in binning
if newAliBinning > 0:
   if debug:
      prnstr(fmtstr('newst {} -> {}', 
                    optionValue(newstLines, 'InputFile', STRING_VALUE), 
                    optionValue(newstLines, 'OutputFile', STRING_VALUE)))
   writeTextFile(makeNextComName(True), newstLines)
   ctfPixSize = (ctfPixSize * newAliBinning) / oldAliBin

# If using raw input, set up input name and optional binning com file
if useRawInput:
   ctfInput = stackName
   if rawBinning > 1:
      ctfInput = fmtstr('{}_red{}_tmp{}', aliRoot, rawBinning, aliExt)
      tempStacks += ' ' + ctfInput
      newstLines = [fmtstr('$newstack -ftreduce {} {} {}', rawBinning, stackName,
                           ctfInput)]
      if debug:
         prnstr(fmtstr('newstack reduce {} -> {}', stackName, ctfInput))
      writeTextFile(makeNextComName(True), newstLines)

# If filtering, do it in an initial command file
if doFilter:
   filtInput = ctfInput
   ctfInput = fmtstr('{}_filt_tmp{}', aliRoot, aliExt)
   tempStacks += ' ' + ctfInput
   mtfsed = [sedModify('InputFile', filtInput, delim = '|'),
             sedModify('OutputFile', ctfInput, delim = '|'),
             sedModify('PixelSize', ctfPixSize, delim = '|')]
   mtfMod = pysed(mtfsed, mtfLines, delim = '|')
   if debug:
      prnstr(fmtstr('mtffilter {} -> {}', filtInput, ctfInput))
   writeTextFile(makeNextComName(True), mtfMod)

if not doCTF:
   ctfOutput = ctfInput

if doErase:
   eraseLines = readTextFile(eraseComFile, 'Gold erasing command file')
   eraseName = fmtstr('{}_erase_tmp{}', aliRoot, aliExt)
   eraseSed = [sedModify('InputFile', ctfOutput, delim = '|'),
            sedModify('OutputFile', eraseName, delim = '|')]
   if useRawInput:
      rawEraseFid = backTransformEraseModel(eraseLines, aliXformFile, 10. * rawPixSize)
      eraseSed.append(sedModify('ModelFile', rawEraseFid, delim = '|'))
      tempStacks += ' ' + rawEraseFid

   eraseMod = pysed(eraseSed, eraseLines, delim = '|')
   eraseMod.append(fmtstr('$b3drename "{}" "{}"', eraseName, ctfOutput))
   
thickness = ySize
numSlices = zSize // aliBinning
if reorient:
   thickness = zSize
   numSlices = ySize // aliBinning

# Get values needed for making volume stack and model
xCenter = (xSize // aliBinning) / 2.
yCenter = (ySize // aliBinning) / 2.
zFinal = zSize // aliBinning
zCenter = zFinal / 2. - 0.5

# ASSUMING CENTER ALIGNED STACK, could use origins to overcome this
if transposeXY:
   (nxRaw, nyRaw) = (nyRaw, nxRaw)
sssx = (nxRaw - nxAli * aliBinning) // 2
sssy = (nyRaw - nyAli * aliBinning) // 2

sedcomBase = [sedModify('IMAGEBINNED', aliBinning, delim = '|'),
              sedModify('FULLIMAGE', fmtstr('{} {}', nxRaw, nyRaw), delim = '|'),
              sedModify('SUBSETSTART', fmtstr('{} {}', sssx, sssy), delim = '|'),
              sedModify('THICKNESS', thickness, delim = '|'),
              '|savework|d'] + \
   sedDelAndAdd('WIDTH', xSize, 'THICKNESS', delim = '|') + \
   sedDelAndAdd('XSubsetLoadRatio', 1.2, 'THICKNESS', delim = '|')

if ctfOutput != aliName:
   sedcomBase.append(sedModify('InputProjections', ctfOutput, delim = '|'))
if useRawInput:
   sedcomBase += sedDelAndAdd('UseUnalignedImages', 1, 'THICKNESS', delim = '|') + \
                 sedDelAndAdd('AlignTransformFile', aliXformFile, 'THICKNESS', 
                              delim = '|') + \
                 [sedModify('IMAGEBINNED', str(rawBinning), delim = '|')]

if typeExt and typeExt != 'mrc':
   sedcomBase.append('|IMOD_OUTPUT_FORMAT|d')

if doCTF:
   # Work out GPU for CTF which can be independent: if it is on it stays on
   # Do CTF in parallel if multiple procs explicitly entered and tilt is using GPUs,
   # or if not using a GPU for CTF and no number was entered - assume 8 in thta case
   maxSlices = 0
   procForCTF = numProc
   if numProc == 0:
      procForCTF = 8
   if numProc > 1 or (numProc == 0 and ctfUseGPU < 0):
      numChunks = 3 * procForCTF
      maxSlices = max(1, (nzAli + numChunks - 1) // numChunks)
                     
# Get format string for particle names to get equal digits on all
numDec = 1
numPts = len(pointList)
delMatch = '-[0-9]'
while numPts > 9:
   numDec += 1
   numPts = numPts // 10
   delMatch += '[0-9]'
numFormat = '-{:0' + str(numDec) + 'd}'
numPts = len(pointList)

# Loop through all the points, getting their command file text and z shifts
numPtTot = 0
minZshift = 1.e10
maxZshift = -1.e10
shiftList = []
sedList = []
volNames = []
for ptNum in range(numPts):
   numUse = numPtTot + 1
   if skipVolNumbers:
      numUse = ptNum + 1

   chunkBase = rootName + fmtstr(numFormat, numUse)

   # Use a forward slash so output is stable and tests work with Windows python
   if outDir:
      chunkBase = outDir + '/' + chunkBase
   chunkName = chunkBase + '.mrc'
   if reorient:
      chunkName = chunkBase + '.tmp'
   sedcom = copy.deepcopy(sedcomBase)
   sedcom.append(sedModify('OutputFile', chunkName, delim = '|'))
   point = pointList[ptNum]

   # Convert points from a point list by the header transformation to match scaled
   # values that came in from model conversion
   if not modelFile:
      point[0] = point[0] * pixXvol - origXvol;
      point[1] = point[1] * pixYvol - origYvol;
      point[2] = point[2] * pixZvol - origZvol;
   
   # Now need to get slice range and X/Z shifts.  X is easy and invariant
   xInAli = (point[0] + origXali) / pixXali
   xShift = aliBinning * (nxAli / 2. - xInAli)

   # For no reorientation, Y comes from Z, Y from Y; z shift is negative of coordinate
   if reorient == 0:
      yInAli = (point[2] + origYali) / pixYali
      zShift = -aliBinning * (point[1]) / pixXali

   # For rotation, Y comes from Y, Z from inversion of Y
   elif reorient < 0:
      yInAli = (point[1] + origYali) / pixYali
      zShift = aliBinning * (point[2]) / pixXali

   # For flip, the origins were not swapped in the header, so undo the origin that was
   # applied and adjust by origin that should have been applied
   else:
      yInAli = (point[1] + origYvol - origZvol - origYali) / pixYali
      zShift = -aliBinning * (point[2] + origZvol - origYvol) / pixXali


   # Get the slice range, skip if too far out of range
   sliceStart = int(round(yInAli - numSlices / 2.)) * aliBinning
   sliceEnd = sliceStart + numSlices * aliBinning - 1
   if yInAli < numSlices / 6. or yInAli > nyAli - numSlices / 6.:
      prnstr(fmtstr('WARNING: {} - Point # {} is skipped; it requires too many Y ' +\
                  'slices outside the reconstructable range for this aligned stack',
                    progname, ptNum + 1))
      continue

   # And set up for blank slices if partly out of the range
   # Get the binned slice range that Tilt will use.  It uses slices numbered from 1
   # and rounds up when binning, but (sl0 + 1 + bin - 1) / bin = sl0 / bin + 1
   # numbered from 1, which is oddly just sl0 / bin numbered from 0.
   newstRange = ''
   binSliceStart = sliceStart // aliBinning
   binSliceEnd = sliceEnd // aliBinning
   if sliceStart < 0 or binSliceEnd >= nyAli:
      if sliceStart < 0:
         numBlank = numSlices - (binSliceEnd + 1)
         newstRange = fmtstr('{}-{}', -numBlank, binSliceEnd)
         sliceStart = 0
      else:
         numBlank = numSlices - (nyAli - binSliceStart)
         newstRange = '0-' + str(numSlices - 1)
         sliceEnd = nyAli * aliBinning - 1
      prnstr(fmtstr('WARNING: {} - Point # {} is near the edge of the aligned stack' +\
                    ' in Y and requires {} blank slices', progname, ptNum + 1,
                    numBlank))

   # Finish the sed com, process the lines
   sedcom += sedDelAndAdd('SHIFT', fmtstr('{} {}', round(xShift, xzShiftDecimals),
                                          round(zShift, xzShiftDecimals)), 'THICKNESS',
                          delim = '|')
   sedcom += sedDelAndAdd('SLICE', fmtstr('{} {}', sliceStart, sliceEnd), 'THICKNESS',
                          delim = '|')
   if overrideGPU >= 0:
      sedcom += sedDelAndAdd('UseGPU', tiltUseGPU, 'THICKNESS', delim = '|')
   sedLines = pysed(sedcom, tiltLines, delim = '|')

   # Add blank slices if needed
   if newstRange:
      sedLines += [fmtstr('$newstack -blank -sec {} "{}" "{}"', newstRange, chunkName,
                          chunkBase + '.tmp2'), 
                   '$b3dremove "' + chunkName + '"']
      chunkName = chunkBase + '.tmp2'
   
   # Add final reorientation
   if reorient:
      oper = 'rotx'
      if reorient > 0:
         oper = 'flipyz'
      sedLines += [fmtstr('$clip {} "{}" "{}"', oper, chunkName, chunkBase + '.mrc'), 
                   '$b3dremove "' + chunkName + '"']

   # Maintain min/max, save the lines and the z shift
   minZshift = min(minZshift, zShift)
   maxZshift = max(maxZshift, zShift + 0.01)
   sedList.append(sedLines)
   shiftList.append(zShift)
   numPtTot += 1
   if makeVolStacks:
      volNames.append(chunkBase + '.mrc')

if not numPtTot:
   exitError('There are no points to do because all points are being skipped')
      
# if doing CTF, figure out the range of Z etc
if doCTF:
   ubPixSize = ctfPixSize / aliBinning
   fullZpixels = maxZshift - minZshift
   fullZnm = fullZpixels * ubPixSize
   if maxZextent > 0:
      numZlevels = int(math.ceil(fullZnm / maxZextent))
   if numZlevels < 2 and maxZextent > 0:
      prnstr(fmtstr('WARNING: {} - The Z extent of {:.0f} nm will result in only one Z' +\
                    ' level because the range of center positions is {:.0f} nm', progname,
                    maxZextent, fullZnm))

   zExtentNm = fullZnm / numZlevels
   prnstr(fmtstr('CTF corrections will be computed at {} levels that are {:.0f} nm thick',
                 numZlevels, zExtentNm))
   delZpixels = zExtentNm / ubPixSize
   numInLevel = []
   ptIndList = []
   invertOpt = optionValue(ctfLines, 'InvertTiltAngles', BOOL_VALUE)
   if invertZoffsets < 0 and invertOpt:
      invertZoffsets = 1
   defocusTol = optionValue(ctfLines, 'DefocusTol', INT_VALUE, numVal = 1) 
   zNmInt = int(round(zExtentNm))
   if defocusTol:
      defocusTol = min(defocusTol, zNmInt)
   else:
      defocusTol = zNmInt

   # Test for X tilt consistency and if it matters
   checkXtiltCtfVsRec(xaxisTilt, ctfXtilt, warnXtiltCrit, 0.1 * nyAli * pixXali,
                       zExtentNm, 'level', progname)

   # Find the number in each level
   for level in range(numZlevels):
      shiftLow = minZshift + level * delZpixels
      shiftHigh = shiftLow + delZpixels
      numTmp = 0
      levelInds = []
      for ind in range(numPtTot):
         if shiftList[ind] >= shiftLow and shiftList[ind] < shiftHigh:
            numTmp += 1
            levelInds.append(ind)
      numInLevel.append(numTmp)
      ptIndList.append(copy.deepcopy(levelInds))

else:
   numZlevels = 1
   delZpixels = maxZshift - minZshift
   numInLevel = [numPtTot]
   ptIndList = [list(range(numPtTot))]

# No processors entered and using GPU for tilt, assume 1
if numProc == 0 and tiltUseGPU != None and tiltUseGPU >= 0:
   numProc = 1

# Loop on levels if any
for level in range(numZlevels):
   numPtsLvl = numInLevel[level]
   if not numPtsLvl:
      continue
   maxChunkLvl = maxChunks
   numOptimal = 1000
   if doCTF:
      maxChunkLvl = int(round((0.9 * maxChunks * numPtsLvl) / numPtTot))
      numOptimal = (numOptimal * numPtsLvl) // numPtTot
      
   if numProc > 1 or zeroProcEntered:

      # If # of processors entered, try for a large # of chunks per processor but lower it
      # to give fewer than 1000 chunks; in any case limit chunks to maximum and to # pts
      numChunkPerProc = maxChunkPerProc
      while numChunkPerProc >= minChunkPerProc:
         numChunks = min(numPtsLvl, numChunkPerProc * numProc, maxChunkLvl)
         if numChunks < numOptimal:
            break;
         numChunkPerProc -= 1

   else:

      # Otherwise base it on default or entered # of runs per chunks; but it must be
      # raised if that gives too many chunks, or lower it if it is too few
      minRuns =  numPtsLvl // maxChunkLvl + 1
      minChunksLvl = min(numPtsLvl, minChunks)
      runsPerChunkLvl = max(numRunsPerChunk, minRuns)
      numChunks = max(minChunksLvl, (numPtsLvl + runsPerChunkLvl - 1) // runsPerChunkLvl)

   runsPerChunkLvl = numPtsLvl // numChunks

   if doCTF:
      offsetPix = ((level + 0.5) * delZpixels + minZshift + zShiftAdjustment) / aliBinning
      if invertZoffsets > 0:
         offsetPix = -offsetPix
      ctfsed = [sedModify('InputStack', ctfInput, delim = '|')] +\
               sedDelAndAdd('OffsetInZ', round(offsetPix, 2), 'DefocusFile',
                            delim = '|')+\
         sedDelAndAdd('UseGPU', ctfUseGPU, 'DefocusFile', delim = '|') + \
         sedDelAndAdd('DefocusTol', fmtstr('{}', defocusTol), 'DefocusFile',
                      delim = '|')

      if useRawInput or newAliBinning > 0:
         ctfsed.append(sedModify('PixelSize', ctfPixSize, delim = '|'))

      if useRawInput:
         ctfsed += ['|TransformFile|d'] + \
                   sedDelAndAdd('XAxisTilt', xaxisTilt, 'DefocusFile', delim = '|') + \
                   sedDelAndAdd('AxisAngle', axisAngle, 'DefocusFile', delim = '|')

      ctfMod = pysed(ctfsed, ctfLines, delim = '|')
      if debug:
         prnstr(fmtstr('ctfcorrection  {} -> {}', ctfInput, 
                       optionValue(ctfMod, 'OutputFileName', STRING_VALUE)))
   
      if maxSlices:
         ctfComOut = 'ctfcorrection.tmp.com'
         writeTextFile(ctfComOut, ctfMod)
         try:
            splitLines = runcmd(fmtstr('splitcorrection -i {} -o -m {} -b {} -uni ' +\
                                       '{} {} -r {} {}', comNum, maxSlices, boundPixels, 
                                       splitCorrSize, optComDir, comRoot, ctfComOut))
            numAdded = findSplitComNumber(splitLines, 'output of splitcorrection')
            comNum += numAdded

         except ImodpyError:
            exitFromImodError(progname)

      else:
         ctfComOut = makeNextComName(True)
         writeTextFile(ctfComOut, ctfMod)

   if doErase:
      if debug:
         prnstr(fmtstr('eraser  {} <-> {}', ctfOutput, eraseName))
      eraseName = makeNextComName(True)
      writeTextFile(eraseName, eraseMod)
      
   # Loop on chunks
   indInLevel = 0
   for chunk in range(numChunks):
      numInChunk = runsPerChunkLvl
      if chunk < numPtsLvl % numChunks:
         numInChunk += 1

      comName = makeNextComName(False)
      chunkLines = []
      if typeExt and typeExt != 'mrc':
         chunkLines = ['$setenv IMOD_OUTPUT_FORMAT MRC']
      for indInChunk in range(numInChunk):
         ptInd = ptIndList[level][indInLevel]
         if debug:
            prnstr(fmtstr('tilt  {} -> {}', 
                          optionValue(sedList[ptInd], 'InputProjections', STRING_VALUE),
                          optionValue(sedList[ptInd], 'OutputFile', STRING_VALUE)))
         indInLevel += 1

         # Add lines to chunk
         chunkLines += sedList[ptInd]

      # Write the file
      writeTextFile(comName, chunkLines)

   if doCTF:
      comName = makeNextComName(True)
      writeTextFile(comName, [fmtstr('$b3dremove {}', ctfOutput)])

if makeVolStacks:

   # Get number of stacks and format for numbering
   numStacks = (numPtTot + makeVolStacks - 1) // makeVolStacks
   numDec = 1
   numPts = numStacks
   while numPts > 9:
      numDec += 1
      numPts = numPts // 10
   numFormat = '-vol{:0' + str(numDec) + 'd}'

   # Make one sync file; loop on stacks
   comName = makeNextComName(True)
   comLines = []
   for group in range(numStacks):

      # Get range of subvols and name for output
      (start, end) = balancedGroupLimits(numPtTot, numStacks, group)
      stackRoot = rootName + fmtstr(numFormat, group + 1)
      if outDir:
         stackRoot = outDir + '/' + stackRoot
      comLines += ['$setenv IMOD_OUTPUT_FORMAT MRC',
                  '$newstack -StandardInput',
                  'OutputFile ' + stackRoot + '.mrc']

      # Add the input file names and accumulate point lines
      ptLines = []
      for ind in range(start, end + 1):
         comLines.append('InputFile ' + volNames[ind])
         contTime = ind + 1 - start
         ptLines.append(fmtstr('1 {} {} {} {} {}', contTime, xCenter, yCenter, zCenter,
                               contTime))

      # Write point file, add commands to convert to model, and remove point file
      writeTextFile(stackRoot + '.pt', ptLines)
      comLines.append(fmtstr('$alterheader -volstack {} "{}.mrc"', zFinal, stackRoot))
      comLines.append(fmtstr('$point2model -scat -time -sphere 3 -image "{0}.mrc"' +\
                             ' "{0}.pt" "{0}.mod"', stackRoot))
      comLines.append('$b3dremove "' + stackRoot + '.pt"')
      
   # After all runs, remove the subvols
   cleanRoot = rootName + delMatch
   if outDir:
      cleanRoot = outDir + '/' + cleanRoot
   comLines.append('$b3dremove -g "' + cleanRoot + '*.mrc"')
   writeTextFile(comName, comLines)

# Write finish file
finlines = [fmtstr('$b3dremove -g "{0}-[0-9][0-9][0-9]*.com*" ' + \
                     '"{0}-[0-9][0-9][0-9]*.log*" "{0}-finish*.com*"', rootWithDir)]
if tempStacks:
   finlines.append('$b3dremove ' + tempStacks)
if doCTF and maxSlices > 0:
   finlines.append('$b3dremove ' + ctfComOut)
writeTextFile(rootWithDir + '-finish.com', finlines)
prnstr(fmtstr('Created {} command files for {} subtomograms; run them with:', comNum,
              numPtTot))
prnstr(fmtstr('    "subm {0}*.com"   or   "processchunks ... {0}"', rootWithDir))
if doCTF and maxSlices:
   cleanupFiles([ctfComOut])
sys.exit(0)
