Example #1
0
def step():
    clicked_node = request.form['smiles']
    x = request.form['x']
    y = request.form['y']
    task_id = request.form['task_id']
    max_reactions = request.form['max_reactions']
    rbc_reaction_mode = request.form['rbc_reaction_mode']

    data = json.loads(current_app.redis.get(task_id))
    graph_dict = json.loads(data['graph_dict'])
    attr_dict = json.loads(data['attr_dict'])
    target_smiles = data['target_smiles']
    network_options = json.loads(data['network_options'])

    graph = nx.from_dict_of_lists(graph_dict, create_using=nx.DiGraph)
    network = Network(graph=graph,
                      target_smiles=target_smiles,
                      print_log=not current_app.config['PRODUCTION'])
    network.update_settings(network_options)
    network.add_attributes(attr_dict)
    network.update_settings({
        'max_reactions': int(max_reactions),
        'retrobiocat_reaction_mode': rbc_reaction_mode
    })

    new_substrate_nodes, new_reaction_nodes = network.add_step(clicked_node)

    all_new_nodes = [clicked_node] + new_substrate_nodes + new_reaction_nodes
    subgraph = network.graph.subgraph(all_new_nodes)

    nodes, edges = network.get_visjs_nodes_and_edges(graph=subgraph)

    for i, node in enumerate(nodes):
        nodes[i].update({'x': x, 'y': y})

    result = {'nodes': nodes, 'edges': edges}

    data['graph_dict'] = json.dumps(nx.to_dict_of_lists(network.graph))
    data['attr_dict'] = json.dumps(network.attributes_dict())
    nodes = add_new(data['nodes'], nodes)
    edges = add_new(data['edges'], edges)
    nodes, edges = delete_nodes_and_edges([], nodes, edges)
    data['nodes'] = nodes
    data['edges'] = edges

    current_app.redis.mset({task_id: json.dumps(data)})
    time_to_expire = 15 * 60  #15 mins * 60 seconds
    current_app.redis.expire(task_id, time_to_expire)

    return jsonify(result=result)
class BFS():
    def __init__(self,
                 network=None,
                 target=None,
                 max_pathways=50000,
                 max_pathway_length=5,
                 min_weight=1,
                 use_random=False,
                 print_log=False,
                 score_pathways=True,
                 allow_longer_pathways=False):
        """
        Best First Search object, for generating pathways from a network

        After initialising, run search using the .run() method

        Args:
            network: a network object which has been generated
            min_weight: the minimum weight to assign to zero complexity change (and Stop)
            max_pathways: the maximum number of pathways to generate before stopping
            use_random: set the bfs to use weighted random selection rather than always picking the best
        """
        self.score_pathways = score_pathways
        self.print_log = print_log
        self.min_weight = min_weight
        self.choices = {}
        self.max_pathways = max_pathways
        self.max_pathway_length = max_pathway_length
        self.allow_longer_pathways = allow_longer_pathways
        self.pathways = []
        self.use_random = use_random
        self.network = network
        self.generate_network = False
        if self.network == None:
            self.target = node_analysis.rdkit_smile(target, warning=True)
            self.generate_network = True
            self.network = Network(target_smiles=self.target,
                                   number_steps=self.max_pathway_length,
                                   print_log=False)
            self.network.generate(self.target, 0)
            self.log('BFS - will generate network')
        else:
            self.target = self.network.target_smiles

    def log(self, msg):
        if self.print_log == True:
            print(msg)

    def _get_context(self, nodes):
        """ Returns the pathway context, which is a string of node numbers"""
        list_node_numbers = []
        context = ''
        for node in nodes:
            list_node_numbers.append(
                self.network.graph.nodes[node]['attributes']['node_num'])

        sorted_node_numbers = sorted(list_node_numbers)
        for node_num in sorted_node_numbers:
            context += str(node_num)
            context += '-'
        return context

    def _expand_network(self, smi):
        nodes_added = []
        new_substrates, new_reactions = self.network.add_step(smi)
        nodes_added.extend(new_substrates)
        nodes_added.extend(new_reactions)
        return nodes_added

    def _get_choices(self, end_nodes):
        """ Returns a list of reaction nodes (and Stop) which are choices for the next step"""
        def get_choice_scores(choices):
            scores = [0]
            for node in choices[1:]:
                scores.append(self.network.graph.nodes[node]['attributes']
                              ['change_in_complexity'])
            return scores

        def get_weighted_scores(scores):
            # invert changes so decreases in complexity are favoured
            inverted_reaction_complexity_changes = [x * -1 for x in scores]

            min_change = min(inverted_reaction_complexity_changes)
            if min_change < 0:
                min_change = -min_change
            else:
                min_change = 0

            non_neg_changes = [
                x + self.min_weight + min_change
                for x in inverted_reaction_complexity_changes
            ]

            return non_neg_changes

        def get_choices(end_nodes, graph):
            successor_reactions = ['Stop']
            for node in end_nodes:
                successor_reactions.extend(list(graph.successors(node)))
            return successor_reactions

        def make_choice_dict(choices, scores):
            choice_dict = {}
            for i, choice in enumerate(choices):
                choice_dict[choice] = scores[i]
            return choice_dict

        choices = get_choices(end_nodes, self.network.graph)
        scores = get_choice_scores(choices)
        weighted_scores = get_weighted_scores(scores)
        choice_dict = make_choice_dict(choices, weighted_scores)

        return choice_dict

    def _pick_choice(self, context):
        """ Given a context, picks an option to extend (or stop) that pathway """
        def pick_best(choices, scores):
            sorted_options = node_analysis.sort_by_score(choices,
                                                         scores,
                                                         reverse=False)
            return sorted_options[0]

        def pick_weighted_random(choices, scores):
            return random.choices(choices, scores, k=1)[0]

        def get_lists_choices_scores(choices_dict):
            list_choices = []
            list_scores = []
            for choice in choices_dict:
                list_choices.append(choice)
                list_scores.append(choices_dict[choice])

            return list_choices, list_scores

        choices, scores = get_lists_choices_scores(self.choices[context])

        if self.use_random == False:
            option = pick_best(choices, scores)
        else:
            option = pick_weighted_random(choices, scores)

        return option

    def _add_reaction(self, reaction_choice):
        new_end_nodes = list(self.network.graph.successors(reaction_choice))
        added_nodes = [reaction_choice] + new_end_nodes

        return added_nodes, new_end_nodes

    def _check_pathway_has_end(self, nodes):
        pathway_subgraph = self.network.graph.subgraph(nodes)
        end_nodes = node_analysis.get_nodes_with_no_successors(
            pathway_subgraph)
        if len(end_nodes) == 0:
            return False
        return True

    def _make_pathway(self, nodes):
        """ Create pathway object from list of nodes"""
        return Pathway(nodes, self.network, calc_scores=self.score_pathways)

    def _check_if_should_expand_network(self, end_nodes, pathway_nodes):
        if self.generate_network == True:
            if self._num_reactions(pathway_nodes) < self.max_pathway_length:
                for node in end_nodes:
                    if len(list(self.network.graph.successors(node))) == 0:
                        self._expand_network(node)

    def _is_node_already_in_pathway(self, current_nodes, new_nodes):
        for node in new_nodes:
            if node in current_nodes:
                return True
        return False

    def _num_reactions(self, nodes):
        count = 0
        for node in nodes:
            if self.network.graph.nodes[node]['attributes'][
                    'node_type'] == 'reaction':
                count += 1
        return count

    def run(self):
        """
        Generate pathways using best first search

        Returns: list of pathways
        """
        self.log('Run BFS')
        self.pathways = []
        self.choices = {}

        nodes = [self.target]
        context = self._get_context(nodes)

        self._check_if_should_expand_network(nodes, nodes)
        self.choices[context] = self._get_choices(nodes)
        start_context = copy.deepcopy(context)

        while (len(self.pathways) < self.max_pathways) and (len(
                self.choices[start_context]) > 0):
            nodes = [self.network.target_smiles]
            context = self._get_context(nodes)
            steps = 0

            while len(self.choices[context]) > 0:
                if steps > self.max_pathway_length:
                    self.choices[context] = []
                    if self._check_pathway_has_end(nodes) == True:
                        self.pathways.append(nodes)
                    break

                best_choice = self._pick_choice(context)
                if best_choice == 'Stop':
                    if self._check_pathway_has_end(nodes) == True:
                        self.pathways.append(nodes)
                    self.choices[context].pop('Stop')
                    steps = 0
                    break

                else:
                    steps += 1
                    added_nodes, new_end_nodes = self._add_reaction(
                        best_choice)

                    if self._is_node_already_in_pathway(nodes,
                                                        added_nodes) == True:
                        self.choices[context].pop(best_choice)
                        break
                    else:
                        new_context = self._get_context(nodes + added_nodes)
                        if new_context not in self.choices:
                            self._check_if_should_expand_network(
                                new_end_nodes, nodes + added_nodes)
                            self.choices[new_context] = self._get_choices(
                                new_end_nodes)

                        if len(self.choices[new_context]) == 0:
                            self.choices[context].pop(best_choice)
                        else:
                            nodes = nodes + added_nodes
                            context = new_context
        self.log('BFS complete')
        if len(self.pathways) >= self.max_pathways:
            self.log('Max pathways reached')
        return self.pathways

    def get_pathways(self):
        pathway_objects = []
        for list_nodes in self.pathways:
            pathway = self._make_pathway(list_nodes)
            if self.allow_longer_pathways == True:
                pathway_objects.append(pathway)
            elif len(pathway.reactions) <= self.max_pathway_length:
                pathway_objects.append(pathway)
        return pathway_objects