#
#

set isWeed [string match head*.cluster [exec uname -n]]
proc GetRfFreqControllawName {} {
    global isWeed
    if $isWeed {
        return "S:RC:RfFreqControlLawC"
    } else {
        return "S:rfFreqControlLawRC"
    }
}
set bumpDirectory /home/helios/oagData/sr/bpmOffsets/bumpConfigurations
set templateDir   /home/helios/oagData/sr/bpmOffsets/templates
set dataDir       /home/helios/oagData/sr/bpmOffsets/results
set latticeDir    /home/helios/oagData/sr/lattices/default/
set offsetSavedConfig ""

proc UseSextupoleBasedMethod {args} {
    set P2Mode "xy"
    set P4Mode "x"
    set bpm ""
    if {[APSStrictParseArguments {bpm P2Mode P4Mode}]} {
        return -code error "UseSextupoleBasedMethod: problem with arguments"
    }

    # Five return values:
    # 0 = Use quadrupole method
    # 1 = Use sextupole for x only
    # 2 = Use sextupole for y only
    # 3 = Use sextupole for x and y 
    # -1 = Skip this measurement (for vertical plane measurement in both-planes mode)
    
    set useSext 0
    if {[string match *P2? $bpm]} {
        switch $P2Mode {
            xy {
                # Yes, I'm using bitwise operations
                set useSext [expr 1|2]
                if [string match *y $bpm] {
                    set useSext -1
                }
            }
            x {
                if [string match *x $bpm] {
                    set useSext 1
                }
            }
            y {
                if [string match *y $bpm] {
                    set useSext 2
                }
            }
            default {
            }
        }
    } elseif {[string match *P3? $bpm]} {
        set useSext [expr 1|2]
        if [string match *y $bpm] {
            set useSext -1
        }
    } elseif {[string match *P4? $bpm]} {
        switch $P4Mode {
            xy {
                set useSext [expr 1|2]
                if [string match *y $bpm] {
                    set useSext -1
                }
            }
            x {
                if [string match *x $bpm] {
                    set useSext 1
                }
            }
            y {
                if [string match *y $bpm] {
                    set useSext 2
                }
            }
            default {
            }
        }
    }
    return $useSext
}

proc PerformQuadrupoleOffsetMeasurement {args} {
    global abortOffsetMeasurement bumpDirectory templateDir dataDir
    
    set bpm ""
    set quadLimitList ""
    set quadSteps 3
    set negativeOnly 1
    set positiveOnly 1
    set bumpLimitList ""
    set bumpStepsList ""
    set bpmUnitFactor 1
    set pause 5.0
    # average over 5 seconds by default.
    set num2Ave 50
    # average the correctors, S35DCCT, and the particular BPM msAve once
    # at each corrector bump.
    set statusCallback "puts"
    if {[APSStrictParseArguments {bpm quadLimitList quadSteps negativeOnly positiveOnly bumpLimitList bumpStepsList num2Ave bpmUnitFactor pause statusCallback}]} {
        return -code error "PerformQuadrupoleOffsetMeasurement: problem with arguments"
    }
    
    if ![string length $bpm] {
        eval $statusCallback {"No bpm specified"}
        return
    }
    if $abortOffsetMeasurement {
        eval $statusCallback {"Offset measurement aborted at bpm $bpm."}
        return
    }

    eval $statusCallback {"Using quadrupole-based measurement for $bpm"}

    if $quadSteps<3 {
        eval $statusCallback {"Too few steps for quadrupole current: setting to 3"}
        set quadSteps 3
    }
    
    set pendIOTime 10

    set year ""
    set month ""
    set day ""
    APSDateBreakDown -monthVariable month -yearVariable year -twoDigitYear 0 -dayVariable day
    set dateSubdirectory $year-$month-$day
    
    # Here, bpm has values S01A:P1x, S01A:P2y, etc
    # sector has values S01, S02, etc.
    # bpmType has values A:P1 A:P2, etc
    # bpmDir has values AP1x AP2x, etc
    regexp {(S[0-4][0-9])([AB]:P.)([xy])} $bpm all sector bpmType coord
    regsub {:} $bpmType$coord {} bpmDir
    regsub {:} $bpm {} meas
    eval $statusCallback {"bpm=$bpm, sector=$sector, bpmType=$bpmType, coord=$coord, bpmDir=$bpmDir, meas=$meas"}

    if ![file exists [set bpmMeasFile $templateDir/quadrupoleMethod/bpmPVs-$coord.mon]] {
        return -code error "PerformQuadrupoleOffsetMeasurement: bpm measurement file $bpmMeasFile not found"
    }
    
    # puts $bpmDir
    createBPMOffsetDirectory -dateSubdirectory $dateSubdirectory -statusCallback $statusCallback -bpmDir $bpmDir
    cd $dataDir/$dateSubdirectory/$bpmDir
    if [catch {set lastFiles [lindex [lsort -decreasing -ascii [glob $meas-??-*.sdds{,.gz}]] 0] } result] {
        # if no files exist then the first generation is 01.
        set nextGeneration [format "%02ld" 1]
    } else {
        regsub $meas-(..)-.*\.sdds(\.gz)?\$ $lastFiles {\1} lastGeneration
        scan $lastGeneration {%ld} intGen
        incr intGen
        set nextGeneration  [format "%02ld" $intGen]
    }
    set rootname $meas-$nextGeneration
    
    eval $statusCallback {"Using rootname $rootname."}
    
    set measurement $meas
    set bpmDev ${sector}$bpmType
    eval $statusCallback {"bpmDev = $bpmDev"}
    
    set sectorBumpFile $bumpDirectory/${sector}-${coord}-LocalBumps.cokn-differential
    if ![file exists $sectorBumpFile] {
        return -code error "PerformQuadrupoleOffsetMeasurement: bump file $sectorBumpFile not found"
    }

    if [catch {exec sddsprocess $sectorBumpFile $rootname.bump -match=parameter,BPMName=$bpmDev -match=parameter,MagnetName=*Q* \
               -redefine=parameter,Gain,1} result] {
        return -code error "PerformQuadrupoleOffsetMeasurement: $result"
    }
    catch {exec sddsprintout -parameter -column $rootname.bump} result
    eval $statusCallback {"$result"}
    
    if ![string match "${sector}\[AB]:Q?" [set quadName [exec sdds2stream -parameter=MagnetName $rootname.bump]]] {
        return -code error "PerformQuadrupoleOffsetMeasurement: didn't find a valid quadrupole name corresponding to BPM $bpmDev ($quadName)"
    }

    eval set quadFamily [os editstring Z: $quadName]

    array set quadLimitArray $quadLimitList
    set quadLimit $quadLimitArray($quadFamily)
    eval $statusCallback {"Using quadrupole current limit of $quadLimit for family $quadFamily."}

    array set bumpLimitArray $bumpLimitList
    set bumpLimit [expr abs($bumpLimitArray($quadFamily,$coord))]
    eval $statusCallback {"Using bump limit of $bumpLimit for family $quadFamily in plane $coord."}

    array set bumpStepsArray $bumpStepsList
    set bumpSteps $bumpStepsArray($quadFamily)
    eval $statusCallback {"Using $bumpSteps bump steps for family $quadFamily."}
    
    if $bumpSteps<3 {
        eval $statusCallback {"Too few steps for bump current: setting to 3"}
        set bumpSteps 3
    }

    if $negativeOnly {
        set quadLoLimit [expr -1.0 * $quadLimit]
        set quadHiLimit 0
    } elseif $positiveOnly {
        set quadLoLimit 0
        set quadHiLimit $quadLimit
    } else {
        set quadLoLimit [expr -1.0 * $quadLimit]
        set quadHiLimit $quadLimit
    }

    set tmpRoot /tmp/[APSTmpString]
    if [string match "S*Q8" $quadName] {
        # Q8 requires special handling since it is a TGD. The main PS changes the dipole field as well as the
        # quadrupole
        eval $statusCallback {"Special handling for Q8"}
        
        set bumpFile $rootname.bump
        if [catch {exec sddscasr $bumpFile -save $rootname.save} result] {
            return -code error "$result"
        }
        if [catch {set bumpName [exec sdds2stream -parameter=ControlName $bumpFile]} result] {
            return -code error "$result"
        }
        set amplitude $bumpLimit
        if [catch {set bumpLimit [exec sdds2stream -parameter=KnobLimit $bumpFile]} result] {
            return -code error "$result"
        }
        if [expr 0.75*$bumpLimit<$amplitude] {
            # Reduce range of scan to avoid limits
            eval $statusCallback "{Reducing bump height to stay within limits}"
            set amplitude [expr $bumpLimit*0.75]
        }

        set mainCurrent [exec cavget -list=$quadName:PS:SetCurrentC]
        set trimCurrent [exec cavget -list=${quadName}T:PS:SetCurrentC]

        set nominalList [APSCurrentToParametersGradientMagnet -ElementName $quadName -mainCurrent $mainCurrent -trimCurrent $trimCurrent]
        array set value $nominalList
        set K1Nom $value(K1)
        set dipoleFSENom $value(FSE_DIPOLE)
        eval $statusCallback {"K1 = $K1Nom  FSE_DIPOLE = $dipoleFSENom"}
        
        set list1 [APSCurrentToParametersGradientMagnet -ElementName $quadName \
                       -mainCurrent [expr $mainCurrent+$quadLoLimit] -trimCurrent $trimCurrent]
        array set value $list1 
        set K1Start $value(K1)
        set list1 [APSCurrentToParametersGradientMagnet -ElementName $quadName \
                       -mainCurrent [expr $mainCurrent+$quadHiLimit] -trimCurrent $trimCurrent]
        array set value $list1
        set K1End $value(K1)

        set fileList ""
        foreach K1 [APSSequence -start $K1Start -end $K1End -number $quadSteps] \
            index [APSSequence -start 0 -delta 1 -number $quadSteps -type integer] {
                APSAddToTmpFileList -ID PerformQuadrupoleOffsetMeasurement -fileList $tmpRoot.setp$index
                APSDeltaK1ToSetpoints -deltaK1 [expr $K1-$K1Nom] -ElementName $quadName -output $tmpRoot.setp$index
                eval lappend fileList [APSReplicateItem -number $bumpSteps -item $tmpRoot.setp$index]
            }
        if [catch {eval exec sddscombine $fileList -pipe=out -retain=col,ControlName,Current \
                     | sddstranspose -pipe \
                     | sddscombine -pipe -merge \
                     | sddsconvert -pipe -rename=col,${quadName}:PS:SetCurrentC=QuadCurrent \
                     -rename=col,${quadName}T:PS:SetCurrentC=TrimCurrent \
                     | sddsprocess -pipe=in $tmpRoot.scan \
                     "-define=parameter,BumpLimit,$amplitude" \
                     "-define=parameter,BumpSteps,$bumpSteps,type=short" \
                     "{-define=parameter,BumpDelta,BumpLimit 2 * BumpSteps 1 - / }" \
                     "{-define=col,BumpSetting,i_row BumpSteps mod BumpDelta * BumpLimit -}" } result] {
            return -code error "PerformQuadrupoleOffsetMeasurement: $result"
        }
        
        APSAddToTmpFileList -ID PerformQuadrupoleOffsetMeasurement -fileList $tmpRoot.scan
        set totalSteps [expr $bumpSteps*$quadSteps]
        # sddsexperiment command that scans the quadrupole
        set quadBumpScanCmd "sddsexperiment -verbose $templateDir/quadrupoleMethod/Q8QuadBumpScanTemplate.exp -macro=quad=$quadName,totalSteps=$totalSteps,quadTolerance=0.1,trimTolerance=0.025,coord=$coord,pause=$pause,num2Ave=$num2Ave,bpmMeasurements=$bpmMeasFile,valuesFile=$tmpRoot.scan,bumpName=$bumpName,bumpKnobFile=$bumpFile"
        
        if [catch {
            exec cavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.ABRT=1
            exec cavput -list=[GetRfFreqControllawName].ABRT=1
        } result] {
            return -code error "Problem aborting controllaw: $result"
        }
        
        if $abortOffsetMeasurement {
            eval $statusCallback {"Offset measurement aborted at bpm S$sector$bpm."}
            return
        }

        exec sddsmakedataset ${rootname}-quadMeasSettings.sdds \
            -column=QuadrupoleName,type=string -data=$quadName \
            -column=QuadrupoleSteps,type=short -data=$quadSteps \
            -column=LowLimit,type=float,units=A -data=$quadLoLimit \
            -column=HighLimit,type=float,units=A -data=$quadHiLimit \
            -column=Coordinate,type=string -data=$coord \
            -column=NumberToAverage,type=short -data=$num2Ave \
            -column=BumpAmplitude,type=float,units=mm -data=$bumpLimit] \
            -column=BumpSteps,type=short -data=$bumpSteps \
            -column=BPMUnitFactor,type=float -data=1 \
            -column=ScanType,type=string -data=combined
    
        eval $statusCallback {"Doing measurement for ${rootname}-quadMeas.sdds"}
        
        APSExecLog .quadBumpScan -unixCommand "$quadBumpScanCmd $rootname-quadMeas.sdds" -width 120 \
            -callback "set bumpScanStatusVar 1" -abortCallback "set bumpScanStatusVar 2" \
            -cancelCallback "set bumpScanStatusVar 2"

        global bumpScanStatusVar
        after 500
        eval $statusCallback  {"Waiting for sddsexperiment for $rootname"}
        tkwait variable bumpScanStatusVar

        catch {StartControllaw -plane h}
        catch {StartControllaw -plane v}

        ProcessQuadrupoleMeasurement -bpm $bpm -date $dateSubdirectory -index $nextGeneration 
    
        return
    } else {
        # Not a Q8
        set bumpFile $rootname.bump
        if [catch {exec sddscasr $bumpFile -save $rootname.save} result] {
            return -code error "$result"
        }
        if [catch {set bumpName [exec sdds2stream -parameter=ControlName $bumpFile]} result] {
            return -code error "$result"
        }
        set amplitude $bumpLimit
        if [catch {set bumpLimit [exec sdds2stream -parameter=KnobLimit $bumpFile]} result] {
            return -code error "$result"
        }
        if [expr 0.75*$bumpLimit<$amplitude] {
            # Reduce range of scan to avoid limits
            eval $statusCallback "{Reducing bump height to stay within limits}"
            set amplitude [expr $bumpLimit*0.75]
        }

        # sddsexperiment command that scans the bump and quadrupole
        set quadBumpScanCmd "sddsexperiment -verbose $templateDir/quadrupoleMethod/quadBumpScanTemplate.exp -macro=quad=$quadName,quadSteps=$quadSteps,quadLoLimit=$quadLoLimit,quadHiLimit=$quadHiLimit,quadTolerance=0.1,coord=$coord,pause=$pause,num2Ave=$num2Ave,bpmMeasurements=$bpmMeasFile,bumpName=$bumpName,bumpKnobFile=$bumpFile,bumpSteps=$bumpSteps,bumpInitial=-$amplitude,bumpFinal=$amplitude"

        if [catch {
            exec cavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.ABRT=1
            exec cavput -list=[GetRfFreqControllawName].ABRT=1 } result] {
            return -code error "Problem aborting controllaw: $result"
        }
        
        if $abortOffsetMeasurement {
            eval $statusCallback {"Offset measurement aborted at bpm S$sector$bpm."}
            return
        }
        
        exec sddsmakedataset ${rootname}-quadMeasSettings.sdds \
            -column=QuadrupoleName,type=string -data=$quadName \
            -column=QuadrupoleSteps,type=short -data=$quadSteps \
            -column=LowLimit,type=float,units=A -data=$quadLoLimit \
            -column=HighLimit,type=float,units=A -data=$quadHiLimit \
            -column=Coordinate,type=string -data=$coord \
            -column=NumberToAverage,type=short -data=$num2Ave \
            -column=BumpAmplitude,type=float,units=mm -data=[expr abs($bumpLimit)] \
            -column=BumpSteps,type=short -data=$bumpSteps \
            -column=BPMUnitFactor,type=float -data=$bpmUnitFactor \
            -column=ScanType,type=string -data=combined
        
        eval $statusCallback {"Doing measurement for ${rootname}-quadMeas.sdds"}

        APSExecLog .quadBumpScan -unixCommand "$quadBumpScanCmd $rootname-quadMeas.sdds" -width 120 \
            -callback "set bumpScanStatusVar 1" -abortCallback "set bumpScanStatusVar 2" \
            -cancelCallback "set bumpScanStatusVar 2"
        global bumpScanStatusVar
        after 500
        eval $statusCallback  {"Waiting for sddsexperiment for $rootname"}
        tkwait variable bumpScanStatusVar
        
        if [string match "*P1?" $bpm] {
            # Duplicate to make P0 measurement
            eval set bpm2 [os editstring %/P1/P0/ $bpm]
            eval set bpmDir2 [os editstring %/P1/P0/ $bpmDir]
            eval $statusCallback {"Duplicating data to create measurement for $bpm2"}
            createBPMOffsetDirectory -dateSubdirectory $dateSubdirectory -statusCallback $statusCallback -bpmDir $bpmDir2
            foreach filename [glob $dataDir/$dateSubdirectory/$bpmDir/${meas}-${nextGeneration}*] {
                eval set newFilename [os editstring 10%/P1/P0/ $filename]
                if [string match *.bump $filename] {
                    exec sddsprocess $filename $newFilename -reedit=param,BPMName,%/P1/P0/
                } else {
                    file copy $filename $newFilename
                }
            }
        }
        
        catch {StartControllaw -plane h}
        catch {StartControllaw -plane v}

        ProcessQuadrupoleMeasurement -bpm $bpm -date $dateSubdirectory -index $nextGeneration 
        
        if [string match "*P1?" $bpm] {
            ProcessQuadrupoleMeasurement -bpm $bpm2 -date $dateSubdirectory -index $nextGeneration 
        }
        
        return
    }
}

proc PerformSextupoleOffsetMeasurement {args} {
    global apsBPMOffsetList dataDir templateDir bumpDirectory latticeDir abortOffsetMeasurement offsetSavedConfig
    global orbitCorrectionConverged
    set bpm ""
    set steps 2
    set fractionalChange 0.5
    set changePause 10
    set bumpAmplitude 1.0
    set bumpSteps 3
    set orbitCorrectionInterval 2.0
    set orbitCorrectionGain 0.5
    set orbitConvergence 1.0
    set orbitConvergedCount 5
    set orbitPVType Sampled
    set num2Ave 15
    set useSext 0
    set statusCallback "puts"
    set bpmUnitFactor 1.0
    if [catch {APSStrictParseArguments {bpm bump steps fractionalChange changePause bumpAmplitude bumpSteps orbitConvergence \
                                            orbitConvergedCount orbitCorrectionInterval orbitCorrectionGain orbitPVType num2Ave useSext \
                                            statusCallback bpmUnitFactor}} result] {
        return -code error "PerformSextupoleOffsetMeasurement: $result"
    }
    eval $statusCallback {"bpm=$bpm steps=$steps fractionalChange=$fractionalChange changePause=$changePause bumpAmplitude=$bumpAmplitude bumpSteps=$bumpSteps orbitCorrectionInterval=$orbitCorrectionInterval orbitCorrectionGain=$orbitCorrectionGain num2Ave=$num2Ave"}

    set t0 [clock milliseconds]
    set pendIOTime 10
    set year ""
    set month ""
    set day ""
    APSDateBreakDown -monthVariable month -yearVariable year -twoDigitYear 0 -dayVariable day
    set dateSubdirectory $year-$month-$day
    
    # Here, bpm has values S01A:P1x, S01A:P2y, etc
    # sector has values S01, S02, etc.
    # bpmType has values A:P1 A:P2, etc
    regexp {(S[0-4][0-9])([AB]:P.)([xy])} $bpm all sector bpmType coord
    regsub {:} $bpm {} meas
    eval $statusCallback {"bpm=$bpm, sector=$sector, bpmType=$bpmType, coord=$coord, meas=$meas"}

    foreach plane {x y} {
        set doPlane($plane) 0
        if {[string compare $plane x]==0 && [expr $useSext&1]} {
            set doPlane(x) 1
        }
        if {[string compare $plane y]==0 && [expr $useSext&2]} {
            set doPlane(y) 1
        }
    }
    eval $statusCallback {"doPlane(x) = $doPlane(x)  doPlane(y) = $doPlane(y)"}
    if !$doPlane($coord) {
        return -code error "PerformSextupoleOffsetMeasurement: coord=$coord but doPlane($coord)==0!"
    }
    
    foreach plane {x y} {
        if $doPlane($plane) {
            regsub {:} $bpmType$plane {} bpmDir($plane)
            createBPMOffsetDirectory -dateSubdirectory $dateSubdirectory -statusCallback $statusCallback -bpmDir $bpmDir($plane)
        }
    }

    # We'll use the requested plane's directory for execution of the experiment
    cd $dataDir/$dateSubdirectory/$bpmDir($coord)
    eval $statusCallback {"Changed directory to [pwd]"}
    if [catch {set lastFiles [lindex [lsort -decreasing -ascii [glob $meas-??-*.sdds{,.gz}]] 0] } result] {
        # if no files exist then the first generation is 01.
        set nextGeneration [format "%02ld" 1]
    } else {
        regsub $meas-(..)-.*\.sdds(\.gz)?\$ $lastFiles {\1} lastGeneration
        scan $lastGeneration {%ld} intGen
        incr intGen
        set nextGeneration  [format "%02ld" $intGen]
    }
    set rootname $meas-$nextGeneration
    
    eval $statusCallback {"Using rootname $rootname."}
    
    set measurement $meas
    set bpmDev ${sector}$bpmType
    eval $statusCallback {"bpmDev = $bpmDev"}

    foreach plane {x y} {
        set sectorBumpFile $bumpDirectory/${sector}-${plane}-LocalBumps.cokn-differential
        if ![file exists $sectorBumpFile] {
            return -code error "PerformSextupoleOffsetMeasurement: bump file $sectorBumpFile not found"
        }
          
        if [catch {exec sddsprocess $sectorBumpFile $rootname-$plane.bump -match=parameter,BPMName=$bpmDev \
                       -match=parameter,MagnetName=*:S*} result] {
            return -code error "PerformSextupoleOffsetMeasurement: $result"
        }
        catch {exec sddsprintout -parameter -column $rootname-$plane.bump} result
        eval $statusCallback {"$result"}
    
        if ![string match "${sector}\[AB]:S?" [set sextName($plane) [exec sdds2stream -parameter=MagnetName $rootname-$plane.bump]]] {
            return -code error "PerformSextupoleOffsetMeasurement: didn't find a valid sextupole name corresponding to BPM $bpmDev ($sextName($plane))"
        }
    }
    if [string compare $sextName(x) $sextName(y)]!=0 {
        return -code error "PerformSextupoleOffsetMeasurement: sextupole is not the same for x and y bumps!"
    }

    eval $statusCallback "{Using sextupole $sextName(x)}"
    
    set tmpRoot /tmp/[APSTmpString]

    # Save orbit setpoints
    # Transfer orbit setpoints
    # Start orbit correction in both planes with special matrices (see AOP-TN-2020-033)
    # For each x bump height {
    #   Pause orbit correction
    #   Set correctors for x bump
    #   For each y bump height {
    #     Set correctors for y bump
    #     Resume orbit correction and wait for convergence
    #     Transfer orbit setpoints
    #     Measure orbit and corrector settings (1)
    #     Change sextupole
    #     Wait for orbit convergence
    #     Measure orbit and corrector settings (2)
    #     Change sextupole back
    #     Wait for orbit correction to converge
    #     Pause orbit correction
    #     }
    #   }
    # Process data

    if [catch {
        exec cavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.ABRT=1
        exec cavput -list=[GetRfFreqControllawName].ABRT=1 } result] {
        return -code error "Problem suspending controllaw: $result"
    }

    # Get the present "high" sextupole current
    if [catch {APScavget -list=$sextName(x):PS:SetCurrentC} sextCurrent0] {
        return -code error "PerformSextupoleOffsetMeasurement: problem getting current for $sextName(x): $sextCurrent0"
    }
    # Determine the current for the "low" K2 value
    if {[catch {APSDeltaK2ToSetpoints -fse [expr -1*abs($fractionalChange)] -ElementName $sextName(x)} result] ||
        [catch {set sextCurrent1 [lindex [split $result "="] end]} result]} {
        return -code error "PerformSextupoleOffsetMeasurement: problem computing perturbed current for $sextName(x): $result"
    }
    # Make a file containing both sextupole currents, then convert to K2
    if [catch {exec sddsmakedataset ${rootname}-sextValues.sdds \
                   -column=ControlName,type=string -data=$sextName(x):PS:SetCurrentC,$sextName(x):PS:SetCurrentC \
                   -column=ValueString,type=string -data=$sextCurrent1,$sextCurrent0
        APSSetpointsToLatticeQuadSext -input ${rootname}-sextValues.sdds -output ${rootname}-sextValues.param} result] {
        return -code error "PerformSextupoleOffsetMeasurement: problem making parameter file for sextupole: $result"
    }
    # Get list of K2 values in order of increasing |K2|
    if [catch {exec sddsprocess ${rootname}-sextValues.param -pipe=out -match=col,ElementParameter=K2 \
                   | sddssort -pipe -column=ParameterValue,incr,abs \
                   | sdds2stream -pipe -column=ParameterValue} K2List] {
        return -code error "PerformSextupoleOffsetMeasurement: problem getting K2 values for sextupole: $K2List"
    }
    file delete ${rootname}-sextValues.sdds ${rootname}-sextValues.param
    # Get the sextupole length    
    if [catch {exec sddsprocess /home/helios/oagData/sr/lattices/default/aps.prof -pipe=out -match=col,ElementName=$sextName(x) \
                   -process=s,spread,%sSpread | sdds2stream -pipe -parameter=sSpread} LSext] {
        return -code error "PerformSextupoleOffsetMeasurement: problem getting L value for sextupole: $LSext"
    }
    # Store all the run parameters, including the currents and K2L values
    if [catch {exec sddsmakedataset ${rootname}-sextMeasSettings.sdds \
                   -column=SextupoleName,type=string -data=$sextName(x) \
                   -column=HighSextupoleCurrent,type=double,units=A -data=$sextCurrent0 \
                   -column=LowSextupoleCurrent,type=double,units=A -data=$sextCurrent1 \
                   -column=LSextupole,type=double,units=m -data=$LSext \
                   -column=HighK2L,type=double,units=SI -data=[expr $LSext*[lindex $K2List 1]] \
                   -column=LowK2L,type=double,units=SI -data=[expr $LSext*[lindex $K2List 0]] \
                   -column=SextupoleSteps,type=short -data=$steps \
                   -column=FractionalSextupoleChange,type=double -data=$fractionalChange \
                   -column=OrbitCorrectionGain,type=double -data=$orbitCorrectionGain \
                   -column=OrbitCorrectionInterval,type=double,units=s -data=$orbitCorrectionInterval \
                   -column=BumpAmplitude,type=double,units=mm -data=$bumpAmplitude \
                   -column=BumpSteps,type=short -data=$bumpSteps \
                   -column=BPMName,type=string -data=${sector}${bpmType} \
                   -column=DoXPlane,type=short -data=$doPlane(x) \
                   -column=DoYPlane,type=short -data=$doPlane(y) \
                   -column=NumberToAverage,type=short -data=$num2Ave \
               } result] {
        return -code error "PerformSextupoleOffsetMeasurement: problem saving currents for $sextName(x): $result"
    }

    # connect to x and y bumps
    foreach xy {x y} {
        set bumpFile($xy) $rootname-${xy}.bump 
        if [catch {set bumpName($xy) [exec sdds2stream -parameter=ControlName $bumpFile($xy)]} result] {
            return -code error "PerformSextupoleOffsetMeasurement: $result"
        }
        if [catch {set bumpLimit($xy) [exec sdds2stream -parameter=KnobLimit $bumpFile($xy)]} result] {
            return -code error "PerformSextupoleOffsetMeasurement: $result"
        }
        eval $statusCallback {"Destroying bump $bumpName($xy)"}
        catch {exec daq-pv-group destroy --group-channel $bumpName($xy)}
    }
    APSWaitWithUpdate -waitSeconds 2 -updateInterval 1 
    foreach xy {x y} {
        eval $statusCallback {"Creating bump $bumpName($xy) with limit $bumpLimit($xy)"}
        exec daq-pv-group create --input-sdds-file $bumpFile($xy) --runtime 3600 &
    }
    
    if $abortOffsetMeasurement {
        eval $statusCallback {"Offset measurement aborted at bpm S$sector$bpm."}
        return
    }

    APSAddToTmpFileList -ID PerformSextupoleOffsetMeasurement -fileList "$tmpRoot.spReq $tmpRoot.spSave $tmpRoot.mon $tmpRoot.conv"

    if [catch {exec sddsprocess $templateDir/sextupoleMethod/orbitConvergenceTest.req $tmpRoot.conv \
                   -redefine=col,ChangeLimit,$orbitConvergence,units=um} result] {
        eval $statusCallback {"Problem making orbit convergence test file: $result."}
        return
    }
    
    eval $statusCallback {"Saving orbit setpoints"}
    # Save orbit and corrector setpoints and transfer orbit to setpoints
    if [catch {APSSaveMachine -machine SR -description "Prior to sextupole-based offset measurement for $sextName(x)"} offsetSavedConfig] {
        return -code error "PerformSextupoleOffsetMeasurement: Problem saving state: $offsetSavedConfig"
    }
    eval $statusCallback {"Saved to $offsetSavedConfig"}
    set offsetSavedConfig /home/helios/oagData/SCR/snapshots/SR/$offsetSavedConfig
    eval $statusCallback {"Transferring errors to setpoints."}
    if [catch {SextupoleOffsetTransferOrbit} result] {
        catch {exec sddscasr -restore $offsetSavedConfig}
        return -code error "PerformSextupoleOffsetMeasurement: Problem transferring setpoints: $result"
    }
    APSWaitWithUpdate -waitSeconds 3 -updateInterval 1
    
    eval $statusCallback {"Starting special orbit correction"}
    # Start special orbit correction. 
    if [catch {StartSextupoleOffsetOrbitCorrection -sextupoleName $sextName(x) -rootname $rootname \
                   -gain $orbitCorrectionGain -interval $orbitCorrectionInterval -pvType $orbitPVType \
                   -bumpFileArrayList [array get bumpFile] -statusCallback $statusCallback} PIDList] {
        catch {exec sddscasr -restore $offsetSavedConfig}
        return -code error "PerformSextupoleOffsetMeasurement: problem starting orbit correction: $PIDList"
    }
    
    # Wait for knobs to have good status
    eval $statusCallback {"Waiting for knobs to fully connect"}
    foreach xy {x y} {
        if [WaitForKnobAllGood -interval 500 -tries 30 -knob $bumpName($xy) -verbose 1 -statusCallback $statusCallback]==0 {
            if [WaitForKnobAllGood -interval 500 -tries 30 -knob $bumpName($xy) -verbose 1 -statusCallback $statusCallback]==0 {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem initializing knob $bumpName($xy)"
            }
        }
    }

    # Find bump limits and set amplitude
    set data [APScavget -list=$bumpName(x),$bumpName(y) -list=:NegativePegValue,:PositivePegValue]
    set limit(x,neg) [expr 0.75*abs([lindex $data 0])]
    set limit(x,pos) [expr 0.75*abs([lindex $data 1])]
    set limit(y,neg) [expr 0.75*abs([lindex $data 2])]
    set limit(y,pos) [expr 0.75*abs([lindex $data 3])]
    foreach xy {x y} {
        if [expr ($limit($xy,pos)>$limit($xy,neg))] {
            set limit($xy) $limit($xy,neg)
        } else {
            set limit($xy) $limit($xy,pos)
        }
        if [expr $limit($xy)>$bumpAmplitude] {
            set limit($xy) $bumpAmplitude
        }
        eval $statusCallback {"Bump amplitude in $xy will be $limit($xy)."}
    }
    
    # Suspend controllaws
    eval $statusCallback {"Suspending special orbit controllaws"}
    if [catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.SUSP=1} result] {
        catch {exec sddscasr -restore $offsetSavedConfig}
        return -code error "PerformSextupoleOffsetMeasurement: problem suspending controllaws: $result"
    }

    
    # Perform 2D bump scan
    set knobTolerance [expr abs($bumpAmplitude)/(10.0*$bumpSteps)]
    set ix -1
    set index 0
    set total [expr $bumpSteps*$bumpSteps]
    foreach knobValue(x) [APSSequence -start -$bumpAmplitude -end $bumpAmplitude -number $bumpSteps] {
        incr ix
        set iy -1
        foreach knobValue(y) [APSSequence -start -$bumpAmplitude -end $bumpAmplitude -number $bumpSteps] {
            incr iy
            incr index
            eval $statusCallback {"Working on point $index of $total"}
            eval $statusCallback {"Elapsed time so far is [expr ([clock milliseconds]-$t0)/1000.0] s"}
            eval $statusCallback {"Setting knobs to $knobValue(x) mm and $knobValue(y) mm"}
            # Set bumps
            set knobsOk 0
            for {set iter 0} {$iter<10} {incr iter} {
                set cmd "APScavput -list=$bumpName(x):Set=$knobValue(x),$bumpName(y):Set=$knobValue(y)"
                #eval $statusCallback {"knob put command: $cmd"}
                if [catch {APScavput -list=$bumpName(x):Set=$knobValue(x),$bumpName(y):Set=$knobValue(y)} result] {
                    catch {exec sddscasr -restore $offsetSavedConfig}
                    return -code error "PerformSextupoleOffsetMeasurement: problem setting bumps: $result"
                }
                after 500
                set code 0
                foreach xy {x y} {
                    incr code [WaitForKnobResponse -interval 500 -tries 5 -verbose 1 \
                                  -knob $bumpName($xy) -tolerance $knobTolerance -setpoint $knobValue($xy) \
                                  -verbose 1 -statusCallback $statusCallback]
                }
                if $code==0 {
                    set knobsOk 1
                    break
                }
            }
            if !$knobsOk {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem with bump knobs"
            }
            # This allows the simulator to update to reflect the knob change
            APSWaitWithUpdate -waitSeconds 3 -updateInterval 0.5

            # Resume orbit correction
            eval $statusCallback {"$index of $total: Resuming special orbit controllaws"}
            if [catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.SUSP=0} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem resuming controllaws: $result"
            }
            # Wait for orbit convergence 
            eval $statusCallback {"$index of $total: Waiting for convergence (up to $changePause s)"}
            APSWaitWithUpdate -waitSeconds 3 -updateInterval 0.5
            set orbitCorrectionConverged 0
            set tconverge0 [clock milliseconds]
            APSExecLog .test -unixCommand \
                "sddspvtest -verbose -interval=2 -exit=converged=$orbitConvergedCount $tmpRoot.conv -time=$changePause" \
                -callback "set orbitCorrectionConverged 1"
            APSWaitWithUpdate -waitSeconds $changePause -updateInterval 0.5 -abortVariable orbitCorrectionConverged
            set timeToConverge [expr ([clock milliseconds]-$tconverge0)/1e3]
            eval $statusCallback {"$index of $total: took $timeToConverge s to converge"}
            
            # Transfer orbit
            eval $statusCallback {"$index of $total: Transferring orbit"}
            if [catch {SextupoleOffsetTransferOrbit} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: Problem transferring setpoints: $result"
            }

            # Measure orbit and correctors
            eval $statusCallback {"$index of $total: Measuring orbit and correctors."}
            if [catch {exec sddsstatmon -includeStat=mean,sigma,sample $templateDir/sextupoleMethod/monitorInput.req \
                           ${rootname}-DataFull1-$ix-$iy.sdds -interval=0.1 -samples=$num2Ave -steps=1} result] {
                return -code error "PerformSextupoleOffsetMeasurement: problem getting data: $result"
            }
            if [expr $bpmUnitFactor!=1] {
                eval $statusCallback {"Converting BPM data to microns"}
                if [catch {exec sddsprocess ${rootname}-DataFull1-$ix-$iy.sdds -nowarning -convert=col,*S???:P?:?,um,um,$bpmUnitFactor} result] {
                    return -code error "PerformSextupoleOffsetMeasurement: problem converting BPM data to um"
                }
                file delete ${rootname}-DataFull1-$ix-$iy.sdds~
            }
            # Change sextupole
            eval $statusCallback {"$index of $total: Ramping sextupole from $sextCurrent0 down to $sextCurrent1."}
            if [catch {APScaRamp -pvList $sextName(x):PS:SetCurrentC -variableList sextupoleCurrent \
                           -beginList $sextCurrent0 -endList $sextCurrent1 -steps 2 -pause 1} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem setting sextupole: $result"
            }
            # Wait for orbit convergence
            eval $statusCallback {"$index of $total: Waiting for convergence (up to $changePause s)"}
            APSWaitWithUpdate -waitSeconds 3 -updateInterval 0.5
            set orbitCorrectionConverged 0
            set tconverge0 [clock milliseconds]
            APSExecLog .test -unixCommand \
                "sddspvtest -verbose -interval=2 -exit=converged=$orbitConvergedCount $tmpRoot.conv -time=$changePause" \
                -callback "set orbitCorrectionConverged 1"
            APSWaitWithUpdate -waitSeconds $changePause -updateInterval 0.5 -abortVariable orbitCorrectionConverged
            set timeToConverge [expr ([clock milliseconds]-$tconverge0)/1e3]
            eval $statusCallback {"$index of $total: took $timeToConverge s to converge"}

            # Measure orbit and correctors
            eval $statusCallback {"$index of $total: Measuring orbit and correctors."}
            if [catch {exec sddsstatmon -includeStat=mean,sigma,sample $templateDir/sextupoleMethod/monitorInput.req \
                           ${rootname}-DataHalf-$ix-$iy.sdds -interval=0.1 -samples=$num2Ave -steps=1} result] {
                return -code error "PerformSextupoleOffsetMeasurement: problem getting data: $result"
            }
            if [expr $bpmUnitFactor!=1] {
                eval $statusCallback {"Converting BPM data to microns"}
                if [catch {exec sddsprocess ${rootname}-DataHalf-$ix-$iy.sdds -nowarning -convert=col,*S???:P?:?,um,um,$bpmUnitFactor} result] {
                    return -code error "PerformSextupoleOffsetMeasurement: problem converting BPM data to um"
                }
                file delete ${rootname}-DataHalf-$ix-$iy.sdds~
            }
            # Suspend orbit correction
            eval $statusCallback {"$index of $total: Suspending special orbit controllaws"}
            if [catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.SUSP=1} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem suspending controllaws: $result"
            }
            # Change sextupole
            eval $statusCallback {"$index of $total: Changing sextupole back to nominal value $sextCurrent0."}
            if [catch {APScaRamp -pvList $sextName(x):PS:SetCurrentC -variableList sextupoleCurrent \
                           -beginList $sextCurrent1 -endList $sextCurrent0 -steps 2 -pause 1} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem setting sextupole: $result"
            }
            # Restore saved correctors from Full1
            eval $statusCallback {"$index of $total: Restoring correctors."}
            if [catch {exec sddsconvert ${rootname}-DataFull1-$ix-$iy.sdds -pipe=out -retain=col,*:*H*Mean,*:*V*Mean \
                           | sddstranspose -pipe -oldColumnNames=ControlName \
                           | sddsprocess -pipe -reedit=col,ControlName,%/Mean/:PS:SetCurrentC/ \
                           -print=col,ValueString,%21.15e,Column \
                           | sddscasr -pipe=in -restore} result] {
                catch {exec sddscasr -restore $offsetSavedConfig}
                return -code error "PerformSextupoleOffsetMeasurement: problem restoring correctors: $result"
            }
            APSWaitWithUpdate -waitSeconds 3 -updateInterval 0.5
            if 0 {
                # This code is to take a second measurement with the sextupole back at the full value.
                # It doesn't really improve the results and takes extra time
                # Resume orbit correction
                eval $statusCallback {"$index of $total: Resuming special orbit controllaws"}
                if [catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.SUSP=1} result] {
                    catch {exec sddscasr -restore $offsetSavedConfig}
                    return -code error "PerformSextupoleOffsetMeasurement: problem suspending controllaws: $result"
                }
                # Wait for orbit convergence
                eval $statusCallback {"$index of $total: Waiting for convergence (up to $changePause s)"}
                APSWaitWithUpdate -waitSeconds 3 -updateInterval 0.5
                set orbitCorrectionConverged 0
                APSExecLog .test -unixCommand \
                    "sddspvtest -verbose -interval=2 -exit=converged=$orbitConvergedCount $tmpRoot.conv -time=$changePause" \
                    -callback "set orbitCorrectionConverged 1"
                APSWaitWithUpdate -waitSeconds $changePause -updateInterval 0.5 -abortVariable orbitCorrectionConverged
                # Measure orbit and correctors
                eval $statusCallback {"$index of $total: Measuring orbit and correctors."}
                if [catch {exec sddsstatmon -includeStat=mean,sigma,sample $templateDir/sextupoleMethod/monitorInput.req \
                               ${rootname}-DataFull2-$ix-$iy.sdds -interval=0.1 -samples=$num2Ave -steps=1} result] {
                    return -code error "PerformSextupoleOffsetMeasurement: problem getting data: $result"
                }
                # Suspend orbit correction
                eval $statusCallback {"$index of $total: Suspending special orbit controllaws"}
                if [catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.SUSP=1} result] {
                    catch {exec sddscasr -restore $offsetSavedConfig}
                    return -code error "PerformSextupoleOffsetMeasurement: problem suspending controllaws: $result"
                }
            }
        }
    }

    # Kill special orbit correction
    catch {APScavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.ABRT=1}
    foreach PID $PIDList {
        APSExecLogAbort -id $PID -destroy 1
    }
    # Restore bump setpoints
    if [catch {exec cavput -list=$bumpName(x):Set=0,$bumpName(y):Set=0} result] {
        catch {exec sddscasr -restore $offsetSavedConfig}
        return -code error "PerformSextupoleOffsetMeasurement: problem resetting bumps: $result"
    }
    foreach xy {x y} {
        WaitForKnobResponse -interval 500 -tries 5 -verbose 1 \
            -knob $bumpName($xy) -tolerance $knobTolerance -setpoint 0 -verbose 1 -statusCallback $statusCallback
    }
    # Destroy bumps
    foreach xy {x y} {
        set bumpFile($xy) $rootname-${xy}.bump 
        exec daq-pv-group destroy --group-channel $bumpName($xy) 
    }
    # Restore everything 
    catch {exec sddscasr -restore $offsetSavedConfig}

    eval $statusCallback {"tmpRoot=$tmpRoot"}

    eval $statusCallback {"Elapsed time was [expr ([clock milliseconds]-$t0)/1000.0] s"}

    catch {StartControllaw -plane h}
    catch {StartControllaw -plane v}
    
    ProcessSextupoleMeasurement -bpm $bpm -date $dateSubdirectory -index $nextGeneration 

    if {$doPlane(x) && $doPlane(y)} {
        # Copy the results to the equivalent directory for y
        foreach filename [lsort [glob $dataDir/$dateSubdirectory/$bpmDir(x)/${rootname}*]] {
            eval set yname [os editstring %/x-/y-/ [file tail $filename]]
            eval file copy $filename $dataDir/$dateSubdirectory/$bpmDir(y)/$yname
        }
    }
    
    return
}

proc SextupoleOffsetTransferOrbit {args} {
    global templateDir

    # Transfer orbit to setpoints
    # Use SampledErrorM for faster response and to avoid filter windows
    if [catch {exec sddssnapshot $templateDir/sextupoleMethod/BPMSetpoints.req -pipe=out -nameOfData=Setpoint \
                   | sddsprocess -pipe -reedit=col,ControlName,%/:SetpointC/:SampledErrorM/ \
                   | sddssnapshot -pipe -average=50,0.1 -nameOfData=Error \
                   | sddsprocess -pipe "-define=column,NewSetpoint,Setpoint Error +" \
                   -reedit=col,ControlName,%/:SampledErrorM/:SetpointC/ \
                   -reprint=column,ValueString,%le,NewSetpoint \
                   | sddscasr -restore -pipe=in} result] {
        return -code error "SextupoleOffsetTransferOrbit: Problem transferring setpoints: $result"
    }
}

proc StartSextupoleOffsetOrbitCorrection {args} {
    global latticeDir templateDir
    set sextupoleName ""
    set rootname ""
    set statusCallback APSNoOp
    set gain 0.25
    set interval 3.0
    set pvType ""
    set bumpFileArrayList ""
    if [APSStrictParseArguments {sextupoleName rootname statusCallback gain interval bumpFileArrayList pvType}] {
        return -code error "StartSextupoleOffsetOrbitCorrection: $result"
    }
    array set bumpFile $bumpFileArrayList
    eval $statusCallback {"StartSextupoleOffsetOrbitCorrection: $sextupoleName $bumpFile(x) $bumpFile(y)"}
    # Abort any existing orbit correction
    if [catch {exec cavput -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.ABRT=1
        exec cavput -list=[GetRfFreqControllawName].ABRT=1 } result] {
        return -code error "StartSextupoleOffsetOrbitCorrection: Problem aborting controllaw: $result"
    }
    # Wait to ensure all are stopped
    set nRun 3
    while {$nRun!=0} {
        if [catch {APScavget -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC,[GetRfFreqControllawName] -list=.RUN} result] {
            return -code error "StartSextupoleOffsetOrbitCorrection: Problem aborting controllaw: $result"
        }
        set nRun 0
        foreach item $result {
            incr nRun $item
        }
        if $nRun==0 break
        after 500
        eval $statusCallback {"Waiting for orbit controllaws to terminate"}
    }

    # Prepare special orbit controllaw configuration
    # -Use all BPMs outside the bump
    # -Use some of the FH1 and FV1 correctors in other sectors, plus correctors near the sextupole
    #  For S1, use H1, V1
    #  For S2 and S3, use (H1, FH1) and (V1, FV1)
    #eval $statusCallback {"Warning: using free path length response matrices"}
    #set responseMatrix(h) $latticeDir/aps.free.hrm
    #set responseMatrix(v) $latticeDir/aps.free.vrm
    set responseMatrix(h) $latticeDir/aps.hrm
    set responseMatrix(v) $latticeDir/aps.vrm
    if {![file exists $responseMatrix(h)] || ![file exists $responseMatrix(v)]} {
        return -code error "StartSextupoleOffsetOrbitCorrection: fixed-length response matrices not found"
    }
    foreach hv {h v} HV {H V} xy {x y} XY {X Y} {
        regexp {(S[0-4][0-9])([AB]):(S[1-3])} $sextupoleName all sector side sext
        switch $side {
            A {
                set otherSide B
            }
            B {
                set otherSide A
            }
            default {
                return -code error "StartSextupoleOffsetOrbitCorrection: problem determining side for sextupole: side=$side"
            }
        }
        if (1) {
            # Make a list of polluted BPMs
            set fd [sdds open $bumpFile($xy)]
            set parameterList [APSGetSDDSNames -sddsFD $fd -class parameter]
            set pollutedRejectOpt ""
            set pollutedList ""
            foreach parameter $parameterList {
                if [string match PollutedBPM* $parameter] {
                    set BPMName [APSGetSDDSParameter -sddsFD $fd -parameter $parameter -page 1]
                    if [string match S*P? $BPMName] {
                        lappend pollutedList $BPMName
                        if [string length $pollutedRejectOpt]==0 {
                            set pollutedRejectOpt -match=column,BPMName=$BPMName,!
                        } else {
                            append pollutedRejectOpt ,BPMName=$BPMName,!,&
                        }
                    }
                }
            }
            sdds close $fd
            eval $statusCallback {"Polluted BPMs for $xy: [join $pollutedList ,]"}
        } else {
            # Just remove BPMs within +/-2 half sectors of the side of interest
            # E.g., for S20A:PX, we'd remove S20A:P?, S20B:P?, S21A:P?, and S19?:P?
            scan $sector S%d isector
            set pollutedRejectOpt -match=column,BPMName=${sector}?:P?,!
            set nsector [expr ($isector)%40+1]
            switch $side {
                A {
                    append pollutedRejectOpt ,BPMName=S[format %02d $nsector]${side}:P?,!,&
                }
                B {
                    append pollutedRejectOpt ,BPMName=S[format %02d $nsector]?:P?,!,&
                }
            }
            set psector [expr ($isector-2)%40+1]
            switch $side {
                A {
                    append pollutedRejectOpt ,BPMName=S[format %02d $psector]?:P?,!,&
                }
                B {
                    append pollutedRejectOpt ,BPMName=S[format %02d $psector]${side}:P?,!,&
                }
            }
            eval $statusCallback {"Polluted BPM rejection option: $pollutedRejectOpt"}
        }
        # these are behaving oddly in the simulator or real machine
        append pollutedRejectOpt ,BPMName=S39C:P?,!,&,BPMName=S39B:P0,!,&,BPMName=S33B:P1,!,&
        # Select columns, invert
        switch $sext {
            S1 {
                set local ${sector}${side}:${HV}1
            }
            S2 -
            S3 {
                set local ${sector}${side}:${HV}1,${sector}${side}:F${HV}2
            }
            default {
                return -code error "StartSextupoleOffsetOrbitCorrection: problem determining sextupole type: sext=$sext"
            }
        }
        #APSAddToTmpFileList -ID StartSextupoleOffsetOrbitCorrection \
        #    -fileList "$tmpRoot.${hv}rm $tmpRoot.${hv}rm-cvt $tmpRoot.i${hv}rm"
        scan $sector S%d isector
        set retainList [list BPMName $local]
        # Use only FH/V1 and H/V7 correctors beyond +/- 2 sectors
        for {set di -20} {$di<21} {incr di} {
            if [expr abs($di)<3] continue
            set i [expr ($isector-1+$di)%40+1]
            lappend retainList [format S%02d${otherSide}:F${HV}1 $i]
            lappend retainList [format S%02d${side}:${HV}7 $i]
        }
        eval $statusCallback {"Retain corrector option: $retainList"}
        if [catch {exec sddsconvert $responseMatrix($hv) -retain=col,[join $retainList ,] -pipe=out \
                       | sddsprocess -pipe=in $rootname.${hv}rm $pollutedRejectOpt } result] {
            return -code error "StartSextupoleOffsetOrbitCorrection: problem downselecting response matrix: $result"
        }
        if [catch {exec convertAPSUResponseMatrix -plane $hv -input $rootname.${hv}rm -output $rootname.${hv}rm-cvt} result] {
            return -code error "StartSextupoleOffsetOrbitCorrection: problem converting response matrix: $result"
        }
        if [catch {exec sddspseudoinverse $rootname.${hv}rm-cvt $rootname.i${hv}rm} result] {
            return -code error "StartSextupoleOffsetOrbitCorrection: problem inverting response matrix: $result"
        }
        catch {file delete $rootname.${hv}rm $rootname.${hv}rm-cvt}
        set clawID($xy) [APSExecLog .orbit${hv} -width 100 -unixCommand \
                             "sddscontrollaw $rootname.i${hv}rm -verbose -steps=7200 -interval=$interval -gain=$gain -controlQuantityDefinition=$templateDir/sextupoleMethod/${xy}PlaneDefns${pvType}.sdds -testValues=$templateDir/sextupoleMethod/${xy}PlaneTests${pvType}.sdds -runControlPV=string=S:RC:OrbitControlLaw${XY}C,pingTimeout=10 -controlLog=$rootname-${hv}OrbCorLog.sdds -deltaLimit=value=0.5 -runControlDescription=string=SRBPMOffset"]
    }

    # Ensure that the controllaws have started
    set nRun 0
    while {$nRun!=2} {
        if [catch {APScavget -list=S:RC:OrbitControlLawXC,S:RC:OrbitControlLawYC -list=.RUN} result] {
            return -code error "StartSextupoleOffsetOrbitCorrection: Problem starting controllaws: $result"
        }
        set nRun 0
        foreach item $result {
            incr nRun $item
        }
        if $nRun==2 break
        after 500
        eval $statusCallback {"Waiting for orbit controllaws to start"}
    }

    return [list $clawID(x) $clawID(y)]
}

proc createBPMOffsetDirectory {args} {
    global apsBPMOffsetList dataDir
    set dateSubdirectory ""
    set statusCallback "puts stderr"
    set bpmDir ""
    APSStrictParseArguments {dateSubdirectory statusCallback bpmDir}
    # check existence of daily data directory 
    if ![file isdirectory $dataDir/$dateSubdirectory] {
        if [string length $statusCallback] {
            eval $statusCallback {"Creating directory $dataDir/$dateSubdirectory and subdirectories."}
        } 
        exec mkdir -p $dataDir/$dateSubdirectory
        if [string length $bpmDir] {
            exec mkdir -p $dataDir/$dateSubdirectory/$bpmDir
        }
    }
    if ![file exist $dataDir/$dateSubdirectory/$bpmDir] {
        exec mkdir -p $dataDir/$dateSubdirectory/$bpmDir
    }
}

proc getOffsetFiles {args} {
    global dataDir 
    
    set bpm ""
    set suffix ""
    # bpm should have the form S1A:P1x
    APSStrictParseArguments {bpm suffix}
    
    if {![string length $bpm] || ![string length $suffix] } {
        return
    }

    regexp {[AB]:P..} $bpm bpmDir
    regsub {:} $bpmDir {} bpmDir
    regsub {:} $bpm {} bpm
    if [catch {glob -nocomplain $dataDir/*/$bpmDir/$bpm-??${suffix}{,.gz} } files] {
        APSAlertBox [APSUniqueName .] -errorMessage $files
        return
    }
    return [lsort -ascii $files]
}

proc ProcessQuadrupoleMeasurement {args} {
    global dataDir

    set date ""
    # bpm should have the form S01A:P1x
    set bpm ""
    set index ""
    set review 0
    set statusCallback "puts"
    set makePlots 1
    set printout 1
    set ultimateCallBack APSNoOp
    APSStrictParseArguments {date bpm index review statusCallback makePlots printout ultimateCallBack}
    if ![string length $date] {
        eval $statusCallback {"ProcessQuadrupoleMeasurement (1): No date argument."}
        return
    }
    if ![string length $bpm] {
        eval $statusCallback {"ProcessQuadrupoleMeasurement (2): No bpm argument."}
        return
    }
    set coord ""
    regexp {(x|y)} $bpm {} coord
    if ![string length $coord] {
        eval $statusCallback {"ProcessQuadrupoleMeasurement (3): couldn't infer coord from $bpm."}
        return
    }
    set bpmDir ""
    regexp {[AB]:P..} $bpm bpmDir
    regsub {:} $bpmDir {} bpmDir
    if ![string length $bpmDir] {
        eval $statusCallback {"ProcessQuadrupoleMeasurement (4): couldn't infer bpmDir from $bpm."}
        return
    }
    cd $dataDir/$date/$bpmDir
    update
    regsub {:} $bpm {} bpmNoColon
    set root ${bpmNoColon}-$index
    eval $statusCallback {"root: $root   pwd: [pwd]"}
    
    if !$review {
        if [catch {exec sdds2stream ${root}.bump -param=BPMName} bpmName ] {
            eval $statusCallback {"ProcessQuadrupoleMeasurement (5): $bpmName"}
            return
        }
        if [catch {exec sdds2stream -param=MagnetName ${root}.bump -noquotes} indep] {
            eval $statusCallback {"ProcessQuadrupoleMeasurement (6): $indep"}
            return
        }
        if [catch {exec sdds2stream -column=QuadrupoleName ${root}-quadMeasSettings.sdds} quadName] {
            eval $statusCallback {"ProcessQuadrupoleMeasurement (6a): $quadName"}
            return
        }
        if [catch {exec sdds2stream ${root}-quadMeasSettings.sdds -column=ScanType} result] {
            # Old measurements may not have the ScanType column. These are all separated scans
            set separatedScan 1
        } else {
            switch $result {
                combined {
                    set separatedScan 0
                }
                default {
                    set separatedScan 1
                }
            }
        }

        # check whether we have stored current data (old measurements)
        set columnList [APSGetSDDSNames -fileName ${root}-quadMeas.sdds -class column]
        if [lsearch $columnList S-DCCT:CurrentM]!=-1 {
            # DCCT is ok if minimum is greater than 0.5 mA and Min/Max>0.9
            set DCCTProcOptList {-process=S-DCCT:CurrentM,min,DCCTMin -process=S-DCCT:CurrentM,max,DCCTMax \
                                   "-define=param,DCCTOk,DCCTMin 0.5 > ? DCCTMin DCCTMax / 0.1 + int : 0 $,type=short"}
        } else {
            # assume DCCT is ok
            set DCCTProcOptList "-define=parameter,DCCTOk,1,type=short"
        }
        set suspectBPMOpt [list "-define=parameter,OffsetOutside,MeanOffset BumpMin < MeanOffset BumpMax > || ? 1 : 0 $ ,type=short" "-define=parameter,SuspectResult,OffsetOutside 1 DCCTOk - + ,type=short"]
        if $separatedScan {
            # make analysis of bump range for later use in comparison to offsets
            if [catch {eval exec sddscombine ${root}-quadMeas.sdds -pipe=out -merge \
                         | sddsprocess -pipe=in ${root}-range.sdds \
                         -process=$bpmName:$coord,spread,BumpRange \
                         -process=$bpmName:$coord,min,BumpMin \
                         -process=$bpmName:$coord,max,BumpMax \
                         $DCCTProcOptList \
                         "{-define=param,BumpCenter,BumpMin BumpMax + 2 /}" \
                     } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (7): $result"}
            }
            update
            
            # extract the pre-change BPM readings for later use
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Determining values of *:${coord} prior to changing $indep"}
            if [catch {exec sddsprocess $root-quadMeas.sdds -clip=1,0,invert -pipe=out \
                           | sddscombine -pipe=in -merge $root-startPos.sdds} result] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (8): $result"}
                return
            }
            
            # do slope analysis of all quad scans. 
            # give the name of the variable column in the sddsexperiment output file.
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Taking slopes of *:${coord} vs quadrupole strength $indep"}
            if [catch {exec sddsprocess $root-quadMeas.sdds -pipe=out -clip=1,0 \
                           | sddsmpfit -pipe -independent=$indep -dependent=S???:P?:${coord} "-sigmaDependent=Sigma%s" \
                           -terms=2 -minimumSigma=0.1 \
                           | sddscollapse -pipe \
                           | sddsxref -pipe=in $root-startPos.sdds -take=* -edit=col,*,ei/0/ $root-qslopes.sdds} result] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (9): $result"}
                #exec sddsplot -col=$indep,*:${coord} $root-quadMeas.sdds -graph=sym -separate -clip=1,0 &
                return
            }
            update
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Quad scan slopes computation done."}
            
            # get slope and intercept for each quad scan vs average position in the bpm in question
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Taking slopes of slopes"}
            if [catch {exec sddsmpfit ${root}-qslopes.sdds -pipe=out -terms=2 \
                           -independent=${bpmName}:${coord}0 -dependent=S???:P?:${coord}Slope -sigmaDependent=%sSigma \
                           | sddscollapse -pipe=in ${root}-posSlopes.sdds } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (10): $result"}
                #exec sddsplot -col=$indep,*:${coord} $root-quadMeas.sdds -graph=sym -separate -clip=1,0 &
                #exec sddsplot -column=${bpmName}:${coord}0,*:${coord}Slope -graph=line,vary -order=spect ${root}-qslopes.sdds &
                return
            }
            update
            
            # Make file with column of slopes, intercept, and their sigmas of quad-slopes vs BPM name
            eval $statusCallback {"ProcessQuadrupoleMeasurement: collecting data into ${root}-slopes.sdds"}
            catch {file delete -force -- ${root}-slopes1.sdds}
            if [catch {exec sddscollect ${root}-posSlopes.sdds -pipe=out \
                           -collect=suffix=Slope,column=Slope -collect=suffix=SlopeSigma,column=SlopeSigma \
                           -collect=suffix=Intercept,column=Intercept -collect=suffix=InterceptSigma,column=InterceptSigma \
                           | sddsconvert -pipe=in ${root}-slopes1.sdds -rename=col,Rootname=BPMName \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (11): $result"}
                return
            }
            update   
        
            # Now have columns with BPMName, Intercept, and Slope for each BPM 
            
            # Compute offsets = -intercept/slope 
            #   eliminate data with exactly zero slope
            #   eliminate unreasonably large slopes (larger than 0.1 mm) and
            #   offsets (larger than 15 mm), and outliers
            #   find average and sigma of offsets
            eval $statusCallback {"ProcessQuadrupoleMeasurement: calculate intercepts"}
            # In case a two point fit is made, the almost zero sigmas
            # will break the rpn calculations. Set a minimum
            # of 1e-6 for InterceptSigma.
            if [catch {exec sddsprocess ${root}-slopes1.sdds -pipe=out \
                           "-define=col,AbsSlope,Slope abs" \
                           "-rpnexpr=0 sto i" \
                           "-define=col,Index,i 1 + sto i,type=long" \
                           -filter=col,Slope,0,0,! -filter=col,Slope,-0.1e3,0.1e3 \
                           "-define=col,Offset,Intercept Slope chs /,units=um" \
                           "-redef=col,InterceptSigma,InterceptSigma 1e-6 2 maxn" \
                           "-define=col,OffsetPropagatedSigma,InterceptSigma Intercept / sqr SlopeSigma Slope / sqr + sqrt Offset abs *,units=um" \
                           "-define=col,InvOffsetPropSigma2,OffsetPropagatedSigma rec sqr,units=1/um\$a2\$n" \
                           -filter=col,Offset,-15e3,15e3 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsxref ${root}-range.sdds -pipe -nowarning -leave=* -transfer=param,Bump*,DCCT* \
                           | sddsprocess -pipe=in ${root}-slopes2.sdds \
                           -process=Offset,ave,AverageOffset \
                           -process=Offset,stand,StDevOffset \
                           -process=InvOffsetPropSigma2,sum,InvSigma2Sum \
                           "-define=col,WeightedOffset,Offset InvOffsetPropSigma2 * InvSigma2Sum /,units=um" \
                           -process=WeightedOffset,sum,MeanOffset \
                           "-define=param,SigmaOffset,InvSigma2Sum rec sqrt,units=um" \
                           -reprint=param,BPMName,$bpmName:$coord \
                           -reprint=param,Coordinate,$coord \
                           -reprint=param,QuadName,$quadName \
                           -reprint=param,Rootname,${root} \
                           "-def=para,RangeFraction,MeanOffset BumpCenter - BumpRange / 2 *,description=fractional offset relative to the bump range center" \
                           "-def=para,OffsetDiff,AverageOffset MeanOffset -,description=difference between average and weighted mean offsets" \
                           "-print=param,OffsetLabel,Offset: %.3f\$sa\$e%.3f%s (ave:%.3f%s  st.dev.:%.3f%s),MeanOffset,SigmaOffset,SigmaOffset.units,AverageOffset,AverageOffset.units,StDevOffset,StDevOffset.units" \
                           "-print=param,RangeCheckLabel,Range:\[%.3f\\, %.3f]%s  Range fraction: %.3f,BumpMin,BumpMax,BumpMin.units,RangeFraction" \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (12): $result"}
                return
            }
            update
            
            # make histogram of offsets 
            eval $statusCallback {"ProcessQuadrupoleMeasurement: making histogram ${root}-offsets.his"}
            catch {file delete -force -- ${root}-offsets.his}
            if [catch { exec sddshist ${root}-slopes2.sdds -pipe=out -data=Offset -norm=area -bins=16 -sides \
                            | sddsxref -pipe ${root}-slopes2.sdds -noWarnings -leave=* -transfer=param,OffsetLabel \
                            | sddsprocess -pipe=in ${root}-offsets.his \
                            -reprint=param,BPMName,$bpmName:$coord \
                            -reprint=param,QuadName,$quadName \
                            -reprint=param,Rootname,${root} \
                        } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (13): $result"}
                return
            }
            update
        
            if [catch {exec sddsxref -noWarnings ${root}-qslopes.sdds ${root}-slopes2.sdds -leave=* -transfer=param,*Label \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (14): $result"}
                return
            }
            catch {file delete ${root}-qslopes.sdds~}
            
            if [catch { eval exec sddsconvert ${root}-slopes2.sdds -pipe=out \
                          -retain=para,BPMName,AverageOffset,StDevOffset,MeanOffset,SigmaOffset,RangeFraction,QuadName,OffsetDiff,Bump*,DCCT* \
                          | sddsprocess -pipe=in ${root}-quadResults.sdds $suspectBPMOpt \
                        } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (15): $result"}
                return
            }
        } else {
            # combined scan 
            eval $statusCallback {"Combined-scan file detected"}
            # make analysis of bump range for later use in comparison to offsets
            if [catch {eval exec sddscombine ${root}-quadMeas.sdds -pipe=out -merge \
                         | sddsprocess -pipe=in ${root}-range.sdds \
                         -process=$bpmName:$coord,spread,BumpRange \
                         -process=$bpmName:$coord,min,BumpMin \
                         -process=$bpmName:$coord,max,BumpMax \
                         $DCCTProcOptList \
                         "{-define=param,BumpCenter,BumpMin BumpMax + 2 /}" \
                     } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c7): $result"}
            }
            update

            # extract the BPM readings with initial quad setting for later use
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Determining values of *:${coord} for nominal $indep"}
            if [catch {exec sddsexpand ${root}-quadMeasSettings.sdds ${root}-quadMeasSettings.sddse
                exec sddsxref $root-quadMeas.sdds ${root}-quadMeasSettings.sddse -pipe=out -leave=* -transfer=param,* \
                           -reuse=page,row \
                           | sddsprocess -pipe \
                           "-define=col,QuadIndex,i_row BumpSteps / int,type=short" \
                           "-define=col,BumpIndex,i_row BumpSteps mod,type=short" \
                           | tee $root-quadMeas.sdds1 \
                           | sddsprocess -pipe "-test=column,QuadIndex 0 ==" \
                           | sddscombine -pipe=in -merge $root-startPos.sdds} result] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c8): $result"}
                return
            }
            catch {file delete $root-quadMeasSettings.sddse}
            
            # do slope analysis of all quad scans. 
            # give the name of the variable column in the sddsexperiment output file.
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Taking slopes of *:${coord} vs quadrupole strength $indep"}
            if [catch {exec sddssort $root-quadMeas.sdds1 -pipe=out -column=BumpIndex \
                           | sddsbreak -pipe -change=BumpIndex \
                           | sddsmpfit -pipe -independent=$indep -dependent=S???:P?:${coord} "-sigmaDependent=Sigma%s" \
                           -terms=2 -minimumSigma=0.1 \
                           | sddscollapse -pipe \
                           | sddsxref -pipe=in $root-startPos.sdds -take=* -edit=col,*,ei/0/ $root-qslopes.sdds} result] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c9): $result"}
                #exec sddsplot -col=$indep,*:${coord} $root-quadMeas.sdds -graph=sym -separate -clip=1,0 &
                return
            }
            # catch {file delete $root-quadMeas.sdds1}
            update
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Quad scan slopes computation done."}
            
            # get slope and intercept for each quad scan vs average position in the bpm in question
            eval $statusCallback {"ProcessQuadrupoleMeasurement: Taking slopes of slopes"}
            if [catch {exec sddsmpfit ${root}-qslopes.sdds -pipe=out -terms=2 \
                           -independent=${bpmName}:${coord}0 -dependent=S???:P?:${coord}Slope -sigmaDependent=%sSigma \
                           | sddscollapse -pipe=in ${root}-posSlopes.sdds } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c10): $result"}
                #exec sddsplot -col=$indep,*:${coord} $root-quadMeas.sdds -graph=sym -separate -clip=1,0 &
                #exec sddsplot -column=${bpmName}:${coord}0,*:${coord}Slope -graph=line,vary -order=spect ${root}-qslopes.sdds &
                return
            }
            update
            
            # Make file with column of slopes, intercept, and their sigmas of quad-slopes vs BPM name
            eval $statusCallback {"ProcessQuadrupoleMeasurement: collecting data into ${root}-slopes.sdds"}
            catch {file delete -force -- ${root}-slopes1.sdds}
            if [catch {exec sddscollect ${root}-posSlopes.sdds -pipe=out \
                           -collect=suffix=Slope,column=Slope -collect=suffix=SlopeSigma,column=SlopeSigma \
                           -collect=suffix=Intercept,column=Intercept -collect=suffix=InterceptSigma,column=InterceptSigma \
                           | sddsconvert -pipe=in ${root}-slopes1.sdds -rename=col,Rootname=BPMName \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c11): $result"}
                return
            }
            update   
        
            # Now have columns with BPMName, Intercept, and Slope for each BPM 
            
            # Compute offsets = -intercept/slope 
            #   eliminate data with exactly zero slope
            #   eliminate unreasonably large slopes (larger than 0.1 mm) and
            #   offsets (larger than 15 mm), and outliers
            #   find average and sigma of offsets
            eval $statusCallback {"ProcessQuadrupoleMeasurement: calculate intercepts"}
            # In case a two point fit is made, the almost zero sigmas
            # will break the rpn calculations. Set a minimum
            # of 1e-6 for InterceptSigma.
            if [catch {exec sddsprocess ${root}-slopes1.sdds -pipe=out \
                           "-define=col,AbsSlope,Slope abs" \
                           "-rpnexpr=0 sto i" \
                           "-define=col,Index,i 1 + sto i,type=long" \
                           -filter=col,Slope,0,0,! -filter=col,Slope,-0.1e3,0.1e3 \
                           "-define=col,Offset,Intercept Slope chs /,units=um" \
                           "-redef=col,InterceptSigma,InterceptSigma 1e-6 2 maxn" \
                           "-define=col,OffsetPropagatedSigma,InterceptSigma Intercept / sqr SlopeSigma Slope / sqr + sqrt Offset abs *,units=um" \
                           "-define=col,InvOffsetPropSigma2,OffsetPropagatedSigma rec sqr,units=1/um\$a2\$n" \
                           -filter=col,Offset,-15e3,15e3 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsoutlier -pipe -col=Offset -stDevLimit=2 \
                           | sddsxref ${root}-range.sdds -pipe -nowarning -leave=* -transfer=param,Bump*,DCCT* \
                           | sddsprocess -pipe=in ${root}-slopes2.sdds \
                           -process=Offset,ave,AverageOffset \
                           -process=Offset,stand,StDevOffset \
                           -process=InvOffsetPropSigma2,sum,InvSigma2Sum \
                           "-define=col,WeightedOffset,Offset InvOffsetPropSigma2 * InvSigma2Sum /,units=um" \
                           -process=WeightedOffset,sum,MeanOffset \
                           "-define=param,SigmaOffset,InvSigma2Sum rec sqrt,units=um" \
                           -reprint=param,BPMName,$bpmName:$coord \
                           -reprint=param,Coordinate,$coord \
                           -reprint=param,QuadName,$quadName \
                           -reprint=param,Rootname,${root} \
                           "-def=para,RangeFraction,MeanOffset BumpCenter - BumpRange / 2 *,description=fractional offset relative to the bump range center" \
                           "-def=para,OffsetDiff,AverageOffset MeanOffset -,description=difference between average and weighted mean offsets" \
                           "-print=param,OffsetLabel,Offset: %.3f\$sa\$e%.3f%s (ave:%.3f%s  st.dev.:%.3f%s),MeanOffset,SigmaOffset,SigmaOffset.units,AverageOffset,AverageOffset.units,StDevOffset,StDevOffset.units" \
                           "-print=param,RangeCheckLabel,Range:\[%.3f\\, %.3f]%s  Range fraction: %.3f,BumpMin,BumpMax,BumpMin.units,RangeFraction" \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c12): $result"}
                return
            }
            update
            
            # make histogram of offsets 
            eval $statusCallback {"ProcessQuadrupoleMeasurement: making histogram ${root}-offsets.his"}
            catch {file delete -force -- ${root}-offsets.his}
            if [catch { exec sddshist ${root}-slopes2.sdds -pipe=out -data=Offset -norm=area -bins=16 -sides \
                            | sddsxref -pipe ${root}-slopes2.sdds -noWarnings -leave=* -transfer=param,OffsetLabel \
                            | sddsprocess -pipe=in ${root}-offsets.his \
                            -reprint=param,BPMName,$bpmName:$coord \
                            -reprint=param,QuadName,$quadName \
                            -reprint=param,Rootname,${root} \
                        } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c13): $result"}
                return
            }
            update
        
            if [catch {exec sddsxref -noWarnings ${root}-qslopes.sdds ${root}-slopes2.sdds -leave=* -transfer=param,*Label \
                       } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c14): $result"}
                return
            }
            catch {file delete ${root}-qslopes.sdds~}
            
            if [catch { eval exec sddsconvert ${root}-slopes2.sdds -pipe=out \
                          -retain=para,BPMName,AverageOffset,StDevOffset,MeanOffset,SigmaOffset,RangeFraction,QuadName,OffsetDiff,Bump*,DCCT* \
                          | sddsprocess -pipe=in  ${root}-quadResults.sdds $suspectBPMOpt \
                        } result ] {
                eval $statusCallback {"ProcessQuadrupoleMeasurement (c15): $result"}
                return
            }
        }
        
        if {[catch {exec sddsprintout ${root}-quadResults.sdds ${root}-printout.txt \
                        -param=BPMName,end \
                        -param=AverageOffset -param=StDevOffset,end \
                        -param=MeanOffset -param=SigmaOffset,end \
                        -param=OffsetDiff,end \
                      -param=RangeFraction,end \
                      -param=SuspectResult,end -param=DCCT*,end \
                    } result] || \
                [catch {exec sdds2stream -rows ${root}-slopes2.sdds >> ${root}-printout.txt \
                        } result]} {
            eval $statusCallback {"ProcessQuadrupoleMeasurement (c16): $result"}
            return
        }
        exec echo "Full filename: $dataDir/$date/$bpmDir/$root" >> ${root}-printout.txt
        regexp {(S.*[AB]:P.):([xy])} $bpmName {} bpm coord
        set pvName "$bpmName:${coord}:BpmZeroOffsetC"
        if {[catch {exec cavget -list=$pvName} lastOffset] || [string compare ? $lastOffset]==0} {
            APSAlertBox [APSUniqueName .] -errorMessage "Unable to read present offset for $bpmName (PV $pvName): $result"
            eval $statusCallback {"Error reading present offset."}
            return
        }
        exec echo "Present  offset: [format %.4e $lastOffset]" >> ${root}.printout
        exec echo "Measured offset: [format %.4e [exec sdds2stream ${root}-quadResults.sdds -para=MeanOffset]]" >> ${root}.printout
    }

    # start of review block
    if $printout {
        set displayWidget .[APSUniqueName printout]
        APSFileDisplayWindow $displayWidget -fileName ${root}-printout.txt -width 80
        APSDialogBoxAddButton .install -parent $displayWidget \
          -text Install... \
          -command "InstallOffset -fileName $dataDir/$date/$bpmDir/${root}-quadResults.sdds -statusCallback \"$statusCallback\"" \
          -contextHelp "Sends the offset you are looking at back to the BPM as the Offset value."
        
        APSDialogBoxAddButton .installtr -parent $displayWidget \
          -text "Install/Adjust Setpoint..." \
          -command "InstallOffset -fileName $dataDir/$date/$bpmDir/${root}-quadResults.sdds -statusCallback \"$statusCallback\" -adjustSetpoint 1" \
          -contextHelp "Sends the offset you are looking at back to the BPM as the Offset value.  Also adjusts the setpoint so that the orbit readback does not change.  Useful in keeping the same orbit."
        
        APSDialogBoxAddButton .remove -parent $displayWidget \
          -text "Remove Measurement..." \
          -command "RemoveBadMeasurement -fileName $dataDir/$date/$bpmDir/${root} -statusCallback \"$statusCallback\"" \
          -contextHelp "Move the bad data to a wastebasket directory."
    }

    set bpmName [exec sdds2stream ${root}-quadResults.sdds -param=BPMName]

    if $makePlots {
        eval $statusCallback {"ProcessQuadrupoleMeasurement: bpmName = $bpmName, makePlots = $makePlots"}
	if {$makePlots != 2} {
	    exec sddsplot -labelsize=0.025 -col=Slope,Offset,OffsetPropagatedSigma \
		-grap=error ${root}-slopes2.sdds \
		"-title=scan: $root  $coord" &
	    exec sddsplot -labelsize=0.025 -col=Offset,frequency ${root}-offsets.his \
		"-title=scan: $root  $coord"  -topline=@OffsetLabel &
	}
        set columns [exec sddsquery ${root}-qslopes.sdds -col]
        if {[lsearch -exact $columns ${bpmName}0]!=-1} {
            eval $statusCallback {"ProcessQuadrupoleMeasurement: plotting slopes"}
            exec sddsplot -labelsize=0.025  \
              -col=${bpmName}0,*:${coord}Slope \
              ${root}-qslopes.sdds -axes=x \
              -graph=sym,conn=sub,vary=sub -limit=ymin=-1000,ymax=1000,autoscaling \
              "-title=scan: $root $coord" -ytopline &
            exec sddsplot -labelsize=0.025  -separate -same \
              -col=${bpmName}0,*:${coord}Slope \
              ${root}-qslopes.sdds -axes=x \
              -graph=sym,conn -limit=ymin=-1000,ymax=1000,autoscaling \
              "-title=scan: $root $coord" -ytopline &
        }            
    }
    eval $statusCallback {"ProcessQuadrupoleMeasurement: Done with ${root}."}
    eval $ultimateCallBack
}

proc InstallOffset {args} {
    set fileName ""
    set statusCallback puts
    set noConfirm 0
    set adjustSetpoint 0
    if [APSStrictParseArguments {fileName statusCallback noConfirm adjustSetpoint}] {
        return -code error "InstallOffset: problem with arguments"
    }
    if [catch {sdds open $fileName r} fd] {
        eval $statusCallback {"unable to open  $fileName: $fd"}
        return
    }
    if [catch {sdds getParameter $fd MeanOffset } offset] {
        eval $statusCallback {"$offset"}
        return
    }
    if [catch {sdds getParameter $fd BPMName } bpmName] {
        eval $statusCallback {"$bpmName"}
        return
    }
    if [catch {sdds close $fd } result] {
        eval $statusCallback {"$result"}
        return
    }
    set coord ""
    set bpm ""
    regexp {(S.*[AB]:P.):([xy])} $bpmName {} bpm coord
    set pvName "$bpmName:BpmZeroOffsetC"

    if [catch {exec cavget -list=$pvName} lastOffset] {
        APSAlertBox [APSUniqueName .] -errorMessage "Unable to read present offset for $bpmName: $result"
        eval $statusCallback {"Offset not installed."}
        return
    }
    eval $statusCallback {"Previous offset for $bpmName is $lastOffset"}
    set delta [expr $offset-$lastOffset]
    if $adjustSetpoint {
        set wwo "with"
    } else {
        set wwo "without"
    }
    if { $noConfirm || \
           [APSYesNoPopUp "Confirm: change ${coord} offset for $bpmName from $lastOffset to $offset $wwo setpoint adjustment"] } {
        if $adjustSetpoint {
            set suffix BpmZeroOffsetC=$delta,SetpointC=[expr -1.0*$delta]
        } else {
            set suffix BpmZeroOffsetC=$delta
        }
        if [catch {exec cavput -list=$bpm:$coord: -list=$suffix -delta -pendIoTime=5} result] {
            APSAlertBox [APSUniqueName .] -errorMessage "Error trying to set ${pvName} for $bpmName"
            eval $statusCallback {"Offset not installed."}
            return
        } 
        if $adjustSetpoint {
            eval $statusCallback {"Offset $offset installed for $bpmName and setpoint adjusted."}
        } else {
            eval $statusCallback {"Offset $offset installed for $bpmName"}
        }
        return
    }
    eval $statusCallback {"Offset not installed."}
}

proc InstallSextupoleOffsets {args} {
    set rootname ""
    set statusCallback puts
    set noConfirm 0
    set adjustSetpoint 0
    if [APSStrictParseArguments {rootname statusCallback noConfirm adjustSetpoint}] {
        return
    }
    if [catch {sdds load ${rootname}-sextMeasSettings.sdds sextMeas} result] {
        return -code error "InstallSextupoleOffset: $result"
    }
    set do(x) [lindex [lindex $sextMeas(Column.DoXPlane) 0] 0]
    set do(y) [lindex [lindex $sextMeas(Column.DoYPlane) 0] 0]
    set BPMName [lindex [lindex $sextMeas(Column.BPMName) 0] 0]

    if [catch {sdds load ${rootname}-sextResults.sdds fit} result] {
        return -code error "InstallSextupoleOffset: $result"
    }
    set offset(x) [expr 1e6*[lindex $fit(Parameter.x0) 0]]
    set offset(y) [expr 1e6*[lindex $fit(Parameter.y0) 0]]

    foreach plane {x y} {
        if [catch {exec cavget -list=$BPMName:$plane:BpmZeroOffsetC} result] {
            return -code error "InstallSextupoleOffset: $result"
        }
        set lastOffset $result

        eval $statusCallback {"Previous $plane offset for $BPMName is $lastOffset"}
        set delta [expr $offset($plane)-$lastOffset]
        if $adjustSetpoint {
            set wwo "with"
        } else {
            set wwo "without"
        }
        set didIt 0
        set message1 ""
        if !$do($plane) {
            set message1 "(note: atypical to use sextupole for this offset)"
        }
        if { $noConfirm || \
                 [APSYesNoPopUp "Confirm: change ${plane} offset for $BPMName from $lastOffset to $offset($plane) $wwo setpoint adjustment $message1?"] } {
            if $adjustSetpoint {
                set suffix BpmZeroOffsetC=$delta,SetpointC=[expr -1.0*$delta]
            } else {
                set suffix BpmZeroOffsetC=$delta
            }
            set reallyDoIt 1
            if !$do($plane) {
                set reallyDoIt [APSYesNoPopUp "Reconfirm: Using sextupole-based result for $BPMName:$plane is atypical. Proceed?"]
            }
            if $reallyDoIt {
                if [catch {exec cavput -list=$BPMName:$plane: -list=$suffix -delta -pendIoTime=5} result] {
                    return -code error "InstallSextupoleOffset: Error trying to set offset/setpoint for $BPMName:$plane: $result"
                }
                set didIt 1
            }
        }
        if $didIt {
            if $adjustSetpoint {
                eval $statusCallback {"Offset installed for $BPMName:$plane and setpoint adjusted."}
            } else {
                eval $statusCallback {"Offset installed for $BPMName:$plane"}
            }
        } else {
            eval $statusCallback {"Offset not installed for $BPMName:$plane."}
        }
    }
}

proc RemoveBadMeasurement {args} {
    set fileName ""
    set statusCallback puts
    if [APSStrictParseArguments {fileName statusCallback}] {
        return
    }
    if [APSYesNoPopUp "Ok to remove bad offset measurement?"] {
        set dir [file dirname $fileName]
        if ![file isdirectory $dir/.wastebasket] {
            file mkdir $dir/.wastebasket
        }
        if [catch {eval file rename -force [glob -nocomplain ${fileName}*] $dir/.wastebasket} result] {
            APSAlertBox [APSUniqueName .] -errorMessage "Error trying to remove the bad offset measurement: $result"
            eval $statusCallback {"Offset measurement not removed."}
            return
        } 
        eval $statusCallback {"Offset measurement removed for ${fileName}"}
        return
    }
    eval $statusCallback {"Offset measurement not removed."}
}

proc CleanFiles {args} {
    set fileName ""
    set coord x
    set statusCallback puts
    if [APSStrictParseArguments {fileName coord statusCallback}] {
        return
    }
    if [APSYesNoPopUp "Ok to remove processed data files?"] {
        catch {file delete -force -- ${fileName}.slopes1 ${fileName}-quad.slopes ${fileName}-quad.slopes1 ${fileName}.slopes1.his}
        eval $statusCallback {"Cleaned files for $fileName"}
        return
    }
    eval $statusCallback {"Offset measurement not removed."}
}


proc ScanBumpAmplitude {args} {
    global bumpScanStatusVar
    set bumpFile ""
    set amplitude 1.0
    set steps -1
    set verbose 0
    set restore 1
    set rootname ""
    set command ""
    set bpmUnitFactor 1.0
    set statusCallback APSNoOp
    if {[APSStrictParseArguments {bumpFile amplitude steps verbose restore rootname command statusCallback bpmUnitFactor}] || ![string length $bumpFile] || [expr $steps<2] || \
            ([string length $command] && ![string length $rootname])} {
        return -code error "$usage"
    }

    if $verbose {
        puts stdout "Saving starting values"
        flush stdout
    }
    if [catch {exec sddscasr $bumpFile -save $rootname.save} result] {
        return -code error "$result"
    }
    if [catch {set bumpName [exec sdds2stream -parameter=ControlName $bumpFile]} result] {
        return -code error "$result"
    }
    if [catch {set bumpLimit [exec sdds2stream -parameter=KnobLimit $bumpFile]} result] {
        return -code error "$result"
    }
    if [expr 0.75*$bumpLimit<$amplitude] {
        # Reduce range of scan to avoid limits
        eval $statusCallback "{Reducing bump height to stay within limits}"
        set amplitude [expr $bumpLimit*0.75]
    }
    set start $amplitude
    set end -$amplitude
    
    if $verbose {
        puts stdout "Creating knob $bumpName"
        flush stdout
    }

    exec daq-pv-group create --input-sdds-file $bumpFile --runtime 3600 &
    if [WaitForKnobAllGood -interval 500 -tries 30 -knob $bumpName -verbose 1 -statusCallback $statusCallback]==0 {
        if [WaitForKnobAllGood -interval 500 -tries 30 -knob $bumpName -verbose 1 -statusCallback $statusCallback]==0 {
            return -code error "ScanBumpAmplitude: problem initializing knob $bumpName"
        }
    }
    
    set knobValueList [APSSequence -start $start -end $end -number $steps]
    puts stdout "knobValueList: [join $knobValueList ,]"
    set index 0
    set fileList ""
    set knobTolerance [expr abs($start-$end)/(10.0*$steps)]
    foreach knobValue $knobValueList {
        if $verbose {
            puts stdout "Knob value => $knobValue, index = $index"
            flush stdout
        }
        set knobOk 0
        for {set iter 0} {$iter<10} {incr iter} {
            if [catch {exec cavput -list=$bumpName:Set=$knobValue} result] {
                return -code error "$result"
            }
            after 500
            set code [WaitForKnobResponse -interval 500 -tries 5 -verbose 1 -knob $bumpName -tolerance $knobTolerance -setpoint $knobValue \
                          -verbose 1 -statusCallback $statusCallback]
            if !$code {
                set knobOk 1
                break
            }
            if [catch {exec cavget -list=$bumpName:Status} status] {
                return -code error "$status"
            }
        }
        if !$knobOk {
            break
        }
        eval $statusCallback {"Waiting after changing bump"}
        APSWaitWithUpdate -waitSeconds 10 -updateInterval 1
        set bumpScanStatusVar 0
        if [string length $command] {
            set runName [format "$rootname-%02d.sdds" $index]
            if $verbose {
                puts stdout "Running $command $runName"
                flush stdout
            }
            APSExecLog .bumpScan -unixCommand "$command $runName" -width 120 \
                -callback "set bumpScanStatusVar 1" -abortCallback "set bumpScanStatusVar 2" \
                -cancelCallback "set bumpScanStatusVar 2"
            after 500
            eval $statusCallback  {"Waiting for sddsexperiment $runName"}
            tkwait variable bumpScanStatusVar
            if [expr $bpmUnitFactor!=1] {
                eval $statusCallback  {"Converting BPM values to right units"} 
                if [catch {exec sddsprocess $runName -convert=col,*S???:P?:?,um,um,$bpmUnitFactor -nowarning} result] {
                    return -code error "$result"
                }
                file delete ${runName}~
            }
            #puts stdout "Callback received: $bumpScanStatusVar"
            lappend fileList $runName
        }
        if $bumpScanStatusVar==2 break
        incr index
        #puts stdout "index => $index"
    }

    if [llength $fileList]==$steps {
        if [catch {eval exec sddscombine $fileList -overwrite $rootname.sdds} result] {
            return -code error "$result"
        }
        catch {eval file delete $fileList}
    } else {
        eval $statusCallback {"Too few files found"}
    }
    
    if [catch {exec daq-pv-group destroy --group-channel $bumpName} result] {
        eval $statusCallback {"Problem destroying bump knob: $result"}
    }

    if $restore {
        if $verbose {
            puts stdout "Restoring"
            flush stdout
        }
        if [catch {exec sddscasr $rootname.save -restore} result] {
            return -code error "$result"
        }
    }
    
}

proc WaitForKnobResponse {args} {
    set tries 10
    set interval 500
    set knob ""
    set statusCallback APSNoOp
    set verbose 0
    set setpoint 0
    set tolerance -1.0
    if {[APSStrictParseArguments {tries interval knob verbose setpoint tolerance statusCallback}] || ![string length $knob]} {
        return -code error "waitForKnobResponse: invalid arguments"
    }

    for {set try 0} {$try<$tries} {incr try} {
        after $interval
        if [catch {exec cavget -list=$knob: -list=Status,Read} result] {
            eval $statusCallback {"WaitForKnobResponse ($knob): $result"}
            return 1
        }
        set status [string trim [lindex $result 0] "\" "]
        if [string match *pegged* $status] {
            eval $statusCallback {"WaitForKnobResponse ($knob): pegged"}
            return 2
        }
        set readback [lindex $result 1]
        if $verbose {
            eval $statusCallback {"WaitForKnobResponse ($knob): $readback, $status"}
        }
        if [expr abs($setpoint-$readback)<$tolerance] {
            return 0
        }
    } 
    eval $statusCallback {"WaitForKnobResponse ($knob): timed out waiting for readback to respond"}
    return 3
}

proc WaitForKnobAllGood {args} {
    set tries 10
    set interval 500
    set knob ""
    set statusCallback APSNoOp
    set verbose 0
    if {[APSStrictParseArguments {tries interval knob verbose statusCallback}] || ![string length $knob]} {
        return -code error "waitForKnobAllGood: invalid arguments"
    }

    set allGood 0
    for {set try 0} {$try<$tries} {incr try} {
        after $interval
        incr tries -1
        if [catch {exec cavget -list=$knob:Status -noquote} result] {
            after $interval
        } else {
            if $verbose {
                eval $statusCallback {"WaitForKnobAllGood ($knob): $result"}
            }
            if [string match "all good" $result] {
                puts stderr "Knob returns $result"
                return 1
            }
        }
    }
    eval $statusCallback {"WaitForKnobAllGood ($knob): timed out waiting for all good status"}
    return 0
}

proc ProcessSextupoleMeasurement {args} {
    global dataDir

    set date ""
    # bpm should have the form S01A:P1x
    set bpm ""
    set index ""
    set review 0
    set statusCallback "puts"
    set makePlots 1
    set printout 1
    set ultimateCallBack APSNoOp
    APSStrictParseArguments {date bpm index review statusCallback makePlots printout ultimateCallBack}
    eval $statusCallback "{date=$date, bpm=$bpm, index=$index, review=$review, makePlots=$makePlots}"
    if ![string length $date] {
        return -code error "ProcessSextupoleMeasurement: No date argument."
    }
    if ![string length $bpm] {
        return -code error "ProcessSextupoleMeasurement: No bpm argument."
    }
    set coord ""
    regexp {(x|y)} $bpm {} coord
    if ![string length $coord] {
        return -code error "ProcessSextupoleMeasurement: couldn't infer coord from $bpm."
    }
    set bpmDir ""
    regexp {[AB]:P..} $bpm bpmDir
    regsub {:} $bpmDir {} bpmDir
    if ![string length $bpmDir] {
        return -code error "ProcessSextupoleMeasurement: couldn't infer bpmDir from $bpm."
    }
    cd $dataDir/$date/$bpmDir
    update
    regsub {:} $bpm {} bpmNoColon
    set root ${bpmNoColon}-$index
    eval $statusCallback {"root: $root   pwd: [pwd]"}
    
    if [catch {sdds load ${root}-sextMeasSettings.sdds sextMeas} result] {
        return -code error "ProcessSextupoleMeasurement: $result"
    }
    foreach item $sextMeas(ColumnNames) {
        set $item [lindex [lindex $sextMeas(Column.$item) 0] 0]
    }

    if !$review {
        set fd [open $root-printout.txt w]
        puts $fd "$date/$root\n"
        
        for {set ix 0} {$ix<$BumpSteps} {incr ix} {
            for {set iy 0} {$iy<$BumpSteps} {incr iy} {
                foreach type {Full1 Half} {
                    # Collect data into columns indexed by Rootname for BPMs
                    if [catch {exec sddsconvert ${bpmNoColon}-${index}-Data${type}-$ix-$iy.sdds -pipe=out -retain=col,*:P* \
                                   | sddscollect -pipe=in ${bpmNoColon}-${index}-Data${type}-$ix-$iy.colBPM \
                                   -collect=suffix=Mean -collect=suffix=Sigma 
                        exec sddsconvert ${bpmNoColon}-${index}-Data${type}-$ix-$iy.sdds -pipe=out -retain=col,*:*H*,*:*V* \
                                   | sddscollect -pipe=in ${bpmNoColon}-${index}-Data${type}-$ix-$iy.colCor \
                                   -collect=suffix=Mean -collect=suffix=Sigma } result] {
                        return -code error "ProcessSextupoleMeasurement (1): $result"
                    }
                }
                # Combine the half-strength and full-strength corrector data. The latter is the reference.
                if [catch {exec sddsxref ${bpmNoColon}-$index-DataHalf-$ix-$iy.colCor -match=Rootname -nowarning \
                               ${bpmNoColon}-$index-DataFull1-$ix-$iy.colCor -take=Mean,Sigma ${bpmNoColon}-$index-$ix-$iy.procKicks \
                               -edit=col,*,ei/Ref/} result] {
                    return -code error "ProcessSextupoleMeasurement (2): $result"
                }
                file delete ${bpmNoColon}-${index}-DataFull1-$ix-$iy.colCor ${bpmNoColon}-$index-DataHalf-$ix-$iy.colCor

                foreach plane {x y} {
                    # Separate x and y data for BPMs
                    if [catch {exec sddsprocess ${bpmNoColon}-${index}-DataFull1-$ix-$iy.colBPM \
                                   "-match=col,Rootname=*:*${plane}*" ${bpmNoColon}-${index}-DataFull1-$ix-$iy.procBPM$plane} result] {
                        return -code error "ProcessSextupoleMeasurement (3): $result"
                    }
                }
                eval file delete [glob ${bpmNoColon}-${index}-*-$ix-$iy.colBPM]
            }
        }

        # Combine all the corrector data into one file, subtract reference from perturbed values,
        # convert from amperes to delta kick angle. Still have both H and V data in each page.
        if [catch {eval exec sddscombine [lsort [glob ${bpmNoColon}-${index}-?-?.procKicks]] -pipe=out \
                       | sddsprocess -pipe "{-define=col,Delta,Mean MeanRef -}" \
                       {-match=col,Rootname=*:\[HV\]2,!} \
                       -edit=col,Type,Rootname,Z: \
                       "{-define=col,DeltaSigma,Sigma sqr SigmaRef sqr + sqrt}" \
                       | sddsxref -pipe /home/helios/oagData/sr/magnets/correctorLimits.sdds -match=Type=CorrectorType \
                       -take=CurrentLimit,DeflectionLimit,ElementNameFragment -reuse=row,page \
                       | sddsprocess -pipe=in ${bpmNoColon}-$index.procKicks \
                       "{-define=col,DeltaKick,Delta CurrentLimit / DeflectionLimit *,units=rad}" \
                       "{-define=col,DeltaKickSigma,DeltaSigma CurrentLimit / DeflectionLimit *,units=rad}" \
                       -edit=column,SectorSideName,Rootname,S/:/D \
                       -print=column,ElementName,%s:%s,SectorSideName,ElementNameFragment } result] {
            return -code error "ProcessSextupoleMeasurement (4): $result"
        }
        eval file delete [glob ${bpmNoColon}-${index}-?-?.procKicks]

        eval set sector [os editstring 3f10d $bpm]
        eval set sectorSide [os editstring 4f10d $bpm]

        switch -glob $SextupoleName  {
            *S1 {
                # Separate the H and V data into different files, then convert H1 and V1 delta kicks into
                # equivalent sextupole kick (by simply changing the sign).
                eval $statusCallback {"Using S1 method"}
                set HV H
                foreach xy {x y} HV {H V} {
                    eval set corrName [os editstring %/:S/:${HV}/ $SextupoleName]
                    if [catch {exec sddsprocess ${bpmNoColon}-$index.procKicks -pipe=out -match=col,Rootname=$corrName \
                                   "-define=col,Sext${xy}Kick,DeltaKick chs,units=rad" \
                                   "-define=col,Sext${xy}KickSigma,DeltaKickSigma,units=rad" \
                                   -process=Sext${xy}Kick,ave,%sAve \
                                   -process=Sext${xy}KickSigma,rms,%sAve \
                                   | tee ${bpmNoColon}-$index.${xy}Kicks.detail \
                                   | sddscollapse -pipe=in ${bpmNoColon}-$index.${xy}Kicks} result] {
                        return -code error "ProcessSextupoleMeasurement (5): $result"
                    }
                }
            }
            default {
                # Separate the H and V data into different files, then use simulation-based scaling coefficients to
                # convert to equivalent sextupole kick. The sextupole kick is proportional to the corrector kicks that
                # are required to reclose the bump.
                eval $statusCallback {"Using S2/S3 method"}
                foreach plane {horizontal vertical} HV {H V} xy {x y} {
                    eval set typeName [os editstring 3d%/://3f100d $bpm]
                    set coefFile [glob /home/helios/oagData/sr/bpmOffsets/templates/sextupoleMethod/${typeName}-S?-coef.cor]
                    if [catch {exec sddsprocess $coefFile ${bpmNoColon}-$index.coef \
                                   -print=col,ElementName,${sectorSide}:%s,ElementNameFragment \
                                   -match=param,Plane=$plane} result] {
                        return -code error "ProcessSextupoleMeasurement (6): $result"
                    }
                    if [catch {exec sddsprocess ${bpmNoColon}-$index.procKicks -pipe=out -match=col,Rootname=*${HV}* \
                                   | sddsxref -pipe ${bpmNoColon}-$index.coef -match=ElementName -take=ParameterValue \
                                   -reuse=page -rename=col,ParameterValue=ReferenceValue -nowarning \
                                   | sddsprocess -pipe \
                                   "-define=col,Sext${xy}Kick,DeltaKick ReferenceValue / 10e-6 *,units=rad"  \
                                   "-define=col,Sext${xy}KickSigma,DeltaKickSigma ReferenceValue / 10e-6 *,units=rad"  \
                                   -process=Sext${xy}Kick,ave,%sAve \
                                   -process=Sext${xy}KickSigma,rms,%sAve \
                                   | tee ${bpmNoColon}-$index.${xy}Kicks.detail \
                                   | sddscollapse -pipe=in ${bpmNoColon}-$index.${xy}Kicks} result] {
                        return -code error "ProcessSextupoleMeasurement (7): $result"
                    }
                    file delete ${bpmNoColon}-$index.coef
                }
            }
        }

        # Combine the BPM data files for each plane and perform some name conversions,
        # and keep only the data at the BPM nearest the sextupole.
        eval set nearestBPM [os editstring %/x//%/y// $bpm]
        foreach xy {x y} {
            if [catch {eval exec sddscombine [lsort [glob ${bpmNoColon}-${index}-DataFull1-?-?.procBPM$xy]] -pipe=out \
                           | sddsprocess -pipe \
                           "{-define=col,${xy}Sigma,Sigma,units=um}" \
                           -edit=col,BPMName,Rootname,%/:$xy// \
                           | tee ${bpmNoColon}-${index}.${xy}Orbits \
                           | sddsprocess -pipe -match=col,BPMName=$nearestBPM -print=parameter,BPMName,$nearestBPM \
                           | sddscombine -pipe -merge \
                           | sddsconvert -pipe=in ${bpmNoColon}-${index}.${xy}BPM -rename=col,Mean=$xy} result] {
                return -code error "ProcessSextupoleMeasurement (8): $result"
            }
            eval file delete [glob ${bpmNoColon}-${index}-DataFull1-?-?.procBPM$xy]
        }

        # Finally, combine everything
        if [catch {exec sddsxref ${bpmNoColon}-$index.xKicks ${bpmNoColon}-$index.yKicks -pipe=out -take=Sext* \
                       | sddsconvert -pipe -retain=col,Sext* \
                       | sddsxref -pipe=in ${bpmNoColon}-${index}.xBPM ${bpmNoColon}-${index}.yBPM -take=BPMName,x*,y* \
                       ${bpmNoColon}-$index.proc} result] {
            return -code error "ProcessSextupoleMeasurement (9): $result"
        }
        file delete ${bpmNoColon}-$index.xKicks ${bpmNoColon}-$index.yKicks ${bpmNoColon}-${index}.xBPM ${bpmNoColon}-${index}.yBPM 

        set dK2L [expr $LowK2L-$HighK2L]

        # Perform fit
        if [catch {exec sddsexpand ${bpmNoColon}-$index-sextMeasSettings.sdds ${bpmNoColon}-$index-sextMeasSettings.sddse
            exec sddsprocess ${bpmNoColon}-$index.proc -pipe=out -define=col,Zero,0 \
                       {-convert=column,[xy],m,um,1e-6} \
                       | sddsgenericfit -pipe \
                       -variable=name=x0,lower=-1e-2,upper=1e-2,stepsize=1e-4,start=0,units=m \
                       -variable=name=y0,lower=-1e-2,upper=1e-2,stepsize=1e-4,start=0,units=m \
                       -ycolumn=Zero -copy=SextxKickAve,SextyKickAve,x,y -logfile=${bpmNoColon}-$index.fitlog \
                       "-expression=x x0 - sqr y y0 - sqr - $dK2L * -2 / sto dKx,dKx" \
                       "-expression=x x0 - y y0 - * $dK2L * sto dKy,dKy" \
                       "-equation=dKx SextxKickAve - abs dKy SextyKickAve - abs +" \
                       -simplex=restart=2,cycles=3,evaluations=1500 -target=1e-14 -tolerance=1e-15 \
                       | sddsxref -pipe=in ${bpmNoColon}-$index-sextMeasSettings.sddse ${bpmNoColon}-$index-sextResults.sdds \
                       -leave=* -transfer=param,* } result] {
            return -code error "ProcessSextupoleMeasurement (10): $result"
        }
        catch {file delete ${bpmNoColon}-$index-sextMeasSettings.sddse}
        
        set pvName "$BPMName:${coord}:BpmZeroOffsetC"
        if {[catch {exec cavget -list=$BPMName: -list=x,y -list=:BpmZeroOffsetC} previousOffsetList] || [llength $previousOffsetList]!=2} {
            return -code error "Unable to read present offsets for $BPMName: $previousOffsetList"
        }
        set xPrevious [lindex $previousOffsetList 0]
        set yPrevious [lindex $previousOffsetList 1]

        if [catch {sdds load ${bpmNoColon}-$index-sextResults.sdds offsetResults} result] {
            return -code error "ProcessSextupoleMeasurement (11): $result"
        }
        set xNew [expr 1e6*[lindex $offsetResults(Parameter.x0) 0]]
        set yNew [expr 1e6*[lindex $offsetResults(Parameter.y0) 0]]
        eval $statusCallback {"Installed offsets: [format %4.1f $xPrevious]um [format %4.1f $yPrevious]um"}
        eval $statusCallback {"New offsets:       [format %4.1f $xNew]um [format %4.1f $yNew]um"}

        puts $fd "Installed offsets: [format %4.1f $xPrevious]um [format %4.1f $yPrevious]um"
        puts $fd "New offsets:       [format %4.1f $xNew]um [format %4.1f $yNew]um"
        puts $fd "\nMeasurement settings:"
        foreach item $sextMeas(ColumnNames) {
            puts $fd "    $item: [set $item]"
        }
        close $fd
    }

    # start of review block
    if $printout {
        set displayWidget .[APSUniqueName printout]
        APSFileDisplayWindow $displayWidget -fileName ${root}-printout.txt -width 80
        APSDialogBoxAddButton .install -parent $displayWidget \
          -text Install... \
          -command "InstallSextupoleOffsets -rootname $dataDir/$date/$bpmDir/${root} -statusCallback \"$statusCallback\"" \
          -contextHelp "Sends the offset you are looking at back to the BPM as the Offset value."
        
        APSDialogBoxAddButton .installtr -parent $displayWidget \
          -text "Install/Adjust Setpoint..." \
          -command "InstallSextupoleOffsets -rootname $dataDir/$date/$bpmDir/${root} -statusCallback \"$statusCallback\" -adjustSetpoint 1" \
          -contextHelp "Sends the offset you are looking at back to the BPM as the Offset value.  Also adjusts the setpoint so that the orbit readback does not change.  Useful in keeping the same orbit."
        
        APSDialogBoxAddButton .remove -parent $displayWidget \
          -text "Remove Measurement..." \
          -command "RemoveBadMeasurement -fileName $dataDir/$date/$bpmDir/${root} -statusCallback \"$statusCallback\"" \
          -contextHelp "Move the bad data to a wastebasket directory."
    }

    if $makePlots {
        eval $statusCallback {"ProcessSextupoleMeasurement: bpmName = $BPMName, makePlots = $makePlots"}
        exec sddsplot -labelsize=0.025 \
            -col=SextxKickAve,dKx {-xlabel=Measured x kick ($gm$rrad)} {-ylabel=Fit x kick ($gm$rrad)} \
            -graph=sym ${root}-sextResults.sdds -title=$date/$root -factor=xm=1e6,ym=1e6 -end \
            -col=SextyKickAve,dKy {-xlabel=Measured y kick ($gm$rrad)} {-ylabel=Fit y kick ($gm$rrad)} \
            -graph=sym ${root}-sextResults.sdds -title=$date/$root -factor=xm=1e6,ym=1e6 -end \
            -column=Step,Chi ${root}.fitlog -title=$date/$root -mode=y=autolog &
    }
    eval $statusCallback {"ProcessSextupoleMeasurement: Done with ${root}."}
    eval $ultimateCallBack
}
