from phaser.pipeline import stage
from phaser.pipeline import calculation
from phaser.pipeline import expert
from phaser.pipeline import mr_object

from phaser import output

import operator

    
class Plan(object):
    """
    A plan on the search - two level implementation
    """
    
    def __init__(
        self,
        level1,
        level2,
        postprocessors,
        criterion,
        b_factor_refinement = True,
        threshold_refinement = 0.75,
        threshold_postrefinement = 0.75,
        threshold_space_group = 0.5,
        ):
        
        self.level1 = level1
        self.level2 = level2
        self.postprocessors = postprocessors
        self.criterion = criterion
        self.b_factor_refinement = b_factor_refinement
        self.threshold_refinement = threshold_refinement
        self.threshold_postrefinement = threshold_postrefinement
        self.threshold_space_group = threshold_space_group
        
        
    def progress(self, case, initial, context):
        
        return LocationProgress(
            case = case,
            initials = initial,
            context = context,
            level1 = self.level1,
            level2 = self.level2,
            postprocessors = self.postprocessors,
            criterion = self.criterion,
            threshold_refinement = self.threshold_refinement,
            threshold_postrefinement = self.threshold_postrefinement,
            b_factor_refinement = self.b_factor_refinement
            )
        
        
    def __str__(self):
        
        return "\n".join(
            [ "Solvers to use before finding a solution:" ]
            + [ "  %s" % s for s in ( self.level1 + self.level2 ) ]
            + [ "", "Solvers to use after finding a solution:" ]
            + [ "  %s" % s for s in self.level2 ]
            )
        

class QueueInterface(object):
    """
    Interface to a global queue that only shows jobs submitted through it
    """
    
    def __init__(self, queue):
        
        self.queue = queue
        self.complete = set()
        
        
    def submit(self, owner, calculation, extra):
        
        self.queue.submit(
            owner = ( self, owner ),
            calculation = calculation,
            extra = extra,
            )
        
        
    def poll(self):
        
        for job in self.queue.completed():
            ( qinterface, owner ) = job.owner
            
            if qinterface != self:
                continue
            
            self.queue.remove( job )
            job.owner = owner
            
            self.complete.add( job )
            
            
    def completed(self):
        
        return list( self.complete )
    
    
    def remove(self, job):
        
        self.complete.remove( job )
        
        
    def full(self):
        
        return self.queue.full()
    
    
class LocationProgress(object):
    """
    Progress of a molecular replacement round
    """
    
    def __init__(
        self,
        case,
        initials,
        context,
        level1,
        level2,
        postprocessors,
        criterion,
        threshold_refinement,
        threshold_postrefinement,
        b_factor_refinement,
        ):
        
        self.case = case
        self.context = context
        self.criterion = criterion
        self.level1 = level1
        self.level2 = level2
        self.postprocessors = postprocessors
        
        self.active = [ self ] + self.level2 + self.level1
        self.switch_strategy = self.switch_to_level_2_ks
        
        self.board = mr_object.Blackboard()
        
        for ( index, structure ) in enumerate( initials, start = 1 ):
            bbobj = stage.Initial(
                case = self.case,
                partial = structure,
                missing = self.case.composition - structure.composition(),
                index = index
                )
            self.board.write( obj = bbobj )
            
        self.lowest_initial = min( [ s.score() for s in self.initials ] )
        
        self.inspect = expert.SelectRange( expert = self, type = stage.Pack, count = None )
        
        self.threshold_refinement = threshold_refinement
        self.threshold_postrefinement = threshold_postrefinement
        self.b_factor_refinement = b_factor_refinement
        
        self.queue = QueueInterface( queue = context.queue )
        
        self.refineds = []
        self.solutions = []
    
    
    def identifier(self):
        
        return "Space group: %s" % self.case.space_group_symbol()
    
    
    @property
    def identity_to_rms(self):
        
        return self.context.identity_to_rms
    
        
    @property
    def output(self):
        
        return self.context.output
    
    
    @property
    def superposition(self):
        
        return self.context.superposition
    
    
    def manipulate(self, candidate, board, case, context):
        
        context.output.verbose(
            msg = output.subtitle( text = "Solution identification" )
            )
        context.output.notify(
            message = "status",
            data = "Solution identification"
            )
        context.output.verbose( msg = "Space group %s" % case.space_group_symbol() )
        context.output.verbose( msg = output.blank() )
        judgements = []
        
        for ( index, c ) in enumerate( candidate, start = 1 ):
            if self.criterion( peak = c.tf_peak() ):
                significant = True
                
            else:
                significant = False
            
            judgements.append( significant )
            context.output.verbose(
                msg = "Peak: %s" % "SIGNIFICANT" if significant else "NOT significant"
                )
            context.output.indent()
            context.output.verbose( msg = c )
            context.output.dedent()
            context.output.verbose( msg = output.blank() )
            c.processed_by.add( self )
            st = stage.Scored( pack = c, significant = significant )
            self.board.write( obj = st )
        
        if any( judgements ):
            self.switch_strategy( context = context )
        
        context.output.verbose(
            msg = output.section_close( text = "Solution identification finished" )
            )
        
        
    def switch_to_level_2_ks(self, context):
        
        context.output.verbose( msg = "Switching solution strategy:" )
        context.output.verbose( msg = output.blank() )
        context.output.verbose( msg = "Current:" )
        context.output.indent()
        
        for ( i, ks ) in enumerate( self.active, start = 1 ):
            context.output.verbose( msg = "%d: %s" % ( i, ks ) )
            
        self.active = [ self ] + self.level2
        self.switch_strategy = self.already_switched
        
        context.output.dedent()
        context.output.verbose( msg = output.blank() )
        context.output.verbose( msg = "Next:" )
        context.output.indent()
        
        for ( i, ks ) in enumerate( self.active, start = 1 ):
            context.output.verbose( msg = "%d: %s" % ( i, ks ) )
            
        context.output.dedent()
        
        
    def already_switched(self, context):
        
        pass
        
        
    def show_search_tree(self):
        
        self.output.info( msg = output.subtitle( text = "Summary of searches" ) )
        self.output.info( msg = output.underlined( text = "Abbreviations:" ) )
        self.output.indent()
        self.output.info( msg = "n/a: search not performed" )
        self.output.info( msg = "RF-score:" )
        self.output.indent()
        self.output.info( msg = "LPR - located in previous round" )
        self.output.dedent()
        self.output.info( msg = "Z-score:" )
        self.output.indent()
        self.output.info( msg = "* - significant" )
        self.output.info( msg = "S - superposed" )
        self.output.info( msg = "A - amalgamated" )
        self.output.info( msg = "R - completed according to known regular structure" )
        self.output.dedent()
        self.output.dedent()
        self.output.info( msg = "" )
        data = (
            sorted( self.initials, key = lambda i: i.index ),
            sorted( self.ensembles, key = lambda e: e.index ),
            sorted( self.board.read( type = stage.Templated ), key = lambda t: t.score(), reverse = True ),
            sorted( self.board.read( type = stage.Filled ), key = lambda e: e.index ),
            sorted( self.board.read( type = stage.PreviouslyEstablishedRotation ), key = lambda e: e.index ),
            sorted( self.board.read( type = stage.NormalRotation ), key = lambda r: r.index ),
            sorted( self.translations, key = lambda t: t.index ),
            self.board.read( type = stage.Packed ) + self.board.read( type = stage.Clash ),
            sorted( self.board.read( type = stage.Scored ), key = lambda s: s.score(), reverse = True ),
            self.refineds,
            )
        
        tree = StageTreeTable()
        
        for level in data:
            for child in level:
                tree.add( child = child )
        
        self.output.info( msg = tree.format() )
        
        
    def iterate(self):
        
        self.output.debug( msg = output.subtitle( text = self.identifier() ) )
        self.output.debug( msg = output.underlined( text = "Job polling" ) )
        self.queue.poll()
        
        for job in self.queue.completed():
            self.queue.remove( job )
            self.output.debug(
                msg = output.heading( text = "Job %s: finished" % job )
                )
            self.output.debug( msg = "Started by: %s" % job.owner )
            job.owner.process(
                calculation = job.calculation,
                result = job.result,
                extra = job.extra,
                board = self.board,
                case = self.case,
                context = self
                )
            self.output.debug(
                msg = output.section_close( text = "Job processing finished" )
                )
        
        self.output.debug(
            msg = output.section_close( text = "Job polling completed" )
            )
        
        if self.queue.full():
            return False
        
        action = False
        
        self.output.debug( msg = output.underlined( text = "Board evaluation" ) )
        self.output.debug( 
            msg = "Available cpus - querying knowledge sources" 
            )
        
        for ks in self.active:
            self.output.debug( msg = output.heading( text = "Evaluation" ) )
            self.output.debug( msg = "Knowledge source: %s" % ks )
            candidate = ks.inspect(
                board = self.board,
                case = self.case,
                context = self
                )
             
            if candidate:
                self.output.debug(
                    msg = output.section_close( text = "Selected for action" )
                    )
                ks.manipulate(
                    candidate = candidate,
                    board = self.board,
                    case = self.case,
                    context = self
                    )
                action = True
                break
            
            self.output.debug(
                msg = output.section_close( text = "No action possible" )
                )
        
        else:
            self.output.debug(
                msg = output.section_close( text = "No knowledge sources can act" )
                )
        
        self.output.debug(
            msg = output.section_close( text = "%s finished" % self.identifier() )
            )
        
        return action
    
    
    @property
    def initials(self):
        
        return self.board.read( type = stage.Initial )
    
    
    @property
    def ensembles(self):
        
        return self.board.read( type = stage.Ensemble )
    
    
    @property
    def rotations(self):
        
        return self.board.read( type = stage.Rotation )
    
    
    @property
    def translations(self):
        
        return self.board.read( type = stage.Translation )
        
        
    @property
    def packings(self):
        
        
        return self.board.read( type = stage.Pack )
        
        
    @property
    def clashings(self):
        
        return self.board.read( type = stage.Clash )
    
    
    @property
    def evaluateds(self):
        
        return self.board.read( type = stage.Evaluated )
    
    
    def finalize(self):
        
        self.output.info( msg = output.secondary_title( text = self.identifier() ) )
        self.show_search_tree()
        self.solutions = []
        self.postprocessings = []
        
        if self.refineds:
            self.output.info(
                msg = output.subtitle( text = "Post-refinement selection" )
                )
            self.refineds.sort( key = lambda s: s.score(), reverse = True )
            best = self.refineds[0].score()
            self.output.info( msg = "Best refined score: %.3f" % best )
            improvement = abs( best - self.lowest_initial )
            self.output.info( msg = "Max score improvement: %.3f" % improvement )
            threshold = best - ( 1.0 - self.threshold_postrefinement ) * improvement
            self.output.info( msg = "Acceptance threshold: %.3f" % threshold )
            accepteds = [ st for st in self.refineds if threshold <= st.score() ]
            self.output.info( msg = "Accepted: %d" % len( accepteds ) )
            self.output.info( msg = output.blank() )
            
            for st in accepteds:
                self.output.info( msg = "Peak annotation:" )
                self.output.indent()
                self.output.info( msg = str( st ) )
                self.output.dedent()
                
            self.output.info( msg = output.blank() )
            
            postprocessings = [
                st for st in self.refineds
                if ( st.structure.significant and st not in accepteds )
                ]
                
            self.output.info(
                msg = output.underlined(
                    text = "Further considered for postprocessing: %d" % len( postprocessings )
                    )
                )
                
            for st in postprocessings:
                self.output.info( msg = "Peak annotation:" )
                self.output.indent()
                self.output.info( msg = str( st ) )
                self.output.dedent()
                
            self.output.info( msg = output.blank() )
            self.output.info(
                msg = output.subtitle( text = "Find duplicates" )
                )
            
            self.output.info( msg = "Identify symmetry equivalent peaks" )
            
            self.output.info( msg = output.underlined( text = "Selected" ) )
                
            self.search_for_uniqueness(
                stages = accepteds,
                container = self.solutions,
                )
            
            self.output.info(
                msg = output.underlined( text = "Postprocessing only" )
                )
            
            self.search_for_uniqueness(
                stages = postprocessings,
                container = self.postprocessings,
                )
            
        # Run post-processing
        self.output.verbose(
            msg = output.secondary_title( text = "Prepare for next extension" )
            )
        
        for ppd in self.case.data.read( type = mr_object.PostProcessingData ):
            self.case.data.delete( obj = ppd )
            
        peaks = []
        
        for pp in self.postprocessors:
            self.output.debug( msg = "Next: %s" % pp )
            results = pp(
                board = self.board,
                case = self.case,
                context = self,
                solutions = self.solutions,
                postprocessings = self.postprocessings
                ) 
            peaks.extend(
                [ obj for obj in results if isinstance( obj, mr_object.PotentialPeak ) ]
                )
            nps = [
                obj for obj in results if not isinstance( obj, mr_object.PotentialPeak )
                ]
            
            for np in nps:
                self.case.data.write( obj = np )
            
        self.output.verbose(
            msg = output.secondary_title( text = "Consolidating filled peaks" )
            )
        defined_origin_peaks_relation = mr_object.EquivalenceRelation(
            relation = mr_object.PotentialPeakFixedOriginEquivalence( case = self.case )
            )
        undefined_origin_peaks_relation = mr_object.EquivalenceRelation(
            relation = mr_object.PotentialPeakVariableOriginEquivalence( case = self.case )
            )
        
        for p in peaks:
            if p.is_defined_origin():
                defined_origin_peaks_relation.add( value = p )
                
            else:
                undefined_origin_peaks_relation.add( value = p )
                
        self.output.verbose( msg = "Processing peaks with defined origin:" )
        self.write_best_peaks_from_equivalence_clusters(
            relation = defined_origin_peaks_relation
            )
        self.output.verbose( msg = "Processing peaks with undefined origin:" )
        self.write_best_peaks_from_equivalence_clusters(
            relation = undefined_origin_peaks_relation
            )
            
        self.output.verbose(
            msg = output.section_close( text = "%s finished" % self.identifier() )
            )
        
        
    def write_best_peaks_from_equivalence_clusters(self, relation):
        
        for ( index, cluster ) in enumerate( relation.classes(), start = 1 ):
            self.output.verbose(
                msg = "Cluster %s: %s elements" % ( index, len( cluster ) )
                )
            best = max( cluster, key = lambda v: len( v.associations() ) )
            self.case.data.write( obj = best )

        
    def search_for_uniqueness(self, stages, container):
        
        structure_for = dict(
            [
                (
                    tuple(
                        self.case.create_matching_origin_search_structure(
                            structure = s,
                            cached = False,
                            )
                        ),
                    s,
                    )
                for s in self.case.partials
                ]
            )
        
        from phaser import matching
        
        for st in stages:
            self.output.info(
                msg = "Peak: score %.3f%s" % (
                    st.score(),
                    " (significant)" if st.structure.significant else "",
                    )
                )
            self.output.indent()
            self.output.info( msg = str( st.structure ) )
            self.output.dedent()
            
            stm = self.case.create_matching_origin_search_structure(
                structure = st.structure,
                cached = True,
                template_equivalence = self.context.template_equivalence
                )
            
            equivgen = (
                structure_for[ o ] for o in structure_for.keys()
                if matching.phaser_style_equivalence_match( left = stm, right = o )
                )
            
            try:
                equiv = equivgen.next()
                self.output.info( msg = "Equivalent:" )
                self.output.indent()
                self.output.info( msg = equiv )
                self.output.info( msg = output.blank() )
                self.output.dedent()
                continue
                
            except StopIteration:
                self.output.debug( "UNIQUE" )
                self.output.info( msg = output.blank() )
                self.case.insert( structure = st.structure, score = st.score() )
                structure_for[ tuple( stm ) ] = st.structure
                container.append( st )
                
        self.output.info( msg = output.blank() )
        
        
    def threshold_and_start_refinements(self):
        
        evaluateds = self.evaluateds
        self.refineds = []
        
        if not self.evaluateds:
            return
        
        self.output.info(
            msg = output.subtitle( text = "Space group: %s" % self.case.space_group_symbol() )
            )
        evaluateds.sort( key = lambda s: s.score(), reverse = True )
        best = evaluateds[0].score()
        improvement = abs( best - self.lowest_initial )
        threshold = best - ( 1.0 - self.threshold_refinement ) * improvement
        self.output.info( msg = "Selection threshold: %.3f" % threshold )
        self.output.info( msg = "" )
        
        selecteds = [
            st for st in evaluateds if st.significant() or threshold <= st.score()
            ]
        self.output.info( msg = "Available candidates: %d" % len( selecteds ) )
        self.output.info( msg = "" )
        results = []
        
        for st in evaluateds:
            self.output.info( msg = "Peak annotation:" )
            self.output.indent()
            self.output.info( msg = str( st ) )
            self.output.dedent()
            
            if st.significant():
                reason = "significant"
                
            elif threshold <= st.score():
                reason = "above threshold"
                
            else:
                self.output.info( msg = "REJECTED: below threshold" )
                self.output.info( msg = "" )
                continue
            
            self.output.info( msg = "ACCEPTED: %s" % reason )
            
            if not st.partial().peaks and st.is_refined():
                self.output.info( msg = "There is only one peak and it has been refined" )
                self.output.info( msg = "Skipping refinement" )
                tf_peak = st.tf_peak()
                self.refineds.append(
                    stage.Refined(
                        evaluated = st,
                        structure = mr_object.Structure(
                            peaks = [ tf_peak.as_peak() ],
                            significant = st.significant(),
                            rfz = st.rfz(),
                            tfz = tf_peak.tfz,
                            predecessor = st.partial(),
                            ),
                        llg = st.llg
                        )
                    )
                self.output.info( msg = "" )
                continue
            
            self.output.info( msg = "Refinement job submitted" )
            calc = calculation.Refinement(
                data = self.case.calculation_data(),
                partial = st.partial(),
                peak = st.peak(),
                significant = st.significant(),
                rfz = st.rfz(),
                tfz = st.tf_peak().tfz,
                b_factor_refinement = self.b_factor_refinement
                )
            self.queue.submit( owner = self, calculation = calc, extra = st )
            self.output.info( msg = "" )
        
        self.output.info(
            msg = output.section_close( text = "Refinement jobs started" )
            )
    
    
    def evaluate_refinements(self):
        
        self.queue.poll()
        
        for job in self.queue.completed():
            self.queue.remove( job = job )
            r = job.calculation.process( results = job.result )
            ( llg, structure ) = r.data
            self.refineds.append(
                stage.Refined(
                    evaluated = job.extra,
                    structure = structure,
                    llg = llg
                    )
                )
                
    
    def __str__(self):
        
        return "ExtensionProgress"
        
        
class SolutionProgress(object):
    """
    Data about the solution process
    """
    
    def __init__(
        self,
        cases,
        initials,
        queue,
        output,
        superposition,
        template_equivalence,
        identity_to_rms,
        ):
        
        assert len( cases ) == len( initials )
        self.cases = cases
        self.initials = initials
        self.history = []
        self.output = output
        self.queue = queue
        self.superposition = superposition
        self.template_equivalence = template_equivalence
        self.identity_to_rms = identity_to_rms
        
        
    def results(self):
        
        array = self.history[-1] if self.history else self.initials
        assert len( array ) == len( self.cases )
        return zip( self.cases, array )
        
        
    def solve(self, plan):
    
        self.output.info( msg = output.title( text = "Solution process" ) )
        self.output.notify(
                message = "status",
                data = "Start solution process"
                )
        self.output.info( msg = plan )
        
        while True:
            actives = [ ( case, initial ) for ( case, initial )
                in zip( self.cases, self.initials ) if initial is not None ]
            
            if not actives:
                break
            
            self.history.append( self.initials[:] )
            
            assert all( 0 < len( initial ) for ( case, initial ) in actives )
            self.output.info( msg = output.title( text = "Search coverage" ) )
            self.output.notify(
                message = "status",
                data = "Determine search space group coverage"
                )
            
            extensions = []
            
            for ( index, ( case, initial ) ) in enumerate( actives, start = 1 ):
                self.output.info(
                    msg = output.subtitle(
                        text = "Space group: %s" % case.space_group_symbol()
                        )
                    )
                self.output.info( msg = "Partial structures:" )
                self.output.info( msg = output.blank() )
                
                for ( index, structure ) in enumerate( initial, start = 1 ):
                    self.output.info(
                        "%d: score %.2f" % ( index, case.score_for[ structure ] )
                        )
                    self.output.indent()
                    self.output.info( str( structure ) )
                    self.output.dedent()
                    self.output.info( msg = output.blank() )
                    
                extensions.append(
                    plan.progress( case = case, initial = initial, context = self )
                    )
                
            self.output.info( msg = output.title( text = "Extension stage" ) )
            self.output.notify(
                message = "status",
                data = "Find possible solutions"
                )
            
            while True:
                acted = [ ext.iterate() for ext in extensions ]
                
                if not any( acted ):
                    if self.queue.empty():
                        self.output.debug( msg = "Process finished" )
                        break
                    
                    else:
                        self.output.debug( msg = "Waiting for jobs to finish" )
                        self.queue.wait()
                        
                self.queue.poll()
                        
            self.output.info( msg = output.title( text = "Refinement stage" ) )
            self.output.notify(
                message = "status",
                data = "Refinement"
                )
                
            for ext in extensions:
                ext.threshold_and_start_refinements()
                
            self.queue.join()
            
            self.output.info( msg = output.title( text = "Round evaluation stage" ) )
            self.output.notify(
                message = "status",
                data = "Result evaluation"
                )
            
            for ext in extensions:
                ext.evaluate_refinements()
                ext.finalize()
                
            self.output.info(
                msg = output.title( text = "Search progress evaluation stage" )
                )
            self.output.notify(
                message = "status",
                data = "Search progress evaluation"
                )
                
            exploreds = []
            unexploreds = []
                    
            for ext in extensions:
                self.output.info( msg = output.subtitle( text = ext.identifier() ) )
                    
                assert ext.initials
                
                if not ext.ensembles:
                    self.output.info(
                        msg = "No structures can be completed any further with current models"
                        )
                    exploreds.append( ext )
                    continue
                
                if not ext.packings:
                    self.output.info( msg = "All peaks clash" )
                    exploreds.append( ext )
                    continue
                
                assert ext.evaluateds
                assert ext.refineds
                
                if not ext.solutions:
                    self.output.info( msg = "No novel peaks available" )
                    exploreds.append( ext )
                    continue
                
                self.output.info( msg = "Progress to next extension" )
                unexploreds.append( ext )
                
            if 1 < len( extensions ) and unexploreds:
                self.output.info( msg = output.title( text = "Space group comparison" ) )
                self.output.notify(
                message = "status",
                data = "Space group comparison"
                )
                
                max_scores = [
                    max( [ ext.case.score_for[ s.structure ] for s in ext.solutions ] )
                    for ext in unexploreds
                    ]
                
                best = max( max_scores )
                self.output.info( msg = "Best solution score: %.3f" % best )
                threshold = (
                    best - ( 1.0 - plan.threshold_space_group ) * abs( best )
                    )
                self.output.info(
                    msg = "Space group acceptance threshold: %.3f" % threshold
                    )
                
                accepteds = []
                    
                for ( score, ext ) in zip( max_scores, unexploreds ):    
                    if any( s.structure.significant for s in ext.solutions ):
                        accepteds.append( ext )
                        self.output.info(
                            msg = "%s: ACCEPTED (significant solutions found)" % ext.identifier()
                            )
                        
                    elif threshold <= score:
                        accepteds.append( ext )
                        self.output.info(
                            msg = "%s: ACCEPTED (best solution above threshold)" % ext.identifier()
                            )
                        
                    else:
                        self.output.info( msg = "%s: REJECTED" % ext.identifier() )
                        exploreds.append( ext )
            
            else:
                accepteds = extensions
                
            for ext in accepteds:
                assert ext.case in self.cases
                self.initials[ self.cases.index( ext.case ) ] = [
                    r.structure for r in ext.solutions
                    ]
                
            if exploreds:
                self.output.info(
                    msg = output.title( text = "Space group exploration" )
                    )
                self.output.info( msg = "Search finished this round in:" )
                self.output.indent()
                
                for ext in exploreds:
                    self.output.info( msg = ext.identifier() )
                    assert ext.case in self.cases
                    self.initials[ self.cases.index( ext.case ) ] = None  
                    
                self.output.dedent()
    
    
    def __str__(self):
        
        return "Solution progress"
    
    
    @classmethod
    def Dataset(
        cls,
        composition,
        problem,
        queue,
        output,
        superposition,
        template_equivalence,
        identity_to_rms,
        ):
        
        case = mr_object.Case(
            composition = composition,
            space_group_hall = problem.space_group_hall,
            problem = problem
            )
        return cls(
            cases = [ case ],
            initials = [ [ case.problem.root ] ],
            queue = queue,
            output = output,
            superposition = superposition,
            template_equivalence = template_equivalence,
            identity_to_rms = identity_to_rms,
            )
    
    
    @classmethod
    def Enantiomorph(
        cls,
        composition,
        problem,
        queue,
        output,
        superposition,
        template_equivalence,
        identity_to_rms,
        ):
        
        case = mr_object.Case(
            composition = composition,
            space_group_hall = problem.space_group_hall,
            problem = problem
            )
        
        cases = [ case ]
        
        if case.space_group_type().is_enantiomorphic():
            enantiomorph = case.space_group_info().change_hand()
            cases.append(
                mr_object.Case(
                    composition = composition,
                    space_group_hall = enantiomorph.type().hall_symbol(),
                    problem = problem
                    )
                )
        
        return cls(
            cases = cases,
            initials = [ [ c.problem.root ] for c in cases ],
            queue = queue,
            output = output,
            superposition = superposition,
            template_equivalence = template_equivalence,
            identity_to_rms = identity_to_rms,
            )
    
    
    @classmethod
    def Pointgroup(
        cls,
        composition,
        problem,
        queue,
        output,
        superposition,
        template_equivalence,
        identity_to_rms,
        ):
        
        from cctbx import sgtbx
        sgi = sgtbx.space_group( problem.space_group_hall ).info()
        cases = []
        
        for gr in sgi.reflection_intensity_equivalent_groups():
            cases.append(
                mr_object.Case(
                    composition = composition,
                    space_group_hall = gr.type().hall_symbol(),
                    problem = problem
                    )
                )
        
        return cls(
            cases = cases,
            initials = [ [ c.problem.root ] for c in cases ],
            queue = queue,
            output = output,
            superposition = superposition,
            template_equivalence = template_equivalence,
            identity_to_rms = identity_to_rms,
            )
        
        
class SolutionSelection(object):
    """
    Final selection for solutions
    """
    
    def __init__(self, case, threshold, template_equivalence):
    
        self.case = case
        self.best_score = max(
            [ sc for ( st, sc ) in case.score_for.items()
                if st != case.problem.root ]
            )
        self.threshold_score = (
            self.best_score  - ( 1.0 - threshold ) * abs( self.best_score )
            )
        
        self.thresholdeds = [
            p for p in case.partials
            if ( p != case.problem.root
                and self.threshold_score <= case.score_for[ p ] )
            ]
        self.thresholdeds.sort(
            key = lambda s: case.score_for[ s ],
            reverse = True
            )
        self.solutions = []
        solutions = []
        
        from phaser import matching
        
        for structure in self.thresholdeds:
            trial = case.create_matching_origin_search_structure(
                structure = structure,
                cached = True,
                template_equivalence = template_equivalence
                )
            
            for found in solutions:
                if len( found ) <= len( trial ):
                    continue
                
                pairs = (
                    matching.pairs(
                        overlaps = matching.origin_shifted_overlap( origin = o )
                        )
                    for o in matching.origin_shifts_between(left = trial, right = found )
                    )
            
                if any( len( list( p ) ) == len( trial ) for p in pairs ):
                    break
                
            else:
                self.solutions.append( structure )
                solutions.append( trial )


class StageTreeTable(object):
    """
    A table to represent the tree of stages
    """
    
    def __init__(self):
        
        self.children_for = { None: [] }
        
        
    def add(self, child):
        
        parent = child.parent()
        assert parent in self.children_for, "parent %s of %s not in %s" % ( repr( parent ), repr( child ), self.children_for )
        assert child not in self.children_for
        self.children_for[ parent ].append( child )
        self.children_for[ child ] = []
        
        
    def as_table(self):
        
        cells = {}
        row_count = self.insert_subtree( root = None, index = 0, cells = cells )
        column_count = max( [ k[1] for k in cells ] ) + 1
        
        table = [ [ None ] * column_count for rc in range( row_count ) ]
        
        for ( ( r, c ), e ) in cells.items():
            try:
                table[r][c] = e
                
            except IndexError:
                raise RuntimeError, "Out-of-bounds: ( %d, %d ), limits: ( %d, %d )" % (
                    r, c, row_count, column_count
                    )
            
        return table
    
    
    def format(self):
        
        estages = [ s for s in self.children_for if isinstance( s, stage.Ensemble ) ]
        
        if estages:
            max_char_count = max(
                [ len( str( es.ensemble ) ) for es in estages ]
                )
            ensemble = "Ensemble".rjust( max_char_count )
            
        else:
            ensemble = "Ensemble"
        captions = [ "Partial", ensemble, "RF-score", "TF-score", "Pack",
            "   Z-score", "Final LLG" ]
        widths = [ len( c ) for c in captions ]
        lines = []
        
        for l in self.as_table():
            restricted = self.restrict( iterable = l )
            fill = len( captions ) - len( restricted )
            assert 0 <= fill
            
            lines.append(
                [ c.entry( width = w ) if c else "" for ( c, w ) in zip( restricted, widths ) ]
                + [ "n/a" ] * fill
                )
            
        from phaser import tbx_utils
        return tbx_utils.format_table(
            captions = captions,
            formats = [ "%s" ] * len( captions ),
            lines = lines
            )
        
        
    def insert_subtree(self, root, index, cells):
        
        if root is not None:
            cells[ ( index, root.depth() ) ] = root
        
        if self.children_for[ root ]:
            for c in self.children_for[ root ]:
                index = self.insert_subtree( root = c, index = index, cells = cells )
            
        else:
            index += 1
            
        return index
    
    
    @staticmethod
    def restrict(iterable):
        
        for v in range( len( iterable ) - 1, -1, -1 ):
            if iterable[ v ] is not None:
                return iterable[ : v + 1 ]
            
        return []

