def solve(self, decider=None, initial_state=None):
    
    start_time = datetime.now()
   
    solved_count = 0
    best_solved_count = 0
    
    dead_end_count = 0
    
    max_child_count = 64
    
    # INIT ROOT/ZERO NODE
    current_node = Node(id=Node.next_id(), parent_id=None, state=initial_state.clone(), recursion_depth=0, decider=decider, max_child_count=max_child_count)
    current_node.upsert()

    # LOG INITITAL STATE
    logging.info(datetime.now())
    logging.info('root node {%i} instantiated.' % current_node.id)
    logging.info('with initial state = ')
    logging.info(str(initial_state))
    
    target_depth = initial_state.target_depth()
    
    # CREATE FIRST GENERATION OF CHILDREN
    logging.info('creating child states...')
    current_node.create_children()
    logging.info('%i created.' % current_node.child_count)
    
    decision_path = []
    
    exit_criteria = {
      'timed_out' : False,
      'request_cancel' : False,
      'is_solved' : False,
      'solution_space_exhausted' : False,
      }
      
    rotator_counter = 0
    rotator_interval = 100000
    
    exhaust_solution_space = True
    
    skip_solution = False
    while (True not in exit_criteria.values()):
      
      current_time = datetime.now()
      total_elapsed_time = current_time - start_time
      total_elapsed_seconds = total_elapsed_time.total_seconds()
      approx_total_elapsed_minutes = total_elapsed_seconds / 60
      approx_total_elapsed_hours = total_elapsed_seconds / (60 * 60)
      approx_total_elapsed_days = total_elapsed_seconds / (60 * 60 * 24)
    
      # INDICATE PROGRESS
      rotator_counter = rotator_counter + 1
      if (rotator_counter % rotator_interval == 0):
        print('=-'*10)
        print('started @ %s, now @ %s]' % (start_time, current_time))
        print('%i seconds ~ %i minutes ~ %i hours ~ %i days' %
              (total_elapsed_seconds, approx_total_elapsed_minutes, approx_total_elapsed_hours, approx_total_elapsed_days))
        print('node %i - current {%i}/{%i} vs. best {%i}/{%i}' % (current_node.id, solved_count, target_depth, best_solved_count, target_depth))
        print('total solution count - %i' % len(self.solution_nodes))
        print('tree node count - %i' % len(Node.tree.keys()))
        print('dead-end count - %i' % dead_end_count)
        phys_usage = psutil.phymem_usage() 
        print('%d %% of physical memory used' % phys_usage.percent)
        virt_usage = psutil.virtmem_usage() 
        print('%d %% of virtual memory used' % virt_usage.percent)
      
      # SOLVED
      if (decider.solution_status(current_node.state) == True) and (skip_solution == False):
        exit_criteria['is_solved'] = True
        current_node.is_solution = True
        self.solution_nodes.append(current_node)
        
        logging.info('node {%i} solves.' % current_node.id)
        logging.info('state:')
        logging.info(str(current_node.state))        
        
        print('solved')
        print(str(current_node.state))
        
        if (exhaust_solution_space == True):
          exit_criteria['is_solved'] = False
          skip_solution = True
      
      # CONTINUE [current_node is not solution]
      else:
        skip_solution = False
        logging.info('solution not yet reached.')
        
        # NEED TO GENERATE CHILDREN FOR NODE
        if (current_node.next_child_idx == None):
          logging.info('need to generate children for node {%i} ...' % current_node.id)
          current_node.create_children()
          if (current_node.child_count == 0):
            dead_end_count = dead_end_count + 1 
          
          logging.info('done.  %i children created.' % current_node.child_count)
        
        # MOVE FORWARD [IF THE CURRENT NODE HAS AN UNEVALUATED CHILD, THEN MOVE TO IT]
        if (current_node.next_child_idx != -1):
          logging.info('current node {%i} has an unevaluated child' % current_node.id)
          
          # get next child
          next_node_id = current_node.child_ids[current_node.next_child_idx]
          next_node = current_node.get(next_node_id)
          logging.info('moved fwd to unevaluated child node {%i} of parent node {%i} [child no. %i of %i]' % (next_node.id, current_node.id, current_node.next_child_idx + 1, current_node.child_count))

          # increment next child idx
          if (current_node.next_child_idx < current_node.child_count - 1):
            current_node.next_child_idx = current_node.next_child_idx + 1
          # ALL CHILDREN EXHAUSTED
          else:
            current_node.next_child_idx = -1 
          
          current_node.upsert()
          
          # NOTE MOVE FWD
          solved_count = solved_count + 1
          if (solved_count > best_solved_count):
            best_solved_count = solved_count
          logging.info("current completeness = {%i}/{%i}" % (solved_count, target_depth))
          logging.info("vs. best completeness = {%i}/{%i}" % (best_solved_count, target_depth))

          change_loc = current_node.state.locate_fwd_change(next_node.state)
          logging.info('change loc %s' % str(change_loc))

          decision_path.append(change_loc)
          logging.info('updated decision path')
          logging.info(decision_path)
          
          logging.info('new state')
          logging.info(next_node.state)
          
          # update completeness
          # current_completeness = solved_count / target_count * 100)
          # best_completeness = best_solved_count / target_count * 100)

          # increment pointer to next unevaluated remaining child (dec ## of remaining uneval children)          

          current_node = next_node

        # current_node.has_available_children() == False
        else:       
          logging.info("no unevaluated children, i.e. dead-end reached.")

          # MOVE BACKWARDS
          # if current node has a parent, and thus it is actually possible to move backwards
          if (current_node.parent_id != None):

            # retrieve parent node
            next_node = Node.tree[current_node.parent_id]
            logging.info('moving back to parent node {%i}' % next_node.id)
            
            # locate change
            change_loc = current_node.state.locate_bwd_change(next_node.state)

            solved_count = solved_count - 1
            decision_path.pop()
            
            logging.info("current completeness = {%i}/{%i}" % (solved_count, target_depth))
            logging.info("vs. best completeness = {%i}/{%i}" % (best_solved_count, target_depth))
            
            node_to_discard = current_node
            current_node = next_node
            Node.purge_node(node_to_discard)

          ## CanMoveBackwards() == False
          else:             
            exit_criteria['solution_space_exhausted'] = True
            
            logging.info('node i% has no parent.' % current_node.id)
            logging.info('solution space exhausted.')
       
    for exit_criterion in exit_criteria.keys():
      logging.info('%s - %s' % (exit_criterion, exit_criteria[exit_criterion]))  
    
    return exit_criteria
  def solve(self, decider=None, initial_state=None, max_child_count=None):

    n = 5
    initial_state = State(n)
    decision_path = []
    
    seed = datetime.now().microsecond
    logging.info('seed for random number generator - %i' % seed)
    
    exit_criteria = {
      'timed_out' : False,
      'request_cancel' : False,
      'is_solved' : False,
      'solution_space_exhausted' : False,
      }
    
    decider = Decider(random_seed=seed) 
    
    zero_node = Node(id=Node.next_id(), parent_id=None, state=initial_state, recursion_depth=0, decider=decider, max_child_count=max_child_count)
    zero_node.upsert()
    zero_node.create_children()
    zero_node.upsert()
    
    first_generation_count = zero_node.child_count
    
    total_processor_count = psutil.cpu_count()
    max_count = total_processor_count - 1 # use all CPUs, with one reserved for us
    #max_count = 1
    
    active_procs = {}
    terminated_proc_ids = []
    remaining_count = first_generation_count
    procs_instructed_to_terminate = []
    
    cancel = False    
    
    start_time = time.time()
    time_limit = None
    timed_limit_exceeded = False
    
    update_period = 0.5 # second
    
    solutions = []
    solution_counts = {}
    
    solution_count_time_evolution = {}
    
    next_proc_id = 1
    while ((cancel == True) and (len(active_procs.keys()) > 0)) or ((cancel == False) and ((remaining_count > 0) or (len(active_procs.keys()) > 0))):      
      
      if (time_limit != None):
        if (time.time() - start_time > time_limit):
          timed_limit_exceeded = True
      
      if (timed_limit_exceeded == True):
        cancel = True
        
      '''# ISSUE TERMINATION INSTRUCTIONS'''
      if (cancel == True):
        for proc_id in active_procs.keys():        
          if (proc_id not in procs_instructed_to_terminate):
            connxn = active_procs[proc_id]
            term_msg = TerminationMessage(proc_id=0)
            connxn.send(term_msg)
            print('proc %i instructed to terminate' % proc_id)
            procs_instructed_to_terminate.append(proc_id)
      
      new_solutions_arrived = False
      
      '''# HANDLE INCOMING MESSAGES'''
      for proc_id in active_procs.keys():        
        connxn = active_procs[proc_id]        
        
        while (connxn.poll() == True):
          msg = connxn.recv()
          
          '''# SOLUTION MSG'''
          if (isinstance(msg, SolutionMessage)):
            new_solutions_arrived = True
            solutions.append(msg.solution)
            if (msg.proc_id in solution_counts.keys()):
              solution_counts[msg.proc_id] = solution_counts[msg.proc_id] + 1
            else:
              solution_counts[msg.proc_id] = 1
          
          '''# STATUS MSG'''          
          
          '''# TERMINATION MSG'''
          if (isinstance(msg, TerminationMessage)):
            print('TerminationNotice received from process %i' % msg.proc_id)
            active_procs.pop(msg.proc_id)
            terminated_proc_ids.append(msg.proc_id)
      
      '''# SPAWN NEW PROCESS IF NOT YET DONE'''
      if ((cancel == False) and (len(active_procs.keys()) < max_count) and (remaining_count > 0)):
        
        print(zero_node.next_child_idx)
        
        '''# GET NEXT UNUSED STATE, IF ANY '''
        if (zero_node.next_child_idx != -1):          
          
          node_idx = zero_node.child_ids[zero_node.next_child_idx]
          node = Node.tree[node_idx]
          
          '''# ADJUST next_child_idx '''
          if (zero_node.next_child_idx < zero_node.child_count - 1):
            zero_node.next_child_idx = zero_node.next_child_idx + 1
          else:
            zero_node.next_child_idx = -1
          
          zero_node.upsert()
                  
          proc_id = next_proc_id
          next_proc_id = next_proc_id + 1
          
          (parent_connxn, child_connxn) = Pipe()
          active_procs[proc_id] = parent_connxn
          
          proc = Process(target=tracker_process, args=(proc_id, node, child_connxn))
          proc.start()          
          
          remaining_count = remaining_count - 1
      
      if (new_solutions_arrived):
        cls()
        counts = [('%i - %i' % (pid, solution_counts[pid])) for pid in solution_counts.keys()]
        print(str(len(solutions)) + ' : ' + (','.join(counts)) )
        print('active - ' + str(active_procs.keys()))
        print('completed' + str(terminated_proc_ids))        
        
        d = {}
        d[0] = [len(solutions)]
        for pid in solution_counts.keys():
          d[pid] = solution_counts[pid]
        solution_count_time_evolution[datetime.now()] = d
  
      time.sleep(update_period)
    
    print('all done')
    
    print('writing solutions to disk')
    f = open('solutions.txt', 'w')    
    for state in solutions:
      f.write('\n')
      f.write(str(state))
    f.close()   
    
    print('writing solution count time-evolution to disk')
    
    sorted_keys = sorted([x for x in solution_counts.keys()])
    f = open('solution_count_time_evolution.csv', 'w')
    for key in sorted_keys:
      f.write(str(key) + ',' + ','.join([str(x) for x in solution_count_time_evolution[key]]) + '\n')
    
      counts = solution_count_time_evolution[key]
      s = ''
      for i in range(first_generation_count):
        n = i + 1        
        if (n not in counts.keys()):
          s = s + '0' + ','
        else:           
          s = s + str(counts[n]) + ','
    
      f.write(s + '\n')
    
    f.close()   
  def solve(self, decider=None, initial_state=None):
   
    solved_count = 0
    best_solved_count = 0
    
    max_child_count = 64
    
    # init root/zero node
    current_node = Node(id=Node.next_id(), parent_id=None, state=initial_state.clone(), recursion_depth=0, decider=decider, max_child_count=max_child_count)
    current_node.upsert()
    logging.info(datetime.now())
    logging.info('root node {%i} instantiated.' % current_node.id)
    logging.info('with initial state = ')
    logging.info(str(initial_state))
    
    target_depth = initial_state.target_depth()
    # log initial state

    logging.info('creating child states...')
    current_node.create_children()
    logging.info('%i created.' % current_node.child_count)
    
    decision_path = []
    
    exit_criteria = {
      'timed_out' : False,
      'request_cancel' : False,
      'is_solved' : False,
      'solution_space_exhausted' : False,
      }
      
    rotator_counter = 0
    rotator_interval = 10000
    
    exhaust_solution_space = True
    
    skip_solution = False
    while (True not in exit_criteria.values()):
    
      # INDICATE PROGRESS
      rotator_counter = rotator_counter + 1
      if (rotator_counter % rotator_interval == 0):
        print('node %i - current {%i}/{%i} vs. best {%i}/{%i}' % (current_node.id, solved_count, target_depth, best_solved_count, target_depth))
      
      # SOLVED
      if (decider.solution_status(current_node.state) == True) and (skip_solution == False):
        exit_criteria['is_solved'] = True
        current_node.is_solution = True
        Node.tree[current_node.id] = current_node
        
        logging.info('node {%i} solves.' % current_node.id)
        logging.info('state:')
        logging.info(str(current_node.state))        
        
        print('solved')
        print(str(current_node.state))
        
        if (exhaust_solution_space == True):
          exit_criteria['is_solved'] = False
          skip_solution = True
      
      # CONTINUE
      else:
        skip_solution = False
        logging.info('solution not yet reached.')
        
        # NEED TO GENERATE CHILDREN FOR NODE
        if (current_node.next_child_idx == None):
          logging.info('need to generate children for node {%i} ...' % current_node.id)
          current_node.create_children()
          logging.info('done.  %i children created.' % current_node.child_count)
        
        # IF THE CURRENT NODE HAS AN UNEVALUATED CHILD, THEN MOVE TO IT
        if (current_node.next_child_idx != -1):
          logging.info('current node {%i} has an unevaluated child' % current_node.id)
          
          # get next child
          next_node_id = current_node.child_ids[current_node.next_child_idx]
          next_node = current_node.get(next_node_id)
          logging.info('moved fwd to unevaluated child node {%i} of parent node {%i} [child no. %i of %i]' % (next_node.id, current_node.id, current_node.next_child_idx + 1, current_node.child_count))

          # increment next child idx
          if (current_node.next_child_idx < current_node.child_count - 1):
            current_node.next_child_idx = current_node.next_child_idx + 1
          # ALL CHILDREN EXHAUSTED
          else:
            current_node.next_child_idx = -1 
          
          current_node.upsert()
          
          # NOTE MOVE FWD
          solved_count = solved_count + 1
          if (solved_count > best_solved_count):
            best_solved_count = solved_count
          logging.info("current completeness = {%i}/{%i}" % (solved_count, target_depth))
          logging.info("vs. best completeness = {%i}/{%i}" % (best_solved_count, target_depth))

          change_loc = current_node.state.locate_fwd_change(next_node.state)
          logging.info('change loc %s' % str(change_loc))

          decision_path.append(change_loc)
          logging.info('updated decision path')
          logging.info(decision_path)
          
          logging.info('new state')
          logging.info(next_node.state)
          
          # update completeness
          # current_completeness = solved_count / target_count * 100)
          # best_completeness = best_solved_count / target_count * 100)

          # increment pointer to next unevaluated remaining child (dec ## of remaining uneval children)          

          current_node = next_node

        # current_node.has_available_children() == False
        else:       
          logging.info("no unevaluated children, i.e. dead-end reached.")

          # move backwards
          # if current node has a parent, and thus it is actually possible to move backwards
          if (current_node.parent_id != None):

            # retrieve parent node
            next_node = Node.tree[current_node.parent_id]
            logging.info('moving back to parent node {%i}' % next_node.id)
            
            # locate change
            change_loc = current_node.state.locate_bwd_change(next_node.state)

            solved_count = solved_count - 1
            decision_path.pop()
            
            logging.info("current completeness = {%i}/{%i}" % (solved_count, target_depth))
            logging.info("vs. best completeness = {%i}/{%i}" % (best_solved_count, target_depth))
            
            current_node = next_node

          ## CanMoveBackwards() == False
          else:             
            exit_criteria['solution_space_exhausted'] = True
            
            logging.info('node {i%} has no parent.' % current_node.id)
            logging.info('solution space exhausted.')
       
    for exit_criterion in exit_criteria.keys():
      logging.info('%s - %s' % (exit_criterion, exit_criteria[exit_criterion]))  
    
    return exit_criteria