def __init__(self, network, input_struct=None):
     
     # Deep-copying keras models is very expensive. We avoid this by specifying that network 
     # attribute should always be shallow copied.
     self._network = ShallowCopyProxy(network)
     
     if input_struct == None:
         input_struct = NestedList.generate_random_instance()
     self.input_struct = input_struct
     
     self.tokens = list(input_struct.get_tokens())
     
     self.output_struct = NestedListManager()
     self.execution_history = []
class FeedForwardProcess(object):
    
    def __init__(self, network, input_struct=None):
        
        # Deep-copying keras models is very expensive. We avoid this by specifying that network 
        # attribute should always be shallow copied.
        self._network = ShallowCopyProxy(network)
        
        if input_struct == None:
            input_struct = NestedList.generate_random_instance()
        self.input_struct = input_struct
        
        self.tokens = list(input_struct.get_tokens())
        
        self.output_struct = NestedListManager()
        self.execution_history = []
    
    @property
    def network(self):
        return self._network.referent
    
    def do_next_step(self):
        
        token = self.tokens[len(self.execution_history)]
        
        output_layer, action = self.network.feed_forward(token)
        
        step = FeedForwardStep(token, copy.deepcopy(self.output_struct), output_layer, action)
        self.execution_history.append(step)
        self.output_struct.do_action(action, token)
        
    def run_to_end(self):
        
        while len(self.execution_history) < len(self.tokens):
            self.do_next_step()
    
    def calc_total_score(self):
        
        set(self.input_struct.get_descendents())
        
        self.output_struct.root.get_descendents()
        
    
    def calc_score(self):
        # Wrap the input_struct in an additional NestedList because the NestedListManager for output_strcut 
        # automatically initializes input_struct.root to be a NestedList and does everything inside that root.
        return self.output_struct.root.calc_score(NestedList(self.input_struct))
    
    def fork(self, step_num):
        new_instance = copy.deepcopy(self)
        new_instance.output_struct = new_instance.execution_history[step_num].struct
        new_instance.execution_history = new_instance.execution_history[:step_num]
        return new_instance
    
    def get_random_fork(self):
        
        logging.log(logging.NOTSET, "Choosing at which step to fork")
        entropies = [step.entropy for step in self.execution_history]
        total_entropy = sum(entropies)
        probabilities = [e/total_entropy for e in entropies]
        step_index = np.random.choice(len(self.execution_history), p=probabilities)
        step = self.execution_history[step_index]
        
        logging.log(logging.NOTSET, "Choosing how to modify this step")
        probabilities = step.network_output
        probabilities[step.action] = 0
        probabilities = probabilities * (1/sum(probabilities))
        
        num_possible_actions = len(step.network_output)
        
        new_action = np.random.choice(num_possible_actions, p=probabilities)
        
        new_network_output = self.network.encode_action(new_action)
        
        logging.log(logging.NOTSET, "Forking")
        fork = self.fork(step_index)
        
        logging.log(logging.NOTSET, "Applying modification")
        new_step = FeedForwardStep(step.token, copy.deepcopy(step.struct), new_network_output, new_action)
        fork.execution_history.append(new_step)
        fork.output_struct = copy.deepcopy(step.struct)
        fork.output_struct.do_action(new_action, step.token)
        
        return fork, step_index