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):
   
    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