from phaser.pipeline import mr_object
import phaser

class Progress(object):
    """
    Class to indicate that a search is in progress
    """
    
    def __init__(self, calculation):
        
        self.calculation = calculation
        

# Results
class Result(object):
    """
    Base class for identification
    """
    
    def __init__(self, calculation, logfile, data):
        
        self.calculation = calculation
        self.logfile = logfile
        self.data = data
        

# Calculations
class Calculation(object):
    """
    Base class for identification
    """
    
    def __init__(self, data, partial):
        
        self.data = data
        self.partial = partial
        
        
    def cleanup(self):
        
        self.data = None
        
        
    def __str__(self):
        
        return self.__class__.__name__
    

class RotationSearch(Calculation):
    """
    Rotation search
    """
    
    def __init__(self, data, partial, ensemble, cutoff):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.ensemble = ensemble
        self.cutoff = cutoff
        
        
    def run(self, cpus = 1):
        
        input = phaser.InputMR_FRF()
        input.setJOBS( cpus )
        
        self.data.apply( input = input )
        ensembles = set( self.partial.ensembles() )
        ensembles.add( self.ensemble )
        
        for ense in ensembles:
            ense.apply_ensemble_definition( input = input )
        
        input.setSOLU( [ self.partial.mr_set() ] )
        input.addSEAR_ENSE_NUM( self.ensemble.identifier(), 1 )
        input.setXYZO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        input.setPEAK_ROTA_CUTO( self.cutoff )
        input.setSEAR_DEEP( False )
        input.setELLG_USE( False )
        
        result = phaser.GIL_runMR_FRF( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        raw = result.getDotRlist()
        assert len( raw ) == 1
        ms = raw[0]
        assert all( [ r.MODLID == self.ensemble.identifier() for r in ms.RLIST ] )
        rotations = [ ( r.EULER , r.RF, r.RFZ ) for r in ms.RLIST ]
        return ( True, result.logfile(), rotations )
            
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Rotation calculation failed:\n%s" % logfile
        
        peaks = [
            mr_object.RotationPeak(
                ensemble = self.ensemble,
                rotation = mr_object.Rotation.Euler( angles = a ),
                rf = rf,
                rfz = rfz
                )
            for ( a, rf, rfz ) in definitions
            ]
        
        return Result( calculation = self, logfile = logfile, data = peaks )
    
    
class RestrictedRotationSearch(Calculation):
    """
    Rotation search restricted to a volume
    """
    
    def __init__(self, data, partial, ensemble, rotation, sweep):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.ensemble = ensemble
        self.rotation = rotation
        self.sweep = sweep
    
    
    def run(self, cpus = 1):
        
        input = phaser.InputMR_BRF()
        input.setJOBS( cpus )
        self.data.apply( input = input )
        ensembles = set( self.partial.ensembles() )
        ensembles.add( self.ensemble )
        
        for ense in set( ensembles ):
            ense.apply_ensemble_definition( input )
            
        input.setXYZO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        
        input.setROTA_VOLU( "AROUND" )
        input.setROTA_EULE( self.rotation.euler() )
        input.setROTA_RANG( self.sweep )
        
        input.setSOLU( [ self.partial.mr_set() ] )
        input.addSEAR_ENSE_NUM( self.ensemble.identifier(), 1 )
        input.setSOLU( [ self.partial.mr_set() ] )
         
        result = phaser.GIL_runMR_BRF( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        raw = result.getDotRlist()
        assert len( raw ) == 1
        ms = raw[0]
        assert all( [ r.MODLID == self.ensemble.identifier() for r in ms.RLIST ] )
        rotations = [ ( r.EULER , r.RF, r.RFZ ) for r in ms.RLIST ]
        return ( True, result.logfile(), rotations )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Rotation calculation failed:\n%s" % logfile
        
        peaks = [
            mr_object.RotationPeak(
                ensemble = self.ensemble,
                rotation = mr_object.Rotation.Euler( angles = a ),
                rf = rf,
                rfz = rfz
                )
            for ( a, rf, rfz ) in definitions
            ]
        
        return Result( calculation = self, logfile = logfile, data = peaks )
        
        
class TranslationSearch(Calculation):
    """
    Translation search
    """
    
    def __init__(self, data, partial, rpeak):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.rpeak = rpeak
        
        
    def run(self, cpus = 1):
        
        input = phaser.InputMR_FTF()
        input.setJOBS( cpus )
        
        self.data.apply( input = input )
        ensembles = set( self.partial.ensembles() )
        ensembles.add( self.rpeak.ensemble )
        
        for ense in ensembles:
            ense.apply_ensemble_definition( input )
        
        input.setSOLU(
            [ self.partial.mr_set( rotations = [ self.rpeak ] ) ]
            )
        input.setXYZO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        input.setSGAL_SELE( "NONE" )
        input.setZSCO_USE( False )
        input.setSEAR_METH( "FULL" )
        
        result = phaser.GIL_runMR_FTF( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        raw = result.getDotSol()
        assert all(
            [ t.ANNOTATION.startswith( self.partial.identifier() ) for t in raw ]
            )
        assert all( [ 0 < len( t.KNOWN ) for t in raw ] )
        assert all(
            [ t.KNOWN[-1].getModlid() == self.rpeak.ensemble.identifier()
                for t in raw ]
            )
        definitions = [
            ( t.KNOWN[-1].getFracT(), t.TF, t.TFZ, t.KNOWN[-1].getBfac() ) for t in raw
            ]
        
        return ( True, result.logfile(), definitions )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Translation calculation failed:\n%s" % logfile
        
        peaks = [
            mr_object.TranslationPeak(
                ensemble = self.rpeak.ensemble,
                rotation = self.rpeak.rotation,
                translation = t,
                tf = tf,
                tfz = tfz,
                bfactor = bfac
                )
            for ( t, tf, tfz, bfac ) in definitions
            ]
        
        return Result( calculation = self, logfile = logfile, data = peaks )
    
    
class RestrictedTranslationSearch(Calculation):
    """
    Rotation search restricted to a volume
    """
    
    def __init__(self, data, partial, ensemble, rotations, translation, extent):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.ensemble = ensemble
        self.rotations = rotations
        self.translation = translation
        self.extent = extent
    
    
    def run(self, cpus = 1):
        
        input = phaser.InputMR_BTF()
        input.setJOBS( cpus )
        
        self.data.apply( input = input )
        ensembles = self.partial.ensembles()
        ensembles.append( self.ensemble )
        
        for ense in set( ensembles ):
            ense.apply_ensemble_definition( input )
            
        input.setXYZO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        
        input.setTRAN_VOLU( "AROUND" )
        input.setTRAN_POIN( self.translation )
        input.setTRAN_RANG( self.extent )
        input.setZSCO_USE( False )
        
        input.setSOLU( [ self.partial.mr_set( rotations = self.rpeaks() ) ] )
        
        result = phaser.GIL_runMR_BTF( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        raw = result.getDotSol()
        data = []
    
        assert all(
            [ t.ANNOTATION.startswith( self.partial.identifier() ) for t in raw ]
            )
        assert all( [ 0 < len( t.KNOWN ) for t in raw ] )
        assert all(
            [ t.KNOWN[-1].getModlid() == self.ensemble.identifier() for t in raw ]
            )
        definitions = [
            ( t.KNOWN[-1].getR(), t.KNOWN[-1].getFracT(), t.TF, t.TFZ, t.KNOWN[-1].getBfac() )
            for t in raw
            ]
        
        return ( True, result.logfile(), definitions )
    
    
    def rpeaks(self):
        
        return [
            mr_object.RotationPeak( ensemble = self.ensemble, rotation = r, rf = 0, rfz = 0 )
            for r in self.rotations
            ]
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Translation calculation failed:\n%s" % logfile
        
        peaks = [
            mr_object.TranslationPeak(
                ensemble = self.ensemble,
                rotation = mr_object.Rotation.Matrix( elements = r ),
                translation = t,
                tf = tf,
                tfz = tfz,
                bfactor = bfac
                )
            for ( r, t, tf, tfz, bfac ) in definitions
            ]
        
        return Result( calculation = self, logfile = logfile, data = peaks )


class SinglePoolPackingCheck(object):
    """
    Packing check
    """
    
    ALLOWED = 10
    
    def __init__(self, data, partials, tpeaks):
        
        assert len( partials ) == len( tpeaks )
        self.data = data
        self.partials = partials
        self.tpeaks = tpeaks
        
        
    def run(self, cpus = 1):
        
        import operator
        
        input = phaser.InputMR_PAK()
        input.setJOBS( cpus )
        
        self.data.apply_symmetry( input = input )
        ensembles = set(
            reduce( operator.add, [ p.ensembles() for p in self.partials ] )
            )
        ensembles.update( [ t.ensemble for t in self.tpeaks ] )
        
        for ense in ensembles:
            ense.apply_ensemble_definition( input )
            
        mr_sets = []
        
        for ( i, ( p, t ) ) in enumerate( zip( self.partials, self.tpeaks ) ):
            ms = p.mr_set( translations = [ t ] )
            ms.ANNOTATION = "%s %s" % ( self.peakid( index = i ), ms.ANNOTATION )
            mr_sets.append( ms )
        
        input.setSOLU( mr_sets )
        input.setPACK_SELE( "ALLOW" )
        input.setPACK_CUTO( self.ALLOWED )
        input.setXYZO( False )
        input.setMUTE( True )
        
        result = phaser.GIL_runMR_PAK( input )
        
        if result.Failed():
            return ( False, result.logfile(), None ) 
        
        ms_for = dict(
            [ ( ms.ANNOTATION.split()[0], ms ) for ms in result.getDotSol() ]
            )
        
        definitions = []
        
        for ( i, ( p, t ) ) in enumerate( zip( self.partials, self.tpeaks ) ):
            peakid = self.peakid( index = i )
            
            if peakid not in ms_for:
                definitions.append( [] )
                continue
            
            ms = ms_for[ peakid ]
            
            assert 0 < len( ms.KNOWN )
            assert ms.ANNOTATION.split()[1].startswith( p.identifier() ) 
            assert ms.KNOWN[-1].getModlid() == t.ensemble.identifier(), (
                "%s != %s" %  ( ms.KNOWN[-1].getModlid(), t.ensemble.identifier() )
                )
            
            # Need to save both as packing function changes the rotation
            definitions.append(
                ( ms.KNOWN[-1].getR(), ms.KNOWN[-1].getFracT(), ms.PAK )
                )
            
        return ( True, result.logfile(), definitions )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Packing calculation failed:\n%s" % logfile
        
        data = []
        
        for ( d, t ) in zip( definitions, self.tpeaks ):
            if not d:
                data.append( None )
                
            else:
                ( rot, tra, clashes ) = d
                data.append(
                    mr_object.Peak(
                        ensemble = t.ensemble,
                        rotation = mr_object.Rotation.Matrix( elements = rot ),
                        translation = tra,
                        bfactor = t.bfactor
                        )
                    )
                
        assert len( data ) == len( self.tpeaks )
        return Result( calculation = self, logfile = logfile, data = data )
    
    
    def peakid(self, index):
        
        return "try%d" % index
    
    
    def __str__(self):
        
        return self.__class__.__name__
        
        
class Refinement(Calculation):
    """
    Refinement
    """
    
    def __init__(self, data, partial, peak, significant, rfz, tfz, b_factor_refinement = True):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        self.significant = significant
        self.rfz = rfz
        self.tfz = tfz
        self.b_factor_refinement = b_factor_refinement
        
        
    def run(self, cpus = 1):
        
        input = phaser.InputMR_RNP()
        input.setJOBS( cpus )
        
        self.data.apply( input = input ) 
        cell = self.data.uctbx_unit_cell()
        ensembles = self.partial.ensembles()
        disassembled_peak = self.peak.independent_component_list( cell = cell )
        ensembles.extend( [ p.ensemble for p in disassembled_peak ] )
        
        for ense in set( ensembles ):
            ense.apply_ensemble_definition( input )
        
        input.setSOLU( [ self.partial.mr_set( translations = disassembled_peak ) ] )
        input.setXYZO( False )
        input.setHKLO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        
        input.setMACM_PROT( "CUSTOM" )
        input.addMACM(
            ref_rota = True,
            ref_tran = True,
            ref_bfac = self.b_factor_refinement,
            ref_vrms = False,
            )
        
        result = phaser.GIL_runMR_RNP( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        sol = result.getDotSol()
        assert len( sol ) == 1
        ms = sol[0]
        assert ms.ANNOTATION.startswith( self.partial.identifier() )
        assert len( ms.KNOWN ) == len( ensembles )
        assert all( [ nd.getModlid() == e.identifier()
                for( nd, e ) in zip( ms.KNOWN, ensembles ) ] )
        definitions = [ ( nd.getR(), nd.getFracT(), nd.getBfac() ) for nd in ms.KNOWN ]
        
        return ( True, result.logfile(), ( ms.LLG, definitions ) )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Refinement calculation failed:\n%s" % logfile
        
        ( llg, data ) = definitions
        cell = self.data.uctbx_unit_cell()
        
        ensembles = ( self.partial.ensembles()
            + [ p.ensemble for p in self.peak.independent_component_list( cell = cell ) ] )
        assert len( ensembles ) == len( data )
        structure = mr_object.Structure(
            peaks = [
                mr_object.Peak(
                    ensemble = e,
                    rotation = mr_object.Rotation.Matrix( elements = r ),
                    translation = t,
                    bfactor = b
                    )
                for ( e, ( r, t, b ) ) in zip( ensembles, data )
                ],
            significant = self.significant,
            rfz = self.rfz,
            tfz = self.tfz,
            predecessor = self.partial
            )
        return Result(
            calculation = self,
            logfile = logfile,
            data = ( llg, structure )
            )
        
        
class PeakRefinement(Calculation):
    """
    Refinement of peak only
    """
    
    def __init__(self, data, partial, peak, protocol = True):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        self.b_factor_refinement = protocol
        
        
    def run(self, cpus = 1):
        
        input = phaser.InputMR_RNP()
        input.setJOBS( cpus )
        
        self.data.apply( input = input ) 
        cell = self.data.uctbx_unit_cell()
        ensembles = set( self.partial.ensembles() )
        ensembles.add( self.peak.ensemble )
        
        for ense in ensembles:
            ense.apply_ensemble_definition( input )
        
        input.setSOLU(
            [
                self.partial.mr_set(
                    translations = [ self.peak ],
                    sflags = ( True, True, True ),
                    ),
                ]
            )
        input.setXYZO( False )
        input.setHKLO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        
        input.setMACM_PROT( "CUSTOM" )
        input.addMACM(
            ref_rota = True,
            ref_tran = True,
            ref_bfac = self.b_factor_refinement,
            ref_vrms = False,
            )
        
        result = phaser.GIL_runMR_RNP( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        sol = result.getDotSol()
        assert len( sol ) == 1
        ms = sol[0]
        assert ms.ANNOTATION.startswith( self.partial.identifier() )
        assert 0 < len( ms.KNOWN )
        assert ms.KNOWN[-1].getModlid() == self.peak.ensemble.identifier()
        nd = ms.KNOWN[-1]
        
        definitions = ( nd.getR(), nd.getFracT(), nd.getBfac(), ms.LLG )
        
        return ( True, result.logfile(), definitions )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Refinement calculation failed:\n%s" % logfile
        
        ( r, t, b, llg ) = definitions
        peak = mr_object.Peak(
            ensemble = self.peak.ensemble,
            rotation = mr_object.Rotation.Matrix( elements = r ),
            translation = t,
            bfactor = b
            )
        
        return Result(
            calculation = self,
            logfile = logfile,
            data = ( llg, peak )
            )
        
        
class TFZCalculation(Calculation):
    """
    TFZ calculation
    """
    
    def __init__(self, data, partial, peak):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        
        
    def run(self, cpus = 1):
        
        input = phaser.InputMR_BTF()
        input.setJOBS( cpus )
        
        self.data.apply( input = input )
        ensembles = self.partial.ensembles()
        ensembles.append( self.peak.ensemble )
        
        for ense in set( ensembles ):
            ense.apply_ensemble_definition( input )
            
        input.setXYZO( False )
        input.setMUTE( True )
        input.setMACA_PROT( "OFF" )
        
        input.setTRAN_VOLU( "AROUND" )
        input.setTRAN_POIN( self.peak.translation )
        input.setTRAN_RANG( 1 )
        input.setTRAN_FRAC( True )
        input.setZSCO_USE( False )
        
        rpeak = mr_object.RotationPeak(
            ensemble = self.peak.ensemble,
            rotation = self.peak.rotation,
            rf = 0,
            rfz = 0
            )
        
        input.setSOLU( [ self.partial.mr_set( rotations = [ rpeak ] ) ] )
         
        result = phaser.GIL_runMR_BTF( input )
        
        if result.Failed():
            return ( False, result.logfile(), None )
        
        sol = result.getDotSol()
        
        # Not sure whether this can happen
        if len( sol ) == 0:
            data = ( 0.00, 0.00, self.peak.translation )
        
        else:
            assert 0 < len( sol[0].KNOWN )
            data = ( sol[0].TF, sol[0].TFZ, sol[0].KNOWN[-1].getFracT() )
        
        return ( True, result.logfile(), data )
    
    
    def process(self, results):
        
        ( success, logfile, ( tf, tfz, tra ) ) = results
        
        if not success:
            raise RuntimeError, "TFZ calculation failed:\n%s" % logfile
        
        peak = mr_object.TranslationPeak(
            ensemble = self.peak.ensemble,
            rotation = self.peak.rotation,
            translation = tra,
            tf = tf,
            tfz = tfz,
            bfactor = self.peak.bfactor
            )
        
        return Result( calculation = self, logfile = logfile, data = peak )
        
        
class Evaluation(Calculation):
    """
    Peak evaluation
    """
    
    def __init__(self, data, partial, peak):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        
        
    def run(self, cpus = 1):
        
        # Packing check
        packing = SinglePoolPackingCheck(
            data = self.data,
            partials = [ self.partial ],
            tpeaks = [ self.peak ]
            )
        
        pak_raw = packing.run( cpus = cpus )
        
        try:
            pak_result = packing.process( results = pak_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        assert len( pak_result.data ) == 1
        
        if pak_result.data[0] is None:
            return ( True, pak_result.logfile, None )
        
        # Discard packing rearrangement to remove instability caused by ensemble differences
        tfzcalc = TFZCalculation(
            data = self.data,
            partial = self.partial,
            peak = self.peak
            )
        tfz_raw = tfzcalc.run( cpus = cpus )
        
        return ( True, pak_result.logfile, tfz_raw )
    
    
    def process(self, results):
        
        ( pak_success, logfile, tfz_raw ) = results
        
        if not pak_success:
            raise RuntimeError, logfile
        
        if not tfz_raw:
            data = ( False, None )
            
        else:
            # Process here to ensure object bindings are established correctly
            tfzcalc = TFZCalculation(
                data = self.data,
                partial = self.partial,
                peak = self.peak
                )
            result = tfzcalc.process( results = tfz_raw )
            data = ( True, result.data )
            logfile += result.logfile
        
        return Result( calculation = self, logfile = logfile, data = data )
    
    
class RefinedEvaluation(Calculation):
    """
    Peak evaluation
    """
    
    def __init__(self, data, partial, peak, protocol = True):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        self.protocol = protocol
        
        
    def run(self, cpus = 1):
        
        # Packing check
        packing = SinglePoolPackingCheck(
            data = self.data,
            partials = [ self.partial ],
            tpeaks = [ self.peak ]
            )
        
        pak_raw = packing.run( cpus = cpus )
        
        try:
            pak_result = packing.process( results = pak_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        assert len( pak_result.data ) == 1
        
        if pak_result.data[0] is None:
            return ( True, pak_result.logfile, None )
        
        # Discard packing rearrangement to remove instability caused by ensemble differences
        refinement = PeakRefinement(
            data = self.data,
            partial = self.partial,
            peak = self.peak,
            protocol = self.protocol
            )
        refinement_raw = refinement.run()
        
        try:
            ref_result = refinement.process( results = refinement_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        logfile = pak_result.logfile + ref_result.logfile
        ( llg, peak ) = ref_result.data
            
        tfzcalc = TFZCalculation(
            data = self.data,
            partial = self.partial,
            peak = peak
            )
        tfz_raw = tfzcalc.run( cpus = cpus )
        
        try:
            tfz_result = tfzcalc.process( results = tfz_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        tfz_peak = tfz_result.data # this may change translation to one with highest TF
        
        return (
            True,
            logfile + tfz_result.logfile,
            (
                peak.rotation,
                peak.translation,
                peak.bfactor,
                tfz_peak.tf,
                tfz_peak.tfz,
                llg,
                ),
            )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, logfile
        
        if not definitions:
            data = ( False, None, None )
            
        else:
            ( r, t, b, tf, tfz, llg ) = definitions
            peak = mr_object.TranslationPeak(
                ensemble = self.peak.ensemble,
                rotation = r,
                translation = t,
                tf = tf,
                tfz = tfz,
                bfactor = self.peak.bfactor
                )
            data = ( True, peak, llg )
        
        return Result( calculation = self, logfile = logfile, data = data )
    
    
class MultiOriginEvaluation(Calculation):
    """
    Peak evaluation
    """
    
    def __init__(self, data, partial, peak, origins):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        self.origins = origins
        
        
    def run(self, cpus = 1):
        
        peaks = self.peaks()
        
        packing = SinglePoolPackingCheck(
            data = self.data,
            partials = [ self.partial ] * len( peaks ),
            tpeaks = peaks
            )
        
        pak_raw = packing.run( cpus = cpus )
        
        try:
            pak_result = packing.process( results = pak_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        assert len( pak_result.data ) == len( peaks )
        tdata = []
        
        for ( p, pak_p ) in zip( peaks, pak_result.data ):
            if pak_p is None:
                tdata.append( None )
                continue
            
            tfzcalc = TFZCalculation(
                data = self.data,
                partial = self.partial,
                peak = p
                )
            tfz_raw = tfzcalc.run( cpus = cpus )
            tdata.append( tfz_raw )
            
        return ( True, pak_result.logfile, tdata )
    
    
    def peaks(self):
        
        ( mx, my, mz ) = self.peak.translation
        
        return [
            mr_object.Peak(
                ensemble = self.peak.ensemble,
                rotation = self.peak.rotation,
                translation = ( mx + x, my + y, mz + z ),
                bfactor = self.peak.bfactor
                )
            for ( x, y, z ) in self.origins
            ]
    
    
    def process(self, results):
        
        ( pak_success, logfile, tdata ) = results
        
        if not pak_success:
            raise RuntimeError, logfile
        
        peaks = self.peaks()
        assert len( peaks ) == len( tdata )
        tpeaks = []
        
        for ( p, td ) in zip( peaks, tdata ):
            if td is None:
                continue
            
            # Process here to ensure object bindings are established correctly
            tfzcalc = TFZCalculation(
                data = self.data,
                partial = self.partial,
                peak = p
                )
            result = tfzcalc.process( results = td )
            logfile += result.logfile
            tpeaks.append( result.data )
            
        if not tpeaks:
            data = ( False, None )
            
        else:
            data = ( True, max( tpeaks, key = lambda tp: tp.tf ) )
        
        return Result( calculation = self, logfile = logfile, data = data )
    
    
class RestrictedSearch(Calculation):
    """
    Rotation and translation searches restricted to a volume
    """
    
    def __init__(self, data, partial, ensemble, rotation, sweep, translation, extent):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.ensemble = ensemble 
        self.rotation = rotation
        self.sweep = sweep
        self.translation = translation
        self.extent = extent
        
        
    def run(self, cpus = 1):
        
        restricted_rotation_search = RestrictedRotationSearch(
            data = self.data,
            partial = self.partial,
            ensemble = self.ensemble,
            rotation = self.rotation,
            sweep = self.sweep,
            )
        
        rot_tmp_results = restricted_rotation_search.run( cpus = cpus )
        
        try:
            rot_results = restricted_rotation_search.process( results = rot_tmp_results )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        if not rot_results.data:
            return ( True, rot_results.logfile, [] )
        
        restricted_translation_search = RestrictedTranslationSearch(
            data = self.data,
            partial = self.partial,
            ensemble = self.ensemble,
            rotations = [ p.rotation for p in rot_results.data ],
            translation = self.translation,
            extent = self.extent,
            )
        
        tra_tmp_results = restricted_translation_search.run( cpus = cpus )
        
        try:
            tra_results = restricted_translation_search.process( results = tra_tmp_results )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        return (
            True,
            rot_results.logfile + "\n" + tra_results.logfile,
            [ ( p.rotation, p.translation, p.tf, p.tfz, p.bfactor )
                for p in tra_results.data ],
            )
    
    
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Restricted search calculation failed:\n%s" % logfile
        
        peaks = [
            mr_object.TranslationPeak(
                ensemble = self.ensemble,
                rotation = r,
                translation = t,
                tf = tf,
                tfz = tfz,
                bfactor = bfac
                )
            for ( r, t, tf, tfz, bfac ) in definitions
            ]
        
        return Result( calculation = self, logfile = logfile, data = peaks )


class RestrictedSearchEvaluation(Calculation):
    """
    Select best peak from restricted search
    """
    
    def __init__(self, data, partial, peak, sweep, extent):
        
        super( self.__class__, self ).__init__( data = data, partial = partial )
        self.peak = peak
        self.sweep = sweep
        self.extent = extent
        
        
    def run(self, cpus = 1):
        
        search = RestrictedSearch(
            data = self.data,
            partial = self.partial,
            ensemble = self.peak.ensemble,
            rotation = self.peak.rotation,
            sweep = self.sweep,
            translation = self.peak.translation,
            extent = self.extent,
            )
        
        search_tmp = search.run( cpus = cpus )
        
        try:
            search_results = search.process( results = search_tmp )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        if not search_results.data:
            best = mr_object.TranslationPeak(
                ensemble = self.peak.ensemble,
                rotation = self.peak.rotation,
                translation = self.peak.translation,
                tf = 0,
                tfz = 0,
                bfactor = self.peak.bfac
                )
            
        else:
            best = max( search_results.data, key = lambda s: s.tf )
        
        # Packing check
        pak = SinglePoolPackingCheck(
            data = self.data,
            partials = [ self.partial ],
            tpeaks = [ best ]
            )
        
        pak_raw = pak.run( cpus = cpus )
        
        try:
            pak_results = pak.process( results = pak_raw )
            
        except RuntimeError, e:
            return ( False, e, None )
        
        assert len( pak_results.data ) == 1
        
        return (
            True,
            search_results.logfile + "\n" + pak_results.logfile,
            (
                pak_results.data[0] is not None,
                ( best.rotation, best.translation, best.tf, best.tfz, best.bfactor ),
                )
            )
        
        
    def process(self, results):
        
        ( success, logfile, definitions ) = results
        
        if not success:
            raise RuntimeError, "Restricted search evaluation calculation failed:\n%s" % logfile
        
        ( is_pak, ( r, t, tf, tfz, bfac ) ) = definitions
        
        peak = mr_object.TranslationPeak(
            ensemble = self.peak.ensemble,
            rotation = r,
            translation = t,
            tf = tf,
            tfz = tfz,
            bfactor = bfac
            )
        
        return Result( calculation = self, logfile = logfile, data = ( is_pak, peak ) )
